This commit is contained in:
LagradOst 2022-08-01 03:00:48 +02:00
parent c5406acc1e
commit 3f19429805
8 changed files with 875 additions and 152 deletions

View file

@ -635,6 +635,7 @@ enum class ShowStatus {
} }
enum class DubStatus(val id: Int) { enum class DubStatus(val id: Int) {
None(-1),
Dubbed(1), Dubbed(1),
Subbed(0), Subbed(0),
} }

View file

@ -31,6 +31,8 @@ class APIRepository(val api: MainAPI) {
val mainUrl = api.mainUrl val mainUrl = api.mainUrl
val mainPage = api.mainPage val mainPage = api.mainPage
val hasQuickSearch = api.hasQuickSearch val hasQuickSearch = api.hasQuickSearch
val vpnStatus = api.vpnStatus
val providerType = api.providerType
suspend fun load(url: String): Resource<LoadResponse> { suspend fun load(url: String): Resource<LoadResponse> {
return safeApiCall { return safeApiCall {

View file

@ -111,7 +111,8 @@ data class ResultEpisode(
val name: String?, val name: String?,
val poster: String?, val poster: String?,
val episode: Int, val episode: Int,
val season: Int?, val seasonIndex: Int?, // this is the "season" index used season names
val season: Int?, // this is the display
val data: String, val data: String,
val apiName: String, val apiName: String,
val id: Int, val id: Int,
@ -146,6 +147,7 @@ fun buildResultEpisode(
name: String? = null, name: String? = null,
poster: String? = null, poster: String? = null,
episode: Int, episode: Int,
seasonIndex: Int? = null,
season: Int? = null, season: Int? = null,
data: String, data: String,
apiName: String, apiName: String,
@ -163,6 +165,7 @@ fun buildResultEpisode(
name, name,
poster, poster,
episode, episode,
seasonIndex,
season, season,
data, data,
apiName, apiName,
@ -453,7 +456,7 @@ class ResultFragment : ResultTrailerPlayer() {
private var currentLoadingCount = private var currentLoadingCount =
0 // THIS IS USED TO PREVENT LATE EVENTS, AFTER DISMISS WAS CLICKED 0 // THIS IS USED TO PREVENT LATE EVENTS, AFTER DISMISS WAS CLICKED
private lateinit var viewModel: ResultViewModel //by activityViewModels() private lateinit var viewModel: ResultViewModel2 //by activityViewModels()
private lateinit var syncModel: SyncViewModel private lateinit var syncModel: SyncViewModel
private var currentHeaderName: String? = null private var currentHeaderName: String? = null
private var currentType: TvType? = null private var currentType: TvType? = null
@ -467,7 +470,7 @@ class ResultFragment : ResultTrailerPlayer() {
savedInstanceState: Bundle?, savedInstanceState: Bundle?,
): View? { ): View? {
viewModel = viewModel =
ViewModelProvider(this)[ResultViewModel::class.java] ViewModelProvider(this)[ResultViewModel2::class.java]
syncModel = syncModel =
ViewModelProvider(this)[SyncViewModel::class.java] ViewModelProvider(this)[SyncViewModel::class.java]
@ -703,77 +706,6 @@ class ResultFragment : ResultTrailerPlayer() {
loadTrailer() loadTrailer()
} }
private fun setNextEpisode(nextAiring: NextAiring?) {
result_next_airing_holder?.isVisible =
if (nextAiring == null || nextAiring.episode <= 0 || nextAiring.unixTime <= unixTime) {
false
} else {
val seconds = nextAiring.unixTime - unixTime
val days = TimeUnit.SECONDS.toDays(seconds)
val hours: Long = TimeUnit.SECONDS.toHours(seconds) - days * 24
val minute =
TimeUnit.SECONDS.toMinutes(seconds) - TimeUnit.SECONDS.toHours(seconds) * 60
// val second =
// TimeUnit.SECONDS.toSeconds(seconds) - TimeUnit.SECONDS.toMinutes(seconds) * 60
try {
val ctx = context
if (ctx == null) {
false
} else {
when {
days > 0 -> {
ctx.getString(R.string.next_episode_time_day_format).format(
days,
hours,
minute
)
}
hours > 0 -> ctx.getString(R.string.next_episode_time_hour_format)
.format(
hours,
minute
)
minute > 0 -> ctx.getString(R.string.next_episode_time_min_format)
.format(
minute
)
else -> null
}?.also { text ->
result_next_airing_time?.text = text
result_next_airing?.text =
ctx.getString(R.string.next_episode_format)
.format(nextAiring.episode)
} != null
}
} catch (e: Exception) { // mistranslation
result_next_airing_holder?.isVisible = false
logError(e)
false
}
}
}
private fun setActors(actors: List<ActorData>?) {
if (actors.isNullOrEmpty()) {
result_cast_text?.isVisible = false
result_cast_items?.isVisible = false
} else {
val isImage = actors.first().actor.image != null
if (isImage) {
(result_cast_items?.adapter as ActorAdaptor?)?.apply {
updateList(actors)
}
result_cast_text?.isVisible = false
result_cast_items?.isVisible = true
} else {
result_cast_text?.isVisible = true
result_cast_items?.isVisible = false
setFormatText(result_cast_text, R.string.cast_format,
actors.joinToString { it.actor.name })
}
}
}
private fun setRecommendations(rec: List<SearchResponse>?, validApiName: String?) { private fun setRecommendations(rec: List<SearchResponse>?, validApiName: String?) {
val isInvalid = rec.isNullOrEmpty() val isInvalid = rec.isNullOrEmpty()
result_recommendations?.isGone = isInvalid result_recommendations?.isGone = isInvalid
@ -1424,7 +1356,12 @@ class ResultFragment : ResultTrailerPlayer() {
result_season_button?.setOnClickListener { result_season_button?.setOnClickListener {
result_season_button?.popupMenuNoIconsAndNoStringRes( result_season_button?.popupMenuNoIconsAndNoStringRes(
items = seasonList items = seasonList
.map { Pair(it ?: -2, fromIndexToSeasonText(it)) }, .map { (name, season) ->
Pair(
season ?: -2,
name ?: fromIndexToSeasonText(season)
)
},
) { ) {
val id = this.itemId val id = this.itemId
@ -1730,7 +1667,7 @@ class ResultFragment : ResultTrailerPlayer() {
startValue = null startValue = null
} }
observe(viewModel.publicEpisodes) { episodes -> observe(viewModel.episodes) { episodes ->
when (episodes) { when (episodes) {
is Resource.Failure -> { is Resource.Failure -> {
result_episode_loading?.isVisible = false result_episode_loading?.isVisible = false
@ -1753,18 +1690,17 @@ class ResultFragment : ResultTrailerPlayer() {
} }
observe(viewModel.dubStatus) { status -> observe(viewModel.dubStatus) { status ->
result_dub_select?.text = status.toString() result_dub_select?.apply {
isVisible = status != null
status?.toString()?.let {
text = it
}
}
} }
// val preferDub = context?.getApiDubstatusSettings()?.all { it == DubStatus.Dubbed } == true // val preferDub = context?.getApiDubstatusSettings()?.all { it == DubStatus.Dubbed } == true
observe(viewModel.dubSubSelections) { range -> observe(viewModel.dubSubSelections) { range ->
dubRange = range
// if (preferDub && dubRange?.contains(DubStatus.Dubbed) == true) {
// viewModel.changeDubStatus(DubStatus.Dubbed)
// }
result_dub_select?.visibility = if (range.size <= 1) GONE else VISIBLE result_dub_select?.visibility = if (range.size <= 1) GONE else VISIBLE
if (result_season_button?.isVisible != true && result_episode_select?.isVisible != true) { if (result_season_button?.isVisible != true && result_episode_select?.isVisible != true) {
@ -1810,7 +1746,7 @@ class ResultFragment : ResultTrailerPlayer() {
syncModel.publishUserData() syncModel.publishUserData()
} }
observe(viewModel.publicEpisodesCount) { count -> observe(viewModel.episodesCount) { count ->
if (count < 0) { if (count < 0) {
result_episodes_text?.isVisible = false result_episodes_text?.isVisible = false
} else { } else {
@ -1824,43 +1760,40 @@ class ResultFragment : ResultTrailerPlayer() {
currentId = it currentId = it
} }
observe(viewModel.result) { data -> observe(viewModel.page) { data ->
when (data) { when (data) {
is Resource.Success -> { is Resource.Success -> {
val d = data.value val d = data.value
if (d !is AnimeLoadResponse && result_episode_loading.isVisible) { // no episode loading when not anime
result_episode_loading.isVisible = false
}
updateVisStatus(2) updateVisStatus(2)
result_vpn?.text = when (api.vpnStatus) { result_vpn.setText(d.vpnText)
VPNStatus.MightBeNeeded -> getString(R.string.vpn_might_be_needed) result_info.setText(d.metaText)
VPNStatus.Torrent -> getString(R.string.vpn_torrent) result_no_episodes.setText(d.noEpisodesFoundText)
else -> "" result_title.setText(d.titleText)
} result_meta_site.setText(d.apiName)
result_vpn?.isGone = api.vpnStatus == VPNStatus.None result_meta_type.setText(d.typeText)
result_meta_year.setText(d.yearText)
result_meta_duration.setText(d.durationText)
result_meta_rating.setText(d.ratingText)
result_description.setTextHtml(d.plotText)
result_cast_text.setText(d.actorsText)
setRecommendations.setText(d.nextAiringEpisode)
result_next_airing_time.setText(d.nextAiringDate)
result_info?.text = when (api.providerType) { result_poster.setImage(d.posterImage)
ProviderType.MetaProvider -> getString(R.string.provider_info_meta)
else -> ""
}
result_info?.isVisible = api.providerType == ProviderType.MetaProvider
if (d.type.isEpisodeBased()) { if(!d.posterUrl.isNullOrBlank()) {
val ep = d as? TvSeriesLoadResponse result_poster?.setImage(d.posterUrl, d.posterHeaders)
val epCount = ep?.episodes?.size ?: 1 } else {
if (epCount < 1) { result_poster?.setImageResource(R.drawable.default_cover)
result_info?.text = getString(R.string.no_episodes_found)
result_info?.isVisible = true
}
} }
currentHeaderName = d.name
currentType = d.type
currentPoster = d.posterUrl result_cast_items?.isVisible = d.actors != null
currentIsMovie = !d.isEpisodeBased() (result_cast_items?.adapter as ActorAdaptor?)?.apply {
updateList(d.actors ?: emptyList())
}
result_open_in_browser?.setOnClickListener { result_open_in_browser?.setOnClickListener {
val i = Intent(ACTION_VIEW) val i = Intent(ACTION_VIEW)
@ -1873,31 +1806,21 @@ class ResultFragment : ResultTrailerPlayer() {
} }
result_search?.setOnClickListener { result_search?.setOnClickListener {
QuickSearchFragment.pushSearch(activity, d.name) QuickSearchFragment.pushSearch(activity, d.title)
} }
result_share?.setOnClickListener { result_share?.setOnClickListener {
try { try {
val i = Intent(ACTION_SEND) val i = Intent(ACTION_SEND)
i.type = "text/plain" i.type = "text/plain"
i.putExtra(EXTRA_SUBJECT, d.name) i.putExtra(EXTRA_SUBJECT, d.title)
i.putExtra(EXTRA_TEXT, d.url) i.putExtra(EXTRA_TEXT, d.url)
startActivity(createChooser(i, d.name)) startActivity(createChooser(i, d.title))
} catch (e: Exception) { } catch (e: Exception) {
logError(e) logError(e)
} }
} }
val showStatus = when (d) {
is TvSeriesLoadResponse -> d.showStatus
is AnimeLoadResponse -> d.showStatus
else -> null
}
setShow(showStatus)
setDuration(d.duration)
setYear(d.year)
setRating(d.rating)
setRecommendations(d.recommendations, null) setRecommendations(d.recommendations, null)
setActors(d.actors) setActors(d.actors)
setNextEpisode(if (d is EpisodeResponse) d.nextAiring else null) setNextEpisode(if (d is EpisodeResponse) d.nextAiring else null)
@ -1911,10 +1834,9 @@ class ResultFragment : ResultTrailerPlayer() {
} }
result_meta_site?.text = d.apiName result_meta_site?.text = d.apiName
val posterImageLink = d.posterUrl val posterImageLink = d.posterUrl
if (!posterImageLink.isNullOrEmpty()) { if (!posterImageLink.isNullOrEmpty()) {
result_poster?.setImage(posterImageLink, d.posterHeaders)
//result_poster_blur?.setImageBlur(posterImageLink, 10, 3, d.posterHeaders) //result_poster_blur?.setImageBlur(posterImageLink, 10, 3, d.posterHeaders)
//Full screen view of Poster image //Full screen view of Poster image
if (context?.isTrueTvSettings() == false) // Poster not clickable on tv if (context?.isTrueTvSettings() == false) // Poster not clickable on tv
@ -1950,7 +1872,7 @@ class ResultFragment : ResultTrailerPlayer() {
result_poster_holder?.visibility = VISIBLE result_poster_holder?.visibility = VISIBLE
result_play_movie?.text = result_play_movie?.text =
if (d.type == TvType.Live) getString(R.string.play_livestream_button) else getString( if (d.typeText == TvType.Live) getString(R.string.play_livestream_button) else getString(
R.string.play_movie_button R.string.play_movie_button
) )
//result_plot_header?.text = //result_plot_header?.text =
@ -1961,13 +1883,13 @@ class ResultFragment : ResultTrailerPlayer() {
val builder: AlertDialog.Builder = val builder: AlertDialog.Builder =
AlertDialog.Builder(requireContext(), R.style.AlertDialogCustom) AlertDialog.Builder(requireContext(), R.style.AlertDialogCustom)
builder.setMessage(syno.html()) builder.setMessage(syno.html())
.setTitle(if (d.type == TvType.Torrent) R.string.torrent_plot else R.string.result_plot) .setTitle(if (d.typeText == TvType.Torrent) R.string.torrent_plot else R.string.result_plot)
.show() .show()
} }
result_description?.text = syno.html() result_description?.text = syno.html()
} else { } else {
result_description?.text = result_description?.text =
if (d.type == TvType.Torrent) getString(R.string.torrent_no_plot) else getString( if (d.typeText == TvType.Torrent) getString(R.string.torrent_no_plot) else getString(
R.string.normal_no_plot R.string.normal_no_plot
) )
} }
@ -1982,9 +1904,8 @@ class ResultFragment : ResultTrailerPlayer() {
} }
val tags = d.tags val tags = d.tags
if (tags.isNullOrEmpty()) { result_tag_holder?.isVisible = tags.isNotEmpty()
//result_tag_holder?.visibility = GONE if (tags.isNotEmpty()) {
} else {
//result_tag_holder?.visibility = VISIBLE //result_tag_holder?.visibility = VISIBLE
val isOnTv = context?.isTrueTvSettings() == true val isOnTv = context?.isTrueTvSettings() == true
for ((index, tag) in tags.withIndex()) { for ((index, tag) in tags.withIndex()) {
@ -1997,7 +1918,7 @@ class ResultFragment : ResultTrailerPlayer() {
} }
} }
if (d.type.isMovieType()) { if (d.typeText.isMovieType()) {
val hasDownloadSupport = api.hasDownloadSupport val hasDownloadSupport = api.hasDownloadSupport
lateFixDownloadButton(true) lateFixDownloadButton(true)
@ -2125,22 +2046,6 @@ class ResultFragment : ResultTrailerPlayer() {
lateFixDownloadButton(false) lateFixDownloadButton(false)
} }
context?.getString(
when (d.type) {
TvType.TvSeries -> R.string.tv_series_singular
TvType.Anime -> R.string.anime_singular
TvType.OVA -> R.string.ova_singular
TvType.AnimeMovie -> R.string.movies_singular
TvType.Cartoon -> R.string.cartoons_singular
TvType.Documentary -> R.string.documentaries_singular
TvType.Movie -> R.string.movies_singular
TvType.Torrent -> R.string.torrent_singular
TvType.AsianDrama -> R.string.asian_drama_singular
TvType.Live -> R.string.live_singular
}
)?.let {
result_meta_type?.text = it
}
when (d) { when (d) {
is AnimeLoadResponse -> { is AnimeLoadResponse -> {

View file

@ -77,7 +77,7 @@ class ResultViewModel : ViewModel() {
val id: MutableLiveData<Int> = MutableLiveData() val id: MutableLiveData<Int> = MutableLiveData()
val selectedSeason: MutableLiveData<Int> = MutableLiveData(-2) val selectedSeason: MutableLiveData<Int> = MutableLiveData(-2)
val seasonSelections: MutableLiveData<List<Int?>> = MutableLiveData() val seasonSelections: MutableLiveData<List<Pair<String?, Int?>>> = MutableLiveData()
val dubSubSelections: LiveData<Set<DubStatus>> get() = _dubSubSelections val dubSubSelections: LiveData<Set<DubStatus>> get() = _dubSubSelections
private val _dubSubSelections: MutableLiveData<Set<DubStatus>> = MutableLiveData() private val _dubSubSelections: MutableLiveData<Set<DubStatus>> = MutableLiveData()
@ -228,14 +228,17 @@ class ResultViewModel : ViewModel() {
seasonTypes[i.season] = true seasonTypes[i.season] = true
} }
} }
val seasons = seasonTypes.toList().map { it.first }.sortedBy { it } val seasons = seasonTypes.toList().map { null to it.first }.sortedBy { it.second }
seasonSelections.postValue(seasons) seasonSelections.postValue(seasons)
if (seasons.isEmpty()) { // WHAT THE FUCK DID YOU DO????? HOW DID YOU DO THIS if (seasons.isEmpty()) { // WHAT THE FUCK DID YOU DO????? HOW DID YOU DO THIS
_publicEpisodes.postValue(Resource.Success(emptyList())) _publicEpisodes.postValue(Resource.Success(emptyList()))
return return
} }
val realSelection = if (!seasonTypes.containsKey(selection)) seasons.first() else selection val realSelection =
if (!seasonTypes.containsKey(selection)) seasons.first().second else selection
val internalId = id.value val internalId = id.value
if (internalId != null) setResultSeason(internalId, realSelection) if (internalId != null) setResultSeason(internalId, realSelection)
@ -386,7 +389,6 @@ class ResultViewModel : ViewModel() {
return return
} }
// val status = getDub(mainId)
val statuses = loadResponse.episodes.map { it.key } val statuses = loadResponse.episodes.map { it.key }
// Extremely bruh to have to take in context here, but I'm not sure how to do this in a better way :( // Extremely bruh to have to take in context here, but I'm not sure how to do this in a better way :(

View file

@ -0,0 +1,679 @@
package com.lagradost.cloudstream3.ui.result
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.getId
import com.lagradost.cloudstream3.APIHolder.unixTime
import com.lagradost.cloudstream3.animeproviders.GogoanimeProvider
import com.lagradost.cloudstream3.animeproviders.NineAnimeProvider
import com.lagradost.cloudstream3.metaproviders.SyncRedirector
import com.lagradost.cloudstream3.mvvm.*
import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.player.IGenerator
import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.FillerEpisodeCheck
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
import kotlinx.coroutines.launch
import java.util.concurrent.TimeUnit
/** This starts at 1 */
data class EpisodeRange(
// used to index data
val startIndex: Int,
val length: Int,
// used to display data
val startEpisode: Int,
val endEpisode: Int,
)
data class ResultData(
val url: String,
val tags: List<String>,
val actors: List<ActorData>?,
val actorsText: UiText?,
val comingSoon: Boolean,
val backgroundPosterUrl: String?,
val title: String,
val posterImage: UiImage?,
val plotText: UiText,
val apiName: UiText,
val ratingText: UiText?,
val vpnText: UiText?,
val metaText: UiText?,
val durationText: UiText?,
val onGoingText: UiText?,
val noEpisodesFoundText: UiText?,
val titleText: UiText,
val typeText: UiText,
val yearText: UiText?,
val nextAiringDate: UiText?,
val nextAiringEpisode: UiText?,
)
fun LoadResponse.toResultData(repo: APIRepository): ResultData {
debugAssert({ repo.name == apiName }) {
"Api returned wrong apiName"
}
val hasActorImages = actors?.firstOrNull()?.actor?.image?.isNotBlank() == true
var nextAiringEpisode: UiText? = null
var nextAiringDate: UiText? = null
if (this is EpisodeResponse) {
val airing = this.nextAiring
if (airing != null && airing.unixTime > unixTime) {
val seconds = airing.unixTime - unixTime
val days = TimeUnit.SECONDS.toDays(seconds)
val hours: Long = TimeUnit.SECONDS.toHours(seconds) - days * 24
val minute =
TimeUnit.SECONDS.toMinutes(seconds) - TimeUnit.SECONDS.toHours(seconds) * 60
nextAiringEpisode = when {
days > 0 -> {
txt(
R.string.next_episode_time_day_format,
days,
hours,
minute
)
}
hours > 0 -> txt(
R.string.next_episode_time_hour_format,
hours,
minute
)
minute > 0 -> txt(
R.string.next_episode_time_min_format,
minute
)
else -> null
}?.also {
nextAiringDate = txt(R.string.next_episode_format, airing.episode)
}
}
}
return ResultData(
nextAiringDate = nextAiringDate,
nextAiringEpisode = nextAiringEpisode,
posterImage = img(
posterUrl, posterHeaders
) ?: img(R.drawable.default_cover),
titleText = txt(name),
url = url,
tags = tags ?: emptyList(),
comingSoon = comingSoon,
actors = if (hasActorImages) actors else null,
actorsText = if (hasActorImages) null else txt(
R.string.cast_format,
actors?.joinToString { it.actor.name }),
plotText =
if (plot.isNullOrBlank()) txt(if (this is TorrentLoadResponse) R.string.torrent_no_plot else R.string.normal_no_plot) else txt(
plot!!
),
backgroundPosterUrl = backgroundPosterUrl,
title = name,
typeText = txt(
when (type) {
TvType.TvSeries -> R.string.tv_series_singular
TvType.Anime -> R.string.anime_singular
TvType.OVA -> R.string.ova_singular
TvType.AnimeMovie -> R.string.movies_singular
TvType.Cartoon -> R.string.cartoons_singular
TvType.Documentary -> R.string.documentaries_singular
TvType.Movie -> R.string.movies_singular
TvType.Torrent -> R.string.torrent_singular
TvType.AsianDrama -> R.string.asian_drama_singular
TvType.Live -> R.string.live_singular
}
),
yearText = txt(year),
apiName = txt(apiName),
ratingText = rating?.div(1000f)?.let { UiText.StringResource(R.string.rating_format, it) },
vpnText = txt(
when (repo.vpnStatus) {
VPNStatus.None -> null
VPNStatus.Torrent -> R.string.vpn_torrent
VPNStatus.MightBeNeeded -> R.string.vpn_might_be_needed
}
),
metaText =
if (repo.providerType == ProviderType.MetaProvider) txt(R.string.provider_info_meta) else null,
durationText = txt(R.string.duration_format, duration),
onGoingText = if (this is EpisodeResponse) {
txt(
when (showStatus) {
ShowStatus.Ongoing -> R.string.status_ongoing
ShowStatus.Completed -> R.string.status_completed
else -> null
}
)
} else null,
noEpisodesFoundText =
if ((this is TvSeriesLoadResponse && this.episodes.isEmpty()) || (this is AnimeLoadResponse && !this.episodes.any { it.value.isNotEmpty() })) txt(
R.string.no_episodes_found
) else null
)
}
class ResultViewModel2 : ViewModel() {
private var currentResponse: LoadResponse? = null
data class EpisodeIndexer(
val dubStatus: DubStatus,
val season: Int,
)
/** map<dub, map<season, List<episode>>> */
private var currentEpisodes: Map<EpisodeIndexer, List<ResultEpisode>> = mapOf()
private var currentRanges: Map<EpisodeIndexer, List<EpisodeRange>> = mapOf()
private var currentIndex: EpisodeIndexer? = null
private var currentRange: EpisodeRange? = null
private var currentShowFillers: Boolean = false
private var currentRepo: APIRepository? = null
private var currentId: Int? = null
private var fillers: Map<Int, Boolean> = emptyMap()
private var generator: IGenerator? = null
private var preferDubStatus: DubStatus? = null
private var preferStartEpisode: Int? = null
private var preferStartSeason: Int? = null
private val _page: MutableLiveData<Resource<ResultData>> =
MutableLiveData(Resource.Loading())
val page: LiveData<Resource<ResultData>> = _page
private val _episodes: MutableLiveData<Resource<List<ResultEpisode>>> =
MutableLiveData(Resource.Loading())
val episodes: LiveData<Resource<List<ResultEpisode>>> = _episodes
private val _episodesCount: MutableLiveData<Int> =
MutableLiveData(0)
val episodesCount: LiveData<Int> = _episodesCount
private val _trailers: MutableLiveData<List<TrailerData>> = MutableLiveData(mutableListOf())
val trailers: LiveData<List<TrailerData>> = _trailers
private val _dubStatus: MutableLiveData<DubStatus?> = MutableLiveData(null)
val dubStatus: LiveData<DubStatus?> = _dubStatus
private val _dubSubSelections: MutableLiveData<List<DubStatus>> = MutableLiveData(emptyList())
val dubSubSelections: LiveData<List<DubStatus>> = _dubSubSelections
companion object {
private const val EPISODE_RANGE_SIZE = 50
private const val EPISODE_RANGE_OVERLOAD = 60
private fun filterName(name: String?): String? {
if (name == null) return null
Regex("[eE]pisode [0-9]*(.*)").find(name)?.groupValues?.get(1)?.let {
if (it.isEmpty())
return null
}
return name
}
fun singleMap(ep: ResultEpisode): Map<EpisodeIndexer, List<ResultEpisode>> =
mapOf(
EpisodeIndexer(DubStatus.None, 0) to listOf(
ep
)
)
private fun getRanges(allEpisodes: Map<EpisodeIndexer, List<ResultEpisode>>): Map<EpisodeIndexer, List<EpisodeRange>> {
return allEpisodes.keys.mapNotNull { index ->
val episodes =
allEpisodes[index] ?: return@mapNotNull null // this should never happened
// fast case
if (episodes.size <= EPISODE_RANGE_OVERLOAD) {
return@mapNotNull index to listOf(
EpisodeRange(
0,
episodes.size,
episodes.minOf { it.episode },
episodes.maxOf { it.episode })
)
}
if (episodes.isEmpty()) {
return@mapNotNull null
}
val list = mutableListOf<EpisodeRange>()
val currentEpisode = episodes.first()
var currentIndex = 0
val maxIndex = episodes.size
var targetEpisode = 0
var currentMin = currentEpisode.episode
var currentMax = currentEpisode.episode
while (currentIndex < maxIndex) {
val startIndex = currentIndex
targetEpisode += EPISODE_RANGE_SIZE
while (currentIndex < maxIndex && episodes[currentIndex].episode <= targetEpisode) {
val episodeNumber = episodes[currentIndex].episode
if (episodeNumber < currentMin) {
currentMin = episodeNumber
} else if (episodeNumber > currentMax) {
currentMax = episodeNumber
}
++currentIndex
}
val length = currentIndex - startIndex
if (length <= 0) continue
list.add(
EpisodeRange(
startIndex,
length,
currentMin,
currentMax
)
)
}
/*var currentMin = Int.MAX_VALUE
var currentMax = Int.MIN_VALUE
var currentStartIndex = 0
var currentLength = 0
for (ep in episodes) {
val episodeNumber = ep.episode
if (episodeNumber < currentMin) {
currentMin = episodeNumber
} else if (episodeNumber > currentMax) {
currentMax = episodeNumber
}
if (++currentLength >= EPISODE_RANGE_SIZE) {
list.add(
EpisodeRange(
currentStartIndex,
currentLength,
currentMin,
currentMax
)
)
currentMin = Int.MAX_VALUE
currentMax = Int.MIN_VALUE
currentStartIndex += currentLength
currentLength = 0
}
}
if (currentLength > 0) {
list.add(
EpisodeRange(
currentStartIndex,
currentLength,
currentMin,
currentMax
)
)
}*/
index to list
}.toMap()
}
}
private suspend fun updateFillers(name: String) {
fillers =
try {
FillerEpisodeCheck.getFillerEpisodes(name)
} catch (e: Exception) {
logError(e)
null
} ?: emptyMap()
}
fun changeDubStatus(status: DubStatus) {
postEpisodeRange(currentIndex?.copy(dubStatus = status), currentRange)
}
fun changeRange(range: EpisodeRange) {
postEpisodeRange(currentIndex, range)
}
private fun getEpisodes(indexer: EpisodeIndexer, range: EpisodeRange): List<ResultEpisode> {
//TODO ADD GENERATOR
val startIndex = range.startIndex
val length = range.length
return currentEpisodes[indexer]
?.let { list ->
val start = minOf(list.size, startIndex)
val end = minOf(list.size, start + length)
list.subList(start, end).map {
val posDur = DataStoreHelper.getViewPos(it.id)
it.copy(position = posDur?.position ?: 0, duration = posDur?.duration ?: 0)
}
}
?: emptyList()
}
fun reloadEpisodes() {
_episodes.postValue(
Resource.Success(
getEpisodes(
currentIndex ?: return,
currentRange ?: return
)
)
)
}
private fun postEpisodeRange(indexer: EpisodeIndexer?, range: EpisodeRange?) {
if (range == null || indexer == null) {
return
}
currentIndex = indexer
currentRange = range
//TODO SET KEYS
preferStartEpisode = range.startEpisode
preferStartSeason = indexer.season
preferDubStatus = indexer.dubStatus
val ret = getEpisodes(indexer, range)
_episodes.postValue(Resource.Success(ret))
}
private suspend fun postSuccessful(
loadResponse: LoadResponse,
apiRepository: APIRepository,
updateEpisodes: Boolean,
updateFillers: Boolean,
) {
currentResponse = loadResponse
postPage(loadResponse, apiRepository)
if (updateEpisodes)
postEpisodes(loadResponse, updateFillers)
}
private suspend fun postEpisodes(loadResponse: LoadResponse, updateFillers: Boolean) {
_episodes.postValue(Resource.Loading())
val mainId = loadResponse.getId()
currentId = mainId
if (updateFillers && loadResponse is AnimeLoadResponse) {
updateFillers(loadResponse.name)
}
val allEpisodes = when (loadResponse) {
is AnimeLoadResponse -> {
val existingEpisodes = HashSet<Int>()
val episodes: MutableMap<EpisodeIndexer, MutableList<ResultEpisode>> =
mutableMapOf()
loadResponse.episodes.map { ep ->
val idIndex = ep.key.id
for ((index, i) in ep.value.withIndex()) {
val episode = i.episode ?: (index + 1)
val id = mainId + episode + idIndex * 1000000
if (!existingEpisodes.contains(episode)) {
existingEpisodes.add(id)
val eps =
buildResultEpisode(
loadResponse.name,
filterName(i.name),
i.posterUrl,
episode,
null,
i.season,
i.data,
loadResponse.apiName,
id,
index,
i.rating,
i.description,
fillers.getOrDefault(episode, false),
loadResponse.type,
mainId
)
val season = eps.season ?: 0
val indexer = EpisodeIndexer(ep.key, season)
episodes[indexer]?.add(eps) ?: run {
episodes[indexer] = mutableListOf(eps)
}
}
}
}
episodes
}
is TvSeriesLoadResponse -> {
val episodes: MutableMap<EpisodeIndexer, MutableList<ResultEpisode>> =
mutableMapOf()
val existingEpisodes = HashSet<Int>()
for ((index, episode) in loadResponse.episodes.sortedBy {
(it.season?.times(10000) ?: 0) + (it.episode ?: 0)
}.withIndex()) {
val episodeIndex = episode.episode ?: (index + 1)
val id =
mainId + (episode.season?.times(100000) ?: 0) + episodeIndex + 1
if (!existingEpisodes.contains(id)) {
existingEpisodes.add(id)
val seasonIndex = episode.season?.minus(1)
val currentSeason =
loadResponse.seasonNames?.getOrNull(seasonIndex ?: -1)
val ep =
buildResultEpisode(
loadResponse.name,
filterName(episode.name),
episode.posterUrl,
episodeIndex,
seasonIndex,
currentSeason?.season ?: episode.season,
episode.data,
loadResponse.apiName,
id,
index,
episode.rating,
episode.description,
null,
loadResponse.type,
mainId
)
val season = episode.season ?: 0
val indexer = EpisodeIndexer(DubStatus.None, season)
episodes[indexer]?.add(ep) ?: kotlin.run {
episodes[indexer] = mutableListOf(ep)
}
}
}
episodes
}
is MovieLoadResponse -> {
singleMap(
buildResultEpisode(
loadResponse.name,
loadResponse.name,
null,
0,
null,
null,
loadResponse.dataUrl,
loadResponse.apiName,
(mainId), // HAS SAME ID
0,
null,
null,
null,
loadResponse.type,
mainId
)
)
}
is LiveStreamLoadResponse -> {
singleMap(
buildResultEpisode(
loadResponse.name,
loadResponse.name,
null,
0,
null,
null,
loadResponse.dataUrl,
loadResponse.apiName,
(mainId), // HAS SAME ID
0,
null,
null,
null,
loadResponse.type,
mainId
)
)
}
is TorrentLoadResponse -> {
singleMap(
buildResultEpisode(
loadResponse.name,
loadResponse.name,
null,
0,
null,
null,
loadResponse.torrent ?: loadResponse.magnet ?: "",
loadResponse.apiName,
(mainId), // HAS SAME ID
0,
null,
null,
null,
loadResponse.type,
mainId
)
)
}
else -> {
mapOf()
}
}
currentEpisodes = allEpisodes
val ranges = getRanges(allEpisodes)
currentRanges = ranges
// this takes the indexer most preferable by the user given the current sorting
val min = ranges.keys.minByOrNull { index ->
kotlin.math.abs(
index.season - (preferStartSeason ?: 0)
) + if (index.dubStatus == preferDubStatus) 0 else 100000
}
// this takes the range most preferable by the user given the current sorting
val ranger = ranges[min]
val range = ranger?.firstOrNull {
it.startEpisode >= (preferStartEpisode ?: 0)
} ?: ranger?.lastOrNull()
postEpisodeRange(min, range)
}
// this instantly updates the metadata on the page
private fun postPage(loadResponse: LoadResponse, apiRepository: APIRepository) {
_page.postValue(Resource.Success(loadResponse.toResultData(apiRepository)))
_trailers.postValue(loadResponse.trailers)
}
fun load(
url: String,
apiName: String,
showFillers: Boolean,
dubStatus: DubStatus,
startEpisode: Int,
startSeason: Int
) =
viewModelScope.launch {
_page.postValue(Resource.Loading(url))
_episodes.postValue(Resource.Loading(url))
preferDubStatus = dubStatus
currentShowFillers = showFillers
preferStartEpisode = startEpisode
preferStartSeason = startSeason
// set api
val api = APIHolder.getApiFromNameNull(apiName) ?: APIHolder.getApiFromUrlNull(url)
if (api == null) {
_page.postValue(
Resource.Failure(
false,
null,
null,
"This provider does not exist"
)
)
return@launch
}
// validate url
val validUrlResource = safeApiCall {
SyncRedirector.redirect(
url,
api.mainUrl.replace(NineAnimeProvider().mainUrl, "9anime")
.replace(GogoanimeProvider().mainUrl, "gogoanime")
)
}
if (validUrlResource !is Resource.Success) {
if (validUrlResource is Resource.Failure) {
_page.postValue(validUrlResource)
}
return@launch
}
val validUrl = validUrlResource.value
val repo = APIRepository(api)
currentRepo = repo
when (val data = repo.load(validUrl)) {
is Resource.Failure -> {
_page.postValue(data)
}
is Resource.Success -> {
val loadResponse = data.value
val mainId = loadResponse.getId()
AcraApplication.setKey(
DOWNLOAD_HEADER_CACHE,
mainId.toString(),
VideoDownloadHelper.DownloadHeaderCached(
apiName,
validUrl,
loadResponse.type,
loadResponse.name,
loadResponse.posterUrl,
mainId,
System.currentTimeMillis(),
)
)
postSuccessful(
data.value,
updateEpisodes = true,
updateFillers = showFillers,
apiRepository = repo
)
}
is Resource.Loading -> {
debugException { "Invalid load result" }
}
}
}
}

View file

@ -0,0 +1,126 @@
package com.lagradost.cloudstream3.ui.result
import android.content.Context
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.core.view.isGone
import androidx.core.view.isVisible
import com.lagradost.cloudstream3.utils.AppUtils.html
import com.lagradost.cloudstream3.utils.UIHelper.setImage
sealed class UiText {
data class DynamicString(val value: String) : UiText()
class StringResource(
@StringRes val resId: Int,
vararg val args: Any
) : UiText()
fun asStringNull(context: Context?): String? {
return asString(context ?: return null)
}
fun asString(context: Context): String {
return when (this) {
is DynamicString -> value
is StringResource -> context.getString(resId, *args)
}
}
}
sealed class UiImage {
data class Image(
val url: String,
val headers: Map<String, String>? = null,
@DrawableRes val errorDrawable: Int? = null
) : UiImage()
data class Drawable(@DrawableRes val resId: Int) : UiImage()
}
fun ImageView?.setImage(value: UiImage?) {
when (value) {
is UiImage.Image -> setImageImage(value)
is UiImage.Drawable -> setImageDrawable(value)
null -> {
this?.isVisible = false
}
}
}
fun ImageView?.setImageImage(value: UiImage.Image) {
if (this == null) return
this.isVisible = setImage(value.url, value.headers, value.errorDrawable)
}
fun ImageView?.setImageDrawable(value: UiImage.Drawable) {
if (this == null) return
this.isVisible = true
setImageResource(value.resId)
}
@JvmName("imgNull")
fun img(
url: String?,
headers: Map<String, String>? = null,
@DrawableRes errorDrawable: Int? = null
): UiImage? {
if (url.isNullOrBlank()) return null
return UiImage.Image(url, headers, errorDrawable)
}
fun img(
url: String,
headers: Map<String, String>? = null,
@DrawableRes errorDrawable: Int? = null
): UiImage {
return UiImage.Image(url, headers, errorDrawable)
}
fun img(@DrawableRes drawable: Int): UiImage {
return UiImage.Drawable(drawable)
}
fun txt(value: String): UiText {
return UiText.DynamicString(value)
}
@JvmName("txtNull")
fun txt(value: String?): UiText? {
return UiText.DynamicString(value ?: return null)
}
fun txt(@StringRes resId: Int, vararg args: Any): UiText {
return UiText.StringResource(resId, args)
}
@JvmName("txtNull")
fun txt(@StringRes resId: Int?, vararg args: Any?): UiText? {
if (resId == null || args.any { it == null }) {
return null
}
return UiText.StringResource(resId, args)
}
fun TextView?.setText(text: UiText?) {
if (this == null) return
if (text == null) {
this.isVisible = false
} else {
val str = text.asStringNull(context)
this.isGone = str.isNullOrBlank()
this.text = str
}
}
fun TextView?.setTextHtml(text: UiText?) {
if (this == null) return
if (text == null) {
this.isVisible = false
} else {
val str = text.asStringNull(context)
this.isGone = str.isNullOrBlank()
this.text = str.html()
}
}

View file

@ -464,6 +464,14 @@
android:textColor="?attr/grayTextColor" android:textColor="?attr/grayTextColor"
android:textSize="15sp" android:textSize="15sp"
tools:text="@string/provider_info_meta" /> tools:text="@string/provider_info_meta" />
<TextView
android:id="@+id/result_no_episodes"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:textColor="?attr/grayTextColor"
android:textSize="15sp"
tools:text="@string/no_episodes_found" />
<TextView <TextView
android:id="@+id/result_tag_holder" android:id="@+id/result_tag_holder"