forked from recloudstream/cloudstream
not done
This commit is contained in:
parent
c5406acc1e
commit
3f19429805
8 changed files with 875 additions and 152 deletions
|
@ -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),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 -> {
|
||||||
|
|
|
@ -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 :(
|
||||||
|
|
|
@ -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" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
126
app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt
Normal file
126
app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue