forked from recloudstream/cloudstream
Merge remote-tracking branch 'origin/master'
This commit is contained in:
commit
2f69fffe87
18 changed files with 349 additions and 134 deletions
|
@ -36,7 +36,7 @@ android {
|
||||||
targetSdkVersion 30
|
targetSdkVersion 30
|
||||||
|
|
||||||
versionCode 45
|
versionCode 45
|
||||||
versionName "2.9.18"
|
versionName "2.9.19"
|
||||||
|
|
||||||
resValue "string", "app_version",
|
resValue "string", "app_version",
|
||||||
"${defaultConfig.versionName}${versionNameSuffix ?: ""}"
|
"${defaultConfig.versionName}${versionNameSuffix ?: ""}"
|
||||||
|
@ -96,8 +96,8 @@ dependencies {
|
||||||
implementation 'androidx.appcompat:appcompat:1.4.1'
|
implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||||
implementation 'com.google.android.material:material:1.5.0'
|
implementation 'com.google.android.material:material:1.5.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.0-alpha03'
|
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.0-alpha04'
|
||||||
implementation 'androidx.navigation:navigation-ui-ktx:2.5.0-alpha03'
|
implementation 'androidx.navigation:navigation-ui-ktx:2.5.0-alpha04'
|
||||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1'
|
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1'
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
|
@ -126,6 +126,8 @@ dependencies {
|
||||||
implementation 'com.google.android.exoplayer:exoplayer:2.16.1'
|
implementation 'com.google.android.exoplayer:exoplayer:2.16.1'
|
||||||
implementation 'com.google.android.exoplayer:extension-cast:2.16.1'
|
implementation 'com.google.android.exoplayer:extension-cast:2.16.1'
|
||||||
implementation "com.google.android.exoplayer:extension-mediasession:2.16.1"
|
implementation "com.google.android.exoplayer:extension-mediasession:2.16.1"
|
||||||
|
implementation 'com.google.android.exoplayer:extension-okhttp:2.16.1'
|
||||||
|
|
||||||
//implementation "com.google.android.exoplayer:extension-leanback:2.14.0"
|
//implementation "com.google.android.exoplayer:extension-leanback:2.14.0"
|
||||||
|
|
||||||
// Bug reports
|
// Bug reports
|
||||||
|
@ -154,7 +156,6 @@ dependencies {
|
||||||
// Networking
|
// Networking
|
||||||
implementation "com.squareup.okhttp3:okhttp:4.9.2"
|
implementation "com.squareup.okhttp3:okhttp:4.9.2"
|
||||||
implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1"
|
implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1"
|
||||||
implementation 'com.google.android.exoplayer:extension-okhttp:2.16.1'
|
|
||||||
|
|
||||||
// Util to skip the URI file fuckery 🙏
|
// Util to skip the URI file fuckery 🙏
|
||||||
implementation "com.github.tachiyomiorg:unifile:17bec43"
|
implementation "com.github.tachiyomiorg:unifile:17bec43"
|
||||||
|
|
|
@ -197,7 +197,8 @@ class NineAnimeProvider : MainAPI() {
|
||||||
)
|
)
|
||||||
|
|
||||||
override suspend fun load(url: String): LoadResponse? {
|
override suspend fun load(url: String): LoadResponse? {
|
||||||
val doc = app.get(url).document
|
val validUrl = url.replace("https://9anime.to", mainUrl)
|
||||||
|
val doc = app.get(validUrl).document
|
||||||
val animeid = doc.selectFirst("div.player-wrapper.watchpage").attr("data-id") ?: return null
|
val animeid = doc.selectFirst("div.player-wrapper.watchpage").attr("data-id") ?: return null
|
||||||
val animeidencoded = encode(getVrf(animeid) ?: return null)
|
val animeidencoded = encode(getVrf(animeid) ?: return null)
|
||||||
val poster = doc.selectFirst("aside.main div.thumb div img").attr("src")
|
val poster = doc.selectFirst("aside.main div.thumb div img").attr("src")
|
||||||
|
@ -233,7 +234,7 @@ class NineAnimeProvider : MainAPI() {
|
||||||
else null
|
else null
|
||||||
val tags = doc.select("div.info .meta .col1 div:contains(Genre) a").map { it.text() }
|
val tags = doc.select("div.info .meta .col1 div:contains(Genre) a").map { it.text() }
|
||||||
|
|
||||||
return newAnimeLoadResponse(title, url, tvType) {
|
return newAnimeLoadResponse(title, validUrl, tvType) {
|
||||||
this.posterUrl = poster
|
this.posterUrl = poster
|
||||||
this.plot = description
|
this.plot = description
|
||||||
this.recommendations = recommendations
|
this.recommendations = recommendations
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
package com.lagradost.cloudstream3.metaproviders
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.ErrorLoadingException
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.SyncApis
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.aniListApi
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.malApi
|
||||||
|
import com.lagradost.cloudstream3.utils.SyncUtil
|
||||||
|
|
||||||
|
object SyncRedirector {
|
||||||
|
val syncApis = SyncApis
|
||||||
|
|
||||||
|
suspend fun redirect(url: String, preferredUrl: String): String {
|
||||||
|
for (api in syncApis) {
|
||||||
|
if (url.contains(api.mainUrl)) {
|
||||||
|
val otherApi = when (api.name) {
|
||||||
|
aniListApi.name -> "anilist"
|
||||||
|
malApi.name -> "myanimelist"
|
||||||
|
else -> return url
|
||||||
|
}
|
||||||
|
|
||||||
|
return SyncUtil.getUrlsFromId(api.getIdFromUrl(url), otherApi).firstOrNull { realUrl ->
|
||||||
|
realUrl.contains(preferredUrl)
|
||||||
|
} ?: run {
|
||||||
|
throw ErrorLoadingException("Page does not exist on $preferredUrl")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,13 +4,26 @@ import com.lagradost.cloudstream3.*
|
||||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId
|
import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId
|
||||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
||||||
import com.lagradost.cloudstream3.syncproviders.OAuth2API
|
import com.lagradost.cloudstream3.syncproviders.OAuth2API
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.providers.AniListApi
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.providers.MALApi
|
||||||
|
import com.lagradost.cloudstream3.utils.SyncUtil
|
||||||
|
|
||||||
|
// wont be implemented
|
||||||
class MultiAnimeProvider : MainAPI() {
|
class MultiAnimeProvider : MainAPI() {
|
||||||
override var name = "MultiAnime"
|
override var name = "MultiAnime"
|
||||||
override val lang = "en"
|
override val lang = "en"
|
||||||
override val usesWebView = true
|
override val usesWebView = true
|
||||||
override val supportedTypes = setOf(TvType.Anime)
|
override val supportedTypes = setOf(TvType.Anime)
|
||||||
private val syncApi = OAuth2API.aniListApi
|
private val syncApi: SyncAPI = OAuth2API.aniListApi
|
||||||
|
|
||||||
|
private val syncUtilType by lazy {
|
||||||
|
when (syncApi) {
|
||||||
|
is AniListApi -> "anilist"
|
||||||
|
is MALApi -> "myanimelist"
|
||||||
|
else -> throw ErrorLoadingException("Invalid Api")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val validApis by lazy {
|
private val validApis by lazy {
|
||||||
APIHolder.apis.filter {
|
APIHolder.apis.filter {
|
||||||
|
@ -32,13 +45,25 @@ class MultiAnimeProvider : MainAPI() {
|
||||||
|
|
||||||
override suspend fun load(url: String): LoadResponse? {
|
override suspend fun load(url: String): LoadResponse? {
|
||||||
return syncApi.getResult(url)?.let { res ->
|
return syncApi.getResult(url)?.let { res ->
|
||||||
newAnimeLoadResponse(res.title!!, url, TvType.Anime) {
|
val data = SyncUtil.getUrlsFromId(res.id, syncUtilType).apmap { url ->
|
||||||
|
validApis.firstOrNull { api -> url.startsWith(api.mainUrl) }?.load(url)
|
||||||
|
}.filterNotNull()
|
||||||
|
|
||||||
|
val type =
|
||||||
|
if (data.any { it.type == TvType.AnimeMovie }) TvType.AnimeMovie else TvType.Anime
|
||||||
|
|
||||||
|
newAnimeLoadResponse(
|
||||||
|
res.title ?: throw ErrorLoadingException("No Title found"),
|
||||||
|
url,
|
||||||
|
type
|
||||||
|
) {
|
||||||
posterUrl = res.posterUrl
|
posterUrl = res.posterUrl
|
||||||
plot = res.synopsis
|
plot = res.synopsis
|
||||||
tags = res.genres
|
tags = res.genres
|
||||||
rating = res.publicScore
|
rating = res.publicScore
|
||||||
addTrailer(res.trailerUrl)
|
addTrailer(res.trailerUrl)
|
||||||
addAniListId(res.id.toIntOrNull())
|
addAniListId(res.id.toIntOrNull())
|
||||||
|
recommendations = res.recommendations
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -218,31 +218,26 @@ class NginxProvider : MainAPI() {
|
||||||
|
|
||||||
if (isMovieType) {
|
if (isMovieType) {
|
||||||
val movieName = nfoContent.select("title").text()
|
val movieName = nfoContent.select("title").text()
|
||||||
|
|
||||||
val posterUrl = mediaRootUrl + "poster.jpg"
|
val posterUrl = mediaRootUrl + "poster.jpg"
|
||||||
|
return@mapNotNull newMovieSearchResponse(
|
||||||
return@mapNotNull MovieSearchResponse(
|
|
||||||
movieName,
|
movieName,
|
||||||
mediaRootUrl,
|
mediaRootUrl,
|
||||||
this.name,
|
|
||||||
TvType.Movie,
|
TvType.Movie,
|
||||||
posterUrl,
|
) {
|
||||||
null,
|
addPoster(posterUrl, authHeader)
|
||||||
)
|
}
|
||||||
} else { // tv serie
|
} else { // tv serie
|
||||||
val serieName = nfoContent.select("title").text()
|
val serieName = nfoContent.select("title").text()
|
||||||
|
|
||||||
val posterUrl = mediaRootUrl + "poster.jpg"
|
val posterUrl = mediaRootUrl + "poster.jpg"
|
||||||
|
|
||||||
TvSeriesSearchResponse(
|
newTvSeriesSearchResponse(
|
||||||
serieName,
|
serieName,
|
||||||
nfoPath,
|
nfoPath,
|
||||||
this.name,
|
|
||||||
TvType.TvSeries,
|
TvType.TvSeries,
|
||||||
posterUrl,
|
) {
|
||||||
null,
|
addPoster(posterUrl, authHeader)
|
||||||
null,
|
}
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -605,14 +605,12 @@ open class SflixProvider : MainAPI() {
|
||||||
M3u8Helper().m3u8Generation(M3u8Helper.M3u8Stream(this.file, null), true)
|
M3u8Helper().m3u8Generation(M3u8Helper.M3u8Stream(this.file, null), true)
|
||||||
.map { stream ->
|
.map { stream ->
|
||||||
//println("stream: ${stream.quality} at ${stream.streamUrl}")
|
//println("stream: ${stream.quality} at ${stream.streamUrl}")
|
||||||
val qualityString = if ((stream.quality ?: 0) == 0) label
|
|
||||||
?: "" else "${stream.quality}p"
|
|
||||||
ExtractorLink(
|
ExtractorLink(
|
||||||
caller.name,
|
caller.name,
|
||||||
"${caller.name} $qualityString $name",
|
"${caller.name} $name",
|
||||||
stream.streamUrl,
|
stream.streamUrl,
|
||||||
caller.mainUrl,
|
caller.mainUrl,
|
||||||
getQualityFromName(stream.quality.toString()),
|
getQualityFromName(stream.quality?.toString()),
|
||||||
true,
|
true,
|
||||||
extractorData = extractorData
|
extractorData = extractorData
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package com.lagradost.cloudstream3.syncproviders
|
package com.lagradost.cloudstream3.syncproviders
|
||||||
|
|
||||||
import com.lagradost.cloudstream3.ActorData
|
import com.lagradost.cloudstream3.*
|
||||||
import com.lagradost.cloudstream3.ShowStatus
|
|
||||||
|
|
||||||
interface SyncAPI : OAuth2API {
|
interface SyncAPI : OAuth2API {
|
||||||
val icon: Int
|
val icon: Int
|
||||||
|
@ -24,13 +23,19 @@ interface SyncAPI : OAuth2API {
|
||||||
|
|
||||||
suspend fun search(name: String): List<SyncSearchResult>?
|
suspend fun search(name: String): List<SyncSearchResult>?
|
||||||
|
|
||||||
|
fun getIdFromUrl(url : String) : String
|
||||||
|
|
||||||
data class SyncSearchResult(
|
data class SyncSearchResult(
|
||||||
val name: String,
|
override val name: String,
|
||||||
val syncApiName: String,
|
override val apiName: String,
|
||||||
val id: String,
|
var syncId: String,
|
||||||
val url: String,
|
override val url: String,
|
||||||
val posterUrl: String?,
|
override var posterUrl: String?,
|
||||||
)
|
override var type: TvType? = null,
|
||||||
|
override var quality: SearchQuality? = null,
|
||||||
|
override var posterHeaders: Map<String, String>? = null,
|
||||||
|
override var id: Int? = null,
|
||||||
|
) : SearchResponse
|
||||||
|
|
||||||
data class SyncNextAiring(
|
data class SyncNextAiring(
|
||||||
val episode: Int,
|
val episode: Int,
|
||||||
|
|
|
@ -9,6 +9,7 @@ class SyncRepo(private val repo: SyncAPI) {
|
||||||
val idPrefix = repo.idPrefix
|
val idPrefix = repo.idPrefix
|
||||||
val name = repo.name
|
val name = repo.name
|
||||||
val icon = repo.icon
|
val icon = repo.icon
|
||||||
|
val mainUrl = repo.mainUrl
|
||||||
|
|
||||||
suspend fun score(id: String, status: SyncAPI.SyncStatus): Resource<Boolean> {
|
suspend fun score(id: String, status: SyncAPI.SyncStatus): Resource<Boolean> {
|
||||||
return safeApiCall { repo.score(id, status) }
|
return safeApiCall { repo.score(id, status) }
|
||||||
|
@ -29,4 +30,6 @@ class SyncRepo(private val repo: SyncAPI) {
|
||||||
fun hasAccount() : Boolean {
|
fun hasAccount() : Boolean {
|
||||||
return normalSafeApiCall { repo.loginInfo() != null } ?: false
|
return normalSafeApiCall { repo.loginInfo() != null } ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getIdFromUrl(url : String) : String = repo.getIdFromUrl(url)
|
||||||
}
|
}
|
|
@ -70,6 +70,14 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
return user != null
|
return user != null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getIdFromUrl(url : String): String {
|
||||||
|
return url.removePrefix("$mainUrl/anime/").removeSuffix("/")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getUrlFromId(id: Int): String {
|
||||||
|
return "$mainUrl/anime/$id"
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun search(name: String): List<SyncAPI.SyncSearchResult>? {
|
override suspend fun search(name: String): List<SyncAPI.SyncSearchResult>? {
|
||||||
val data = searchShows(name) ?: return null
|
val data = searchShows(name) ?: return null
|
||||||
return data.data?.Page?.media?.map {
|
return data.data?.Page?.media?.map {
|
||||||
|
@ -77,7 +85,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
it.title.romaji ?: return null,
|
it.title.romaji ?: return null,
|
||||||
this.name,
|
this.name,
|
||||||
it.id.toString(),
|
it.id.toString(),
|
||||||
"$mainUrl/anime/${it.id}",
|
getUrlFromId(it.id),
|
||||||
it.bannerImage
|
it.bannerImage
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -126,7 +134,16 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
publicScore = season.averageScore?.times(100),
|
publicScore = season.averageScore?.times(100),
|
||||||
//recommendations = season.
|
recommendations = season.recommendations?.edges?.mapNotNull { rec ->
|
||||||
|
val recMedia = rec.node.mediaRecommendation
|
||||||
|
SyncAPI.SyncSearchResult(
|
||||||
|
name = recMedia.title?.userPreferred ?: return@mapNotNull null,
|
||||||
|
this.name,
|
||||||
|
recMedia.id?.toString() ?: return@mapNotNull null,
|
||||||
|
getUrlFromId(recMedia.id),
|
||||||
|
recMedia.coverImage?.large ?: recMedia.coverImage?.medium
|
||||||
|
)
|
||||||
|
}
|
||||||
//TODO REST
|
//TODO REST
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -396,6 +413,27 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
recommendations {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
mediaRecommendation {
|
||||||
|
id
|
||||||
|
coverImage {
|
||||||
|
extraLarge
|
||||||
|
large
|
||||||
|
medium
|
||||||
|
color
|
||||||
|
}
|
||||||
|
title {
|
||||||
|
romaji
|
||||||
|
english
|
||||||
|
native
|
||||||
|
userPreferred
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
nextAiringEpisode {
|
nextAiringEpisode {
|
||||||
timeUntilAiring
|
timeUntilAiring
|
||||||
episode
|
episode
|
||||||
|
@ -772,6 +810,21 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
@JsonProperty("trailer") val trailer: MediaTrailer?,
|
@JsonProperty("trailer") val trailer: MediaTrailer?,
|
||||||
@JsonProperty("description") val description: String?,
|
@JsonProperty("description") val description: String?,
|
||||||
@JsonProperty("characters") val characters: CharacterConnection?,
|
@JsonProperty("characters") val characters: CharacterConnection?,
|
||||||
|
@JsonProperty("recommendations") val recommendations: RecommendationConnection?,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class RecommendationConnection(
|
||||||
|
@JsonProperty("edges") val edges: List<RecommendationEdge> = emptyList(),
|
||||||
|
@JsonProperty("nodes") val nodes: List<Recommendation> = emptyList(),
|
||||||
|
//@JsonProperty("pageInfo") val pageInfo: PageInfo,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class RecommendationEdge(
|
||||||
|
//@JsonProperty("rating") val rating: Int,
|
||||||
|
@JsonProperty("node") val node: Recommendation,
|
||||||
|
)
|
||||||
|
data class Recommendation(
|
||||||
|
@JsonProperty("mediaRecommendation") val mediaRecommendation: SeasonMedia,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class CharacterName(
|
data class CharacterName(
|
||||||
|
@ -955,13 +1008,6 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
@JsonProperty("data") val data: LikeData?,
|
@JsonProperty("data") val data: LikeData?,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Recommendation(
|
|
||||||
@JsonProperty("title") val title: String?,
|
|
||||||
@JsonProperty("idMal") val idMal: Int?,
|
|
||||||
@JsonProperty("poster") val poster: String?,
|
|
||||||
@JsonProperty("averageScore") val averageScore: Int?
|
|
||||||
)
|
|
||||||
|
|
||||||
data class AniListTitleHolder(
|
data class AniListTitleHolder(
|
||||||
@JsonProperty("title") val title: Title?,
|
@JsonProperty("title") val title: Title?,
|
||||||
@JsonProperty("isFavourite") val isFavourite: Boolean?,
|
@JsonProperty("isFavourite") val isFavourite: Boolean?,
|
||||||
|
|
|
@ -81,6 +81,10 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getIdFromUrl(url: String): String {
|
||||||
|
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.SyncStatus): Boolean {
|
||||||
return setScoreRequest(
|
return setScoreRequest(
|
||||||
id.toIntOrNull() ?: return false,
|
id.toIntOrNull() ?: return false,
|
||||||
|
@ -173,8 +177,8 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
private fun toSearchResult(node: Node?): SyncAPI.SyncSearchResult? {
|
private fun toSearchResult(node: Node?): SyncAPI.SyncSearchResult? {
|
||||||
return SyncAPI.SyncSearchResult(
|
return SyncAPI.SyncSearchResult(
|
||||||
name = node?.title ?: return null,
|
name = node?.title ?: return null,
|
||||||
syncApiName = this.name,
|
apiName = this.name,
|
||||||
id = node.id.toString(),
|
syncId = node.id.toString(),
|
||||||
url = "https://myanimelist.net/anime/${node.id}",
|
url = "https://myanimelist.net/anime/${node.id}",
|
||||||
posterUrl = node.main_picture?.large
|
posterUrl = node.main_picture?.large
|
||||||
)
|
)
|
||||||
|
|
|
@ -93,6 +93,7 @@ import com.lagradost.cloudstream3.utils.VideoDownloadManager.getFileName
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename
|
||||||
import kotlinx.android.synthetic.main.fragment_result.*
|
import kotlinx.android.synthetic.main.fragment_result.*
|
||||||
import kotlinx.android.synthetic.main.fragment_result_swipe.*
|
import kotlinx.android.synthetic.main.fragment_result_swipe.*
|
||||||
|
import kotlinx.android.synthetic.main.result_recommendations.*
|
||||||
import kotlinx.android.synthetic.main.result_sync.*
|
import kotlinx.android.synthetic.main.result_sync.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
@ -572,14 +573,6 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
|
||||||
setFormatText(result_meta_rating, R.string.rating_format, rating?.div(1000f))
|
setFormatText(result_meta_rating, R.string.rating_format, rating?.div(1000f))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setMalSync(id: Int?): Boolean {
|
|
||||||
return syncModel.setMalId(id?.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setAniListSync(id: Int?): Boolean {
|
|
||||||
return syncModel.setAniListId(id?.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setActors(actors: List<ActorData>?) {
|
private fun setActors(actors: List<ActorData>?) {
|
||||||
if (actors.isNullOrEmpty()) {
|
if (actors.isNullOrEmpty()) {
|
||||||
result_cast_text?.isVisible = false
|
result_cast_text?.isVisible = false
|
||||||
|
@ -601,23 +594,47 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setRecommendations(rec: List<SearchResponse>?) {
|
private fun setRecommendations(rec: List<SearchResponse>?, validApiName: String?) {
|
||||||
val isInvalid = rec.isNullOrEmpty()
|
val isInvalid = rec.isNullOrEmpty()
|
||||||
result_recommendations?.isGone = isInvalid
|
result_recommendations?.isGone = isInvalid
|
||||||
result_recommendations_btt?.isGone = isInvalid
|
result_recommendations_btt?.isGone = isInvalid
|
||||||
result_recommendations_btt?.setOnClickListener {
|
result_recommendations_btt?.setOnClickListener {
|
||||||
if (result_overlapping_panels?.getSelectedPanel()?.ordinal == 1) {
|
val nextFocusDown = if (result_overlapping_panels?.getSelectedPanel()?.ordinal == 1) {
|
||||||
result_recommendations_btt?.nextFocusDownId = R.id.result_recommendations
|
|
||||||
result_overlapping_panels?.openEndPanel()
|
result_overlapping_panels?.openEndPanel()
|
||||||
|
R.id.result_recommendations
|
||||||
} else {
|
} else {
|
||||||
result_recommendations_btt?.nextFocusDownId = R.id.result_description
|
|
||||||
result_overlapping_panels?.closePanels()
|
result_overlapping_panels?.closePanels()
|
||||||
|
R.id.result_description
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result_recommendations_btt?.nextFocusDownId = nextFocusDown
|
||||||
|
result_search?.nextFocusDownId = nextFocusDown
|
||||||
|
result_open_in_browser?.nextFocusDownId = nextFocusDown
|
||||||
|
result_share?.nextFocusDownId = nextFocusDown
|
||||||
}
|
}
|
||||||
result_overlapping_panels?.setEndPanelLockState(if (isInvalid) OverlappingPanelsLayout.LockState.CLOSE else OverlappingPanelsLayout.LockState.UNLOCKED)
|
result_overlapping_panels?.setEndPanelLockState(if (isInvalid) OverlappingPanelsLayout.LockState.CLOSE else OverlappingPanelsLayout.LockState.UNLOCKED)
|
||||||
|
|
||||||
|
val matchAgainst = validApiName ?: rec?.firstOrNull()?.apiName
|
||||||
|
rec?.map { it.apiName }?.distinct()?.let { apiNames ->
|
||||||
|
// very dirty selection
|
||||||
|
result_recommendations_filter_button?.isVisible = apiNames.size > 1
|
||||||
|
result_recommendations_filter_button?.text = matchAgainst
|
||||||
|
result_recommendations_filter_button?.setOnClickListener { _ ->
|
||||||
|
activity?.showBottomDialog(
|
||||||
|
apiNames,
|
||||||
|
apiNames.indexOf(matchAgainst),
|
||||||
|
getString(R.string.home_change_provider_img_des), false, {}
|
||||||
|
) {
|
||||||
|
setRecommendations(rec, apiNames[it])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ?: run {
|
||||||
|
result_recommendations_filter_button?.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
result_recommendations?.post {
|
result_recommendations?.post {
|
||||||
rec?.let { list ->
|
rec?.let { list ->
|
||||||
(result_recommendations?.adapter as SearchAdapter?)?.updateList(list)
|
(result_recommendations?.adapter as SearchAdapter?)?.updateList(list.filter { it.apiName == matchAgainst })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1086,7 +1103,6 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
|
||||||
ACTION_PLAY_EPISODE_IN_PLAYER -> {
|
ACTION_PLAY_EPISODE_IN_PLAYER -> {
|
||||||
viewModel.getGenerator(episodeClick.data)
|
viewModel.getGenerator(episodeClick.data)
|
||||||
?.let { generator ->
|
?.let { generator ->
|
||||||
println("LANUCJ:::: $syncdata")
|
|
||||||
activity?.navigate(
|
activity?.navigate(
|
||||||
R.id.global_to_navigation_player,
|
R.id.global_to_navigation_player,
|
||||||
GeneratorPlayer.newInstance(
|
GeneratorPlayer.newInstance(
|
||||||
|
@ -1641,7 +1657,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
|
||||||
setDuration(d.duration)
|
setDuration(d.duration)
|
||||||
setYear(d.year)
|
setYear(d.year)
|
||||||
setRating(d.rating)
|
setRating(d.rating)
|
||||||
setRecommendations(d.recommendations)
|
setRecommendations(d.recommendations, null)
|
||||||
setActors(d.actors)
|
setActors(d.actors)
|
||||||
|
|
||||||
if (SettingsFragment.accountEnabled) {
|
if (SettingsFragment.accountEnabled) {
|
||||||
|
@ -1950,7 +1966,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result_recommendations.adapter = recAdapter
|
result_recommendations?.adapter = recAdapter
|
||||||
|
|
||||||
context?.let { ctx ->
|
context?.let { ctx ->
|
||||||
result_bookmark_button?.isVisible = ctx.isTvSettings()
|
result_bookmark_button?.isVisible = ctx.isTvSettings()
|
||||||
|
|
|
@ -11,6 +11,9 @@ import com.lagradost.cloudstream3.APIHolder.getApiFromUrlNull
|
||||||
import com.lagradost.cloudstream3.APIHolder.getId
|
import com.lagradost.cloudstream3.APIHolder.getId
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
||||||
|
import com.lagradost.cloudstream3.animeproviders.GogoanimeProvider
|
||||||
|
import com.lagradost.cloudstream3.animeproviders.NineAnimeProvider
|
||||||
|
import com.lagradost.cloudstream3.metaproviders.SyncRedirector
|
||||||
import com.lagradost.cloudstream3.mvvm.Resource
|
import com.lagradost.cloudstream3.mvvm.Resource
|
||||||
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
||||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
||||||
|
@ -127,6 +130,17 @@ class ResultViewModel : ViewModel() {
|
||||||
addTrailer(meta.trailerUrl)
|
addTrailer(meta.trailerUrl)
|
||||||
posterUrl = posterUrl ?: meta.posterUrl ?: meta.backgroundPosterUrl
|
posterUrl = posterUrl ?: meta.posterUrl ?: meta.backgroundPosterUrl
|
||||||
actors = actors ?: meta.actors
|
actors = actors ?: meta.actors
|
||||||
|
|
||||||
|
val realRecommendations = ArrayList<SearchResponse>()
|
||||||
|
val apiNames = listOf(GogoanimeProvider().name, NineAnimeProvider().name)
|
||||||
|
meta.recommendations?.forEach { rec ->
|
||||||
|
apiNames.forEach { name ->
|
||||||
|
realRecommendations.add(rec.copy(apiName = name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
recommendations = recommendations?.union(realRecommendations)?.toList()
|
||||||
|
?: realRecommendations
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -296,10 +310,9 @@ class ResultViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun load(url: String, apiName: String, showFillers: Boolean) = viewModelScope.launch {
|
fun load(url: String, apiName: String, showFillers: Boolean) = viewModelScope.launch {
|
||||||
_resultResponse.postValue(Resource.Loading(url))
|
|
||||||
_publicEpisodes.postValue(Resource.Loading())
|
_publicEpisodes.postValue(Resource.Loading())
|
||||||
|
_resultResponse.postValue(Resource.Loading(url))
|
||||||
|
|
||||||
_apiName.postValue(apiName)
|
|
||||||
val api = getApiFromNameNull(apiName) ?: getApiFromUrlNull(url)
|
val api = getApiFromNameNull(apiName) ?: getApiFromUrlNull(url)
|
||||||
if (api == null) {
|
if (api == null) {
|
||||||
_resultResponse.postValue(
|
_resultResponse.postValue(
|
||||||
|
@ -312,9 +325,31 @@ class ResultViewModel : ViewModel() {
|
||||||
)
|
)
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
_resultResponse.postValue(validUrlResource)
|
||||||
|
}
|
||||||
|
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
val validUrl = validUrlResource.value
|
||||||
|
|
||||||
|
_resultResponse.postValue(Resource.Loading(validUrl))
|
||||||
|
|
||||||
|
_apiName.postValue(apiName)
|
||||||
|
|
||||||
repo = APIRepository(api)
|
repo = APIRepository(api)
|
||||||
|
|
||||||
val data = repo?.load(url) ?: return@launch
|
val data = repo?.load(validUrl) ?: return@launch
|
||||||
|
|
||||||
_resultResponse.postValue(data)
|
_resultResponse.postValue(data)
|
||||||
|
|
||||||
|
@ -331,7 +366,7 @@ class ResultViewModel : ViewModel() {
|
||||||
mainId.toString(),
|
mainId.toString(),
|
||||||
VideoDownloadHelper.DownloadHeaderCached(
|
VideoDownloadHelper.DownloadHeaderCached(
|
||||||
apiName,
|
apiName,
|
||||||
url,
|
validUrl,
|
||||||
d.type,
|
d.type,
|
||||||
d.name,
|
d.name,
|
||||||
d.posterUrl,
|
d.posterUrl,
|
||||||
|
|
|
@ -82,16 +82,16 @@ class SyncViewModel : ViewModel() {
|
||||||
var isValid = false
|
var isValid = false
|
||||||
|
|
||||||
map?.forEach { (prefix, id) ->
|
map?.forEach { (prefix, id) ->
|
||||||
isValid = isValid || addSync(prefix, id)
|
isValid = addSync(prefix, id) || isValid
|
||||||
}
|
}
|
||||||
return isValid
|
return isValid
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setMalId(id: String?): Boolean {
|
private fun setMalId(id: String?): Boolean {
|
||||||
return addSync(malApi.idPrefix, id ?: return false)
|
return addSync(malApi.idPrefix, id ?: return false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setAniListId(id: String?): Boolean {
|
private fun setAniListId(id: String?): Boolean {
|
||||||
return addSync(aniListApi.idPrefix, id ?: return false)
|
return addSync(aniListApi.idPrefix, id ?: return false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,6 @@ import com.lagradost.cloudstream3.SearchQuality
|
||||||
import com.lagradost.cloudstream3.SearchResponse
|
import com.lagradost.cloudstream3.SearchResponse
|
||||||
import com.lagradost.cloudstream3.TvType
|
import com.lagradost.cloudstream3.TvType
|
||||||
import com.lagradost.cloudstream3.syncproviders.OAuth2API
|
import com.lagradost.cloudstream3.syncproviders.OAuth2API
|
||||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
|
||||||
|
|
||||||
class SyncSearchViewModel {
|
class SyncSearchViewModel {
|
||||||
private val repos = OAuth2API.SyncApis
|
private val repos = OAuth2API.SyncApis
|
||||||
|
@ -20,15 +19,4 @@ class SyncSearchViewModel {
|
||||||
override var posterHeaders: Map<String, String>? = null,
|
override var posterHeaders: Map<String, String>? = null,
|
||||||
) : SearchResponse
|
) : SearchResponse
|
||||||
|
|
||||||
private fun SyncAPI.SyncSearchResult.toSearchResponse(): SyncSearchResultSearchResponse {
|
|
||||||
return SyncSearchResultSearchResponse(
|
|
||||||
this.name,
|
|
||||||
this.url,
|
|
||||||
this.syncApiName,
|
|
||||||
null,
|
|
||||||
this.posterUrl,
|
|
||||||
null, //this.id.hashCode()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -7,6 +7,7 @@ import com.google.android.gms.cast.framework.CastSession
|
||||||
import com.google.android.gms.cast.framework.media.RemoteMediaClient
|
import com.google.android.gms.cast.framework.media.RemoteMediaClient
|
||||||
import com.google.android.gms.common.api.PendingResult
|
import com.google.android.gms.common.api.PendingResult
|
||||||
import com.google.android.gms.common.images.WebImage
|
import com.google.android.gms.common.images.WebImage
|
||||||
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.ui.MetadataHolder
|
import com.lagradost.cloudstream3.ui.MetadataHolder
|
||||||
import com.lagradost.cloudstream3.ui.player.SubtitleData
|
import com.lagradost.cloudstream3.ui.player.SubtitleData
|
||||||
import com.lagradost.cloudstream3.ui.result.ResultEpisode
|
import com.lagradost.cloudstream3.ui.result.ResultEpisode
|
||||||
|
@ -64,7 +65,10 @@ object CastHelper {
|
||||||
return builder.build()
|
return builder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun awaitLinks(pending: PendingResult<RemoteMediaClient.MediaChannelResult>?, callback: (Boolean) -> Unit) {
|
fun awaitLinks(
|
||||||
|
pending: PendingResult<RemoteMediaClient.MediaChannelResult>?,
|
||||||
|
callback: (Boolean) -> Unit
|
||||||
|
) {
|
||||||
if (pending == null) return
|
if (pending == null) return
|
||||||
main {
|
main {
|
||||||
val res = withContext(Dispatchers.IO) { pending.await() }
|
val res = withContext(Dispatchers.IO) { pending.await() }
|
||||||
|
@ -90,6 +94,7 @@ object CastHelper {
|
||||||
startIndex: Int? = null,
|
startIndex: Int? = null,
|
||||||
startTime: Long? = null,
|
startTime: Long? = null,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
|
try {
|
||||||
if (this == null) return false
|
if (this == null) return false
|
||||||
if (episodes.isEmpty()) return false
|
if (episodes.isEmpty()) return false
|
||||||
if (currentEpisodeIndex >= episodes.size) return false
|
if (currentEpisodeIndex >= episodes.size) return false
|
||||||
|
@ -97,7 +102,16 @@ object CastHelper {
|
||||||
val epData = episodes[currentEpisodeIndex]
|
val epData = episodes[currentEpisodeIndex]
|
||||||
|
|
||||||
val holder =
|
val holder =
|
||||||
MetadataHolder(apiName, isMovie, title, poster, currentEpisodeIndex, episodes, currentLinks, subtitles)
|
MetadataHolder(
|
||||||
|
apiName,
|
||||||
|
isMovie,
|
||||||
|
title,
|
||||||
|
poster,
|
||||||
|
currentEpisodeIndex,
|
||||||
|
episodes,
|
||||||
|
currentLinks,
|
||||||
|
subtitles
|
||||||
|
)
|
||||||
|
|
||||||
val index = if (startIndex == null || startIndex < 0) 0 else startIndex
|
val index = if (startIndex == null || startIndex < 0) 0 else startIndex
|
||||||
|
|
||||||
|
@ -106,7 +120,8 @@ object CastHelper {
|
||||||
|
|
||||||
awaitLinks(
|
awaitLinks(
|
||||||
this.remoteMediaClient?.load(
|
this.remoteMediaClient?.load(
|
||||||
MediaLoadRequestData.Builder().setMediaInfo(mediaItem).setCurrentTime(startTime ?: 0L).build()
|
MediaLoadRequestData.Builder().setMediaInfo(mediaItem)
|
||||||
|
.setCurrentTime(startTime ?: 0L).build()
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
if (currentLinks.size > index + 1)
|
if (currentLinks.size > index + 1)
|
||||||
|
@ -124,5 +139,9 @@ object CastHelper {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logError(e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -53,7 +53,7 @@ object SyncUtil {
|
||||||
* valid sites are: Gogoanime, Twistmoe and 9anime*/
|
* valid sites are: Gogoanime, Twistmoe and 9anime*/
|
||||||
private suspend fun getIdsFromSlug(
|
private suspend fun getIdsFromSlug(
|
||||||
slug: String,
|
slug: String,
|
||||||
site: String = "GogoanimeGogoanime"
|
site: String = "Gogoanime"
|
||||||
): Pair<String?, String?>? {
|
): Pair<String?, String?>? {
|
||||||
Log.i(TAG, "getIdsFromSlug $slug $site")
|
Log.i(TAG, "getIdsFromSlug $slug $site")
|
||||||
try {
|
try {
|
||||||
|
@ -76,6 +76,28 @@ object SyncUtil {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getUrlsFromId(id: String, type: String = "anilist") : List<String> {
|
||||||
|
val url =
|
||||||
|
"https://raw.githubusercontent.com/MALSync/MAL-Sync-Backup/master/data/$type/anime/$id.json"
|
||||||
|
val response = app.get(url, cacheTime = 1, cacheUnit = TimeUnit.DAYS).mapped<SyncPage>()
|
||||||
|
val pages = response.pages ?: return emptyList()
|
||||||
|
return pages.gogoanime.values.union(pages.nineanime.values).union(pages.twistmoe.values).mapNotNull { it.url }
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SyncPage(
|
||||||
|
@JsonProperty("Pages") val pages: SyncPages?,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SyncPages(
|
||||||
|
@JsonProperty("9anime") val nineanime: Map<String, ProviderPage> = emptyMap(),
|
||||||
|
@JsonProperty("Gogoanime") val gogoanime: Map<String, ProviderPage> = emptyMap(),
|
||||||
|
@JsonProperty("Twistmoe") val twistmoe: Map<String, ProviderPage> = emptyMap(),
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ProviderPage(
|
||||||
|
@JsonProperty("url") val url: String?,
|
||||||
|
)
|
||||||
|
|
||||||
data class MalSyncPage(
|
data class MalSyncPage(
|
||||||
@JsonProperty("identifier") val identifier: String?,
|
@JsonProperty("identifier") val identifier: String?,
|
||||||
@JsonProperty("type") val type: String?,
|
@JsonProperty("type") val type: String?,
|
||||||
|
|
|
@ -180,17 +180,7 @@
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_gravity="end">
|
android:layout_gravity="end">
|
||||||
|
|
||||||
<com.lagradost.cloudstream3.ui.AutofitRecyclerView
|
<include layout="@layout/result_recommendations" />
|
||||||
android:descendantFocusability="afterDescendants"
|
|
||||||
|
|
||||||
android:background="?attr/primaryBlackBackground"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
app:spanCount="3"
|
|
||||||
android:id="@+id/result_recommendations"
|
|
||||||
tools:listitem="@layout/search_result_grid"
|
|
||||||
android:orientation="vertical" />
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
</com.discord.panels.OverlappingPanelsLayout>
|
</com.discord.panels.OverlappingPanelsLayout>
|
||||||
|
|
||||||
|
|
37
app/src/main/res/layout/result_recommendations.xml
Normal file
37
app/src/main/res/layout/result_recommendations.xml
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
style="@style/BlackButton"
|
||||||
|
android:layout_gravity="center_vertical|end"
|
||||||
|
android:text="GogoAnime"
|
||||||
|
android:id="@+id/result_recommendations_filter_button"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:layout_marginBottom="10dp"
|
||||||
|
android:layout_marginStart="0dp"
|
||||||
|
android:layout_marginEnd="0dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/result_recommendations" />
|
||||||
|
|
||||||
|
<com.lagradost.cloudstream3.ui.AutofitRecyclerView
|
||||||
|
android:descendantFocusability="afterDescendants"
|
||||||
|
|
||||||
|
android:background="?attr/primaryBlackBackground"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
app:spanCount="3"
|
||||||
|
android:id="@+id/result_recommendations"
|
||||||
|
tools:listitem="@layout/search_result_grid"
|
||||||
|
android:orientation="vertical" />
|
||||||
|
</LinearLayout>
|
||||||
|
</androidx.core.widget.NestedScrollView>
|
Loading…
Reference in a new issue