From 3f1942980548360d48f7485e417185cac5c8f743 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Mon, 1 Aug 2022 03:00:48 +0200 Subject: [PATCH 01/22] not done --- .../com/lagradost/cloudstream3/MainAPI.kt | 1 + ...eflvProvider.kt => AnimeflvnetProvider.kt} | 0 .../cloudstream3/ui/APIRepository.kt | 2 + .../cloudstream3/ui/result/ResultFragment.kt | 201 ++---- .../cloudstream3/ui/result/ResultViewModel.kt | 10 +- .../ui/result/ResultViewModel2.kt | 679 ++++++++++++++++++ .../cloudstream3/ui/result/UiText.kt | 126 ++++ app/src/main/res/layout/fragment_result.xml | 8 + 8 files changed, 875 insertions(+), 152 deletions(-) rename app/src/main/java/com/lagradost/cloudstream3/animeproviders/{AnimeflvProvider.kt => AnimeflvnetProvider.kt} (100%) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 368c9986..0c600fc7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -635,6 +635,7 @@ enum class ShowStatus { } enum class DubStatus(val id: Int) { + None(-1), Dubbed(1), Subbed(0), } diff --git a/app/src/main/java/com/lagradost/cloudstream3/animeproviders/AnimeflvProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/animeproviders/AnimeflvnetProvider.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/animeproviders/AnimeflvProvider.kt rename to app/src/main/java/com/lagradost/cloudstream3/animeproviders/AnimeflvnetProvider.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt index 0283760d..34cb262c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt @@ -31,6 +31,8 @@ class APIRepository(val api: MainAPI) { val mainUrl = api.mainUrl val mainPage = api.mainPage val hasQuickSearch = api.hasQuickSearch + val vpnStatus = api.vpnStatus + val providerType = api.providerType suspend fun load(url: String): Resource { return safeApiCall { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt index e5b13abf..e3ec3786 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt @@ -111,7 +111,8 @@ data class ResultEpisode( val name: String?, val poster: String?, 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 apiName: String, val id: Int, @@ -146,6 +147,7 @@ fun buildResultEpisode( name: String? = null, poster: String? = null, episode: Int, + seasonIndex: Int? = null, season: Int? = null, data: String, apiName: String, @@ -163,6 +165,7 @@ fun buildResultEpisode( name, poster, episode, + seasonIndex, season, data, apiName, @@ -453,7 +456,7 @@ class ResultFragment : ResultTrailerPlayer() { private var currentLoadingCount = 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 var currentHeaderName: String? = null private var currentType: TvType? = null @@ -467,7 +470,7 @@ class ResultFragment : ResultTrailerPlayer() { savedInstanceState: Bundle?, ): View? { viewModel = - ViewModelProvider(this)[ResultViewModel::class.java] + ViewModelProvider(this)[ResultViewModel2::class.java] syncModel = ViewModelProvider(this)[SyncViewModel::class.java] @@ -703,77 +706,6 @@ class ResultFragment : ResultTrailerPlayer() { 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?) { - 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?, validApiName: String?) { val isInvalid = rec.isNullOrEmpty() result_recommendations?.isGone = isInvalid @@ -1424,7 +1356,12 @@ class ResultFragment : ResultTrailerPlayer() { result_season_button?.setOnClickListener { result_season_button?.popupMenuNoIconsAndNoStringRes( items = seasonList - .map { Pair(it ?: -2, fromIndexToSeasonText(it)) }, + .map { (name, season) -> + Pair( + season ?: -2, + name ?: fromIndexToSeasonText(season) + ) + }, ) { val id = this.itemId @@ -1730,7 +1667,7 @@ class ResultFragment : ResultTrailerPlayer() { startValue = null } - observe(viewModel.publicEpisodes) { episodes -> + observe(viewModel.episodes) { episodes -> when (episodes) { is Resource.Failure -> { result_episode_loading?.isVisible = false @@ -1753,18 +1690,17 @@ class ResultFragment : ResultTrailerPlayer() { } 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 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 if (result_season_button?.isVisible != true && result_episode_select?.isVisible != true) { @@ -1810,7 +1746,7 @@ class ResultFragment : ResultTrailerPlayer() { syncModel.publishUserData() } - observe(viewModel.publicEpisodesCount) { count -> + observe(viewModel.episodesCount) { count -> if (count < 0) { result_episodes_text?.isVisible = false } else { @@ -1824,43 +1760,40 @@ class ResultFragment : ResultTrailerPlayer() { currentId = it } - observe(viewModel.result) { data -> + observe(viewModel.page) { data -> when (data) { is Resource.Success -> { 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) - result_vpn?.text = when (api.vpnStatus) { - VPNStatus.MightBeNeeded -> getString(R.string.vpn_might_be_needed) - VPNStatus.Torrent -> getString(R.string.vpn_torrent) - else -> "" - } - result_vpn?.isGone = api.vpnStatus == VPNStatus.None + result_vpn.setText(d.vpnText) + result_info.setText(d.metaText) + result_no_episodes.setText(d.noEpisodesFoundText) + result_title.setText(d.titleText) + result_meta_site.setText(d.apiName) + 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) { - ProviderType.MetaProvider -> getString(R.string.provider_info_meta) - else -> "" - } - result_info?.isVisible = api.providerType == ProviderType.MetaProvider + result_poster.setImage(d.posterImage) - if (d.type.isEpisodeBased()) { - val ep = d as? TvSeriesLoadResponse - val epCount = ep?.episodes?.size ?: 1 - if (epCount < 1) { - result_info?.text = getString(R.string.no_episodes_found) - result_info?.isVisible = true - } + if(!d.posterUrl.isNullOrBlank()) { + result_poster?.setImage(d.posterUrl, d.posterHeaders) + } else { + result_poster?.setImageResource(R.drawable.default_cover) } - currentHeaderName = d.name - currentType = d.type - currentPoster = d.posterUrl - currentIsMovie = !d.isEpisodeBased() + result_cast_items?.isVisible = d.actors != null + (result_cast_items?.adapter as ActorAdaptor?)?.apply { + updateList(d.actors ?: emptyList()) + } result_open_in_browser?.setOnClickListener { val i = Intent(ACTION_VIEW) @@ -1873,31 +1806,21 @@ class ResultFragment : ResultTrailerPlayer() { } result_search?.setOnClickListener { - QuickSearchFragment.pushSearch(activity, d.name) + QuickSearchFragment.pushSearch(activity, d.title) } result_share?.setOnClickListener { try { val i = Intent(ACTION_SEND) i.type = "text/plain" - i.putExtra(EXTRA_SUBJECT, d.name) + i.putExtra(EXTRA_SUBJECT, d.title) i.putExtra(EXTRA_TEXT, d.url) - startActivity(createChooser(i, d.name)) + startActivity(createChooser(i, d.title)) } catch (e: Exception) { 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) setActors(d.actors) setNextEpisode(if (d is EpisodeResponse) d.nextAiring else null) @@ -1911,10 +1834,9 @@ class ResultFragment : ResultTrailerPlayer() { } result_meta_site?.text = d.apiName - val posterImageLink = d.posterUrl if (!posterImageLink.isNullOrEmpty()) { - result_poster?.setImage(posterImageLink, d.posterHeaders) + //result_poster_blur?.setImageBlur(posterImageLink, 10, 3, d.posterHeaders) //Full screen view of Poster image if (context?.isTrueTvSettings() == false) // Poster not clickable on tv @@ -1950,7 +1872,7 @@ class ResultFragment : ResultTrailerPlayer() { result_poster_holder?.visibility = VISIBLE 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 ) //result_plot_header?.text = @@ -1961,13 +1883,13 @@ class ResultFragment : ResultTrailerPlayer() { val builder: AlertDialog.Builder = AlertDialog.Builder(requireContext(), R.style.AlertDialogCustom) 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() } result_description?.text = syno.html() } else { 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 ) } @@ -1982,9 +1904,8 @@ class ResultFragment : ResultTrailerPlayer() { } val tags = d.tags - if (tags.isNullOrEmpty()) { - //result_tag_holder?.visibility = GONE - } else { + result_tag_holder?.isVisible = tags.isNotEmpty() + if (tags.isNotEmpty()) { //result_tag_holder?.visibility = VISIBLE val isOnTv = context?.isTrueTvSettings() == true 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 lateFixDownloadButton(true) @@ -2125,22 +2046,6 @@ class ResultFragment : ResultTrailerPlayer() { 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) { is AnimeLoadResponse -> { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt index 1cdd5e24..03f8b802 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt @@ -77,7 +77,7 @@ class ResultViewModel : ViewModel() { val id: MutableLiveData = MutableLiveData() val selectedSeason: MutableLiveData = MutableLiveData(-2) - val seasonSelections: MutableLiveData> = MutableLiveData() + val seasonSelections: MutableLiveData>> = MutableLiveData() val dubSubSelections: LiveData> get() = _dubSubSelections private val _dubSubSelections: MutableLiveData> = MutableLiveData() @@ -228,14 +228,17 @@ class ResultViewModel : ViewModel() { 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) if (seasons.isEmpty()) { // WHAT THE FUCK DID YOU DO????? HOW DID YOU DO THIS _publicEpisodes.postValue(Resource.Success(emptyList())) 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 if (internalId != null) setResultSeason(internalId, realSelection) @@ -386,7 +389,6 @@ class ResultViewModel : ViewModel() { return } -// val status = getDub(mainId) 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 :( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt new file mode 100644 index 00000000..4dc5db6c --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt @@ -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, + val actors: List?, + 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>> */ + private var currentEpisodes: Map> = mapOf() + private var currentRanges: Map> = 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 = 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> = + MutableLiveData(Resource.Loading()) + val page: LiveData> = _page + + private val _episodes: MutableLiveData>> = + MutableLiveData(Resource.Loading()) + val episodes: LiveData>> = _episodes + + private val _episodesCount: MutableLiveData = + MutableLiveData(0) + val episodesCount: LiveData = _episodesCount + + private val _trailers: MutableLiveData> = MutableLiveData(mutableListOf()) + val trailers: LiveData> = _trailers + + private val _dubStatus: MutableLiveData = MutableLiveData(null) + val dubStatus: LiveData = _dubStatus + + private val _dubSubSelections: MutableLiveData> = MutableLiveData(emptyList()) + val dubSubSelections: LiveData> = _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> = + mapOf( + EpisodeIndexer(DubStatus.None, 0) to listOf( + ep + ) + ) + + private fun getRanges(allEpisodes: Map>): Map> { + 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() + + 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 { + //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() + val episodes: MutableMap> = + 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> = + mutableMapOf() + val existingEpisodes = HashSet() + 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" } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt new file mode 100644 index 00000000..e55fe873 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt @@ -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? = 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? = null, + @DrawableRes errorDrawable: Int? = null +): UiImage? { + if (url.isNullOrBlank()) return null + return UiImage.Image(url, headers, errorDrawable) +} + +fun img( + url: String, + headers: Map? = 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() + } +} diff --git a/app/src/main/res/layout/fragment_result.xml b/app/src/main/res/layout/fragment_result.xml index fa35b2c8..228a3731 100644 --- a/app/src/main/res/layout/fragment_result.xml +++ b/app/src/main/res/layout/fragment_result.xml @@ -464,6 +464,14 @@ android:textColor="?attr/grayTextColor" android:textSize="15sp" tools:text="@string/provider_info_meta" /> + Date: Mon, 1 Aug 2022 04:46:43 +0200 Subject: [PATCH 02/22] not done 2 --- .../cloudstream3/ui/result/ResultFragment.kt | 353 +++++------------- .../ui/result/ResultViewModel2.kt | 207 +++++++++- .../cloudstream3/ui/result/UiText.kt | 8 +- app/src/main/res/layout/fragment_result.xml | 8 +- app/src/main/res/values/strings.xml | 3 + 5 files changed, 312 insertions(+), 267 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt index e3ec3786..f9309da2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt @@ -21,7 +21,6 @@ import android.view.View.GONE import android.view.View.VISIBLE import android.view.ViewGroup import android.widget.* -import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog import androidx.core.content.FileProvider import androidx.core.view.isGone @@ -39,7 +38,6 @@ import com.google.android.material.button.MaterialButton import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.getApiFromName import com.lagradost.cloudstream3.APIHolder.getId -import com.lagradost.cloudstream3.APIHolder.unixTime import com.lagradost.cloudstream3.APIHolder.updateHasTrailers import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.getCastSession @@ -84,7 +82,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIcons import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes import com.lagradost.cloudstream3.utils.UIHelper.requestRW -import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.VideoDownloadManager.getFileName import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename import kotlinx.android.synthetic.main.fragment_result.* @@ -95,10 +92,8 @@ import kotlinx.android.synthetic.main.result_sync.* import kotlinx.android.synthetic.main.trailer_custom_layout.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import java.io.File -import java.util.concurrent.TimeUnit const val START_ACTION_NORMAL = 0 const val START_ACTION_RESUME_LATEST = 1 @@ -458,11 +453,6 @@ class ResultFragment : ResultTrailerPlayer() { 0 // THIS IS USED TO PREVENT LATE EVENTS, AFTER DISMISS WAS CLICKED private lateinit var viewModel: ResultViewModel2 //by activityViewModels() private lateinit var syncModel: SyncViewModel - private var currentHeaderName: String? = null - private var currentType: TvType? = null - private var currentEpisodes: List? = null - private var downloadButton: EasyDownloadButton? = null - private var syncdata: Map? = null override fun onCreateView( inflater: LayoutInflater, @@ -490,13 +480,6 @@ class ResultFragment : ResultTrailerPlayer() { super.onDestroyView() } - override fun onDestroy() { - //requireActivity().viewModelStore.clear() // REMEMBER THE CLEAR - - - super.onDestroy() - } - override fun onResume() { super.onResume() activity?.let { @@ -563,52 +546,6 @@ class ResultFragment : ResultTrailerPlayer() { var startAction: Int? = null private var startValue: Int? = null - private fun setFormatText(textView: TextView?, @StringRes format: Int, arg: Any?) { - // java.util.IllegalFormatConversionException: f != java.lang.Integer - // This can fail with malformed formatting - normalSafeApiCall { - if (arg == null) { - textView?.isVisible = false - } else { - val text = context?.getString(format)?.format(arg) - if (text == null) { - textView?.isVisible = false - } else { - textView?.isVisible = true - textView?.text = text - } - } - } - } - - private fun setDuration(duration: Int?) { - setFormatText(result_meta_duration, R.string.duration_format, duration) - } - - private fun setShow(showStatus: ShowStatus?) { - val status = when (showStatus) { - null -> null - ShowStatus.Ongoing -> R.string.status_ongoing - ShowStatus.Completed -> R.string.status_completed - } - - if (status == null) { - result_meta_status?.isVisible = false - } else { - context?.getString(status)?.let { - result_meta_status?.text = it - } - } - } - - private fun setYear(year: Int?) { - setFormatText(result_meta_year, R.string.year_format, year) - } - - private fun setRating(rating: Int?) { - setFormatText(result_meta_rating, R.string.rating_format, rating?.div(1000f)) - } - var currentTrailers: List = emptyList() var currentTrailerIndex = 0 @@ -1339,54 +1276,7 @@ class ResultFragment : ResultTrailerPlayer() { result_episode_select?.isFocusableInTouchMode = context?.isTvSettings() == true result_dub_select?.isFocusableInTouchMode = context?.isTvSettings() == true - observe(viewModel.selectedSeason) { season -> - result_season_button?.text = fromIndexToSeasonText(season) - } - observe(viewModel.seasonSelections) { seasonList -> - result_season_button?.visibility = if (seasonList.size <= 1) GONE else VISIBLE.also { - - // If the season button is visible the result season button will be next focus down - if (result_series_parent?.isVisible == true) - setFocusUpAndDown(result_resume_series_button, result_season_button) - else - setFocusUpAndDown(result_bookmark_button, result_season_button) - } - - result_season_button?.setOnClickListener { - result_season_button?.popupMenuNoIconsAndNoStringRes( - items = seasonList - .map { (name, season) -> - Pair( - season ?: -2, - name ?: fromIndexToSeasonText(season) - ) - }, - ) { - val id = this.itemId - - viewModel.changeSeason(if (id == -2) null else id) - } - } - } - - observe(viewModel.selectedRange) { range -> - result_episode_select?.text = range - } - - observe(viewModel.rangeOptions) { range -> - episodeRanges = range - result_episode_select?.visibility = if (range.size <= 1) GONE else VISIBLE.also { - - // If Season button is invisible then the bookmark button next focus is episode select - if (result_season_button?.isVisible != true) { - if (result_series_parent?.isVisible == true) - setFocusUpAndDown(result_resume_series_button, result_episode_select) - else - setFocusUpAndDown(result_bookmark_button, result_episode_select) - } - } - } context?.let { ctx -> val arrayAdapter = ArrayAdapter(ctx, R.layout.sort_bottom_single_choice) @@ -1546,6 +1436,7 @@ class ResultFragment : ResultTrailerPlayer() { result_overlapping_panels?.setStartPanelLockState(if (closed) OverlappingPanelsLayout.LockState.CLOSE else OverlappingPanelsLayout.LockState.UNLOCKED) } + /* observe(viewModel.episodes) { episodeList -> lateFixDownloadButton(episodeList.size <= 1) // movies can have multible parts but still be *movies* this will fix this var isSeriesVisible = false @@ -1666,7 +1557,7 @@ class ResultFragment : ResultTrailerPlayer() { startAction = null startValue = null } - +*/ observe(viewModel.episodes) { episodes -> when (episodes) { is Resource.Failure -> { @@ -1689,25 +1580,91 @@ class ResultFragment : ResultTrailerPlayer() { } } - observe(viewModel.dubStatus) { status -> - result_dub_select?.apply { - isVisible = status != null - status?.toString()?.let { - text = it + observe(viewModel.selectedSeason) { text -> + result_season_button?.setText(text) + + // If the season button is visible the result season button will be next focus down + if (result_season_button?.isVisible == true) + if (result_series_parent?.isVisible == true) + setFocusUpAndDown(result_resume_series_button, result_season_button) + else + setFocusUpAndDown(result_bookmark_button, result_season_button) + } + + observe(viewModel.selectedDubStatus) { status -> + result_dub_select?.setText(status) + + if (result_dub_select?.isVisible == true) + if (result_season_button?.isVisible != true && result_episode_select?.isVisible != true) { + if (result_series_parent?.isVisible == true) + setFocusUpAndDown(result_resume_series_button, result_dub_select) + else + setFocusUpAndDown(result_bookmark_button, result_dub_select) + } + } + + observe(viewModel.selectedRange) { range -> + result_episode_select.setText(range) + + // If Season button is invisible then the bookmark button next focus is episode select + if (result_episode_select?.isVisible == true) + if (result_season_button?.isVisible != true) { + if (result_series_parent?.isVisible == true) + setFocusUpAndDown(result_resume_series_button, result_episode_select) + else + setFocusUpAndDown(result_bookmark_button, result_episode_select) } - } } // val preferDub = context?.getApiDubstatusSettings()?.all { it == DubStatus.Dubbed } == true observe(viewModel.dubSubSelections) { range -> - result_dub_select?.visibility = if (range.size <= 1) GONE else VISIBLE + result_dub_select.setOnClickListener { view -> + view?.context?.let { ctx -> + view.popupMenuNoIconsAndNoStringRes(range + .mapNotNull { (text, status) -> + Pair( + status.ordinal, + text?.asStringNull(ctx) ?: return@mapNotNull null + ) + }) { + viewModel.changeDubStatus(DubStatus.values()[itemId]) + } + } + } + } - if (result_season_button?.isVisible != true && result_episode_select?.isVisible != true) { - if (result_series_parent?.isVisible == true) - setFocusUpAndDown(result_resume_series_button, result_dub_select) - else - setFocusUpAndDown(result_bookmark_button, result_dub_select) + observe(viewModel.rangeSelections) { range -> + result_episode_select.setOnClickListener { view -> + view?.context?.let { ctx -> + val names = range + .mapNotNull { (text, r) -> + r to (text?.asStringNull(ctx) ?: return@mapNotNull null) + } + + view.popupMenuNoIconsAndNoStringRes(names.mapIndexed { index, (_, name) -> + index to name + }) { + viewModel.changeRange(names[itemId].first) + } + } + } + } + + observe(viewModel.seasonSelections) { seasonList -> + result_season_button?.setOnClickListener { view -> + view?.context?.let { ctx -> + val names = seasonList + .mapNotNull { (text, r) -> + r to (text?.asStringNull(ctx) ?: return@mapNotNull null) + } + + view.popupMenuNoIconsAndNoStringRes(names.mapIndexed { index, (_, name) -> + index to name + }) { + viewModel.changeSeason(names[itemId].first) + } + } } } @@ -1716,48 +1673,20 @@ class ResultFragment : ResultTrailerPlayer() { if (hasFocus) result_bookmark_button?.requestFocus() } - result_dub_select.setOnClickListener { - val ranges = dubRange - if (ranges != null) { - it.popupMenuNoIconsAndNoStringRes(ranges - .map { status -> - Pair( - status.ordinal, - status.toString() - ) - } - .toList()) { - viewModel.changeDubStatus(DubStatus.values()[itemId]) - } - } - } - - result_episode_select?.setOnClickListener { - val ranges = episodeRanges - if (ranges != null) { - it.popupMenuNoIconsAndNoStringRes(ranges.mapIndexed { index, s -> Pair(index, s) } - .toList()) { - viewModel.changeRange(itemId) - } - } - } - result_sync_set_score?.setOnClickListener { syncModel.publishUserData() } - observe(viewModel.episodesCount) { count -> - if (count < 0) { - result_episodes_text?.isVisible = false - } else { - // result_episodes_text?.isVisible = true - result_episodes_text?.text = - "$count ${if (count == 1) getString(R.string.episode) else getString(R.string.episodes)}" - } + observe(viewModel.episodesCountText) { count -> + result_episodes_text.setText(count) } - observe(viewModel.id) { - currentId = it + observe(viewModel.trailers) { trailers -> + setTrailers(trailers.flatMap { it.mirros }) // I dont care about subtitles yet! + } + + observe(viewModel.recommendations) { recommendations -> + setRecommendations(recommendations, null) } observe(viewModel.page) { data -> @@ -1778,17 +1707,11 @@ class ResultFragment : ResultTrailerPlayer() { result_meta_rating.setText(d.ratingText) result_description.setTextHtml(d.plotText) result_cast_text.setText(d.actorsText) - setRecommendations.setText(d.nextAiringEpisode) + result_next_airing.setText(d.nextAiringEpisode) result_next_airing_time.setText(d.nextAiringDate) result_poster.setImage(d.posterImage) - if(!d.posterUrl.isNullOrBlank()) { - result_poster?.setImage(d.posterUrl, d.posterHeaders) - } else { - result_poster?.setImageResource(R.drawable.default_cover) - } - result_cast_items?.isVisible = d.actors != null (result_cast_items?.adapter as ActorAdaptor?)?.apply { @@ -1821,11 +1744,6 @@ class ResultFragment : ResultTrailerPlayer() { } } - setRecommendations(d.recommendations, null) - setActors(d.actors) - setNextEpisode(if (d is EpisodeResponse) d.nextAiring else null) - setTrailers(d.trailers.flatMap { it.mirros }) // I dont care about subtitles yet! - if (syncModel.addSyncs(d.syncData)) { syncModel.updateMetaAndUser() syncModel.updateSynced() @@ -1833,70 +1751,20 @@ class ResultFragment : ResultTrailerPlayer() { syncModel.addFromUrl(d.url) } - result_meta_site?.text = d.apiName - val posterImageLink = d.posterUrl - if (!posterImageLink.isNullOrEmpty()) { + result_play_movie.setText(d.playMovieText) - //result_poster_blur?.setImageBlur(posterImageLink, 10, 3, d.posterHeaders) - //Full screen view of Poster image - if (context?.isTrueTvSettings() == false) // Poster not clickable on tv - result_poster_holder?.setOnClickListener { - try { - context?.let { ctx -> - runBlocking { - val sourceBuilder = AlertDialog.Builder(ctx) - sourceBuilder.setView(R.layout.result_poster) - - val sourceDialog = sourceBuilder.create() - sourceDialog.show() - - sourceDialog.findViewById(R.id.imgPoster) - ?.apply { - setImage(posterImageLink) - setOnClickListener { - sourceDialog.dismissSafe() - } - } - } - } - } catch (e: Exception) { - logError(e) - } - } - - } else { - result_poster?.setImageResource(R.drawable.default_cover) - //result_poster_blur?.setImageResource(R.drawable.default_cover) - } - - result_poster_holder?.visibility = VISIBLE - - result_play_movie?.text = - if (d.typeText == TvType.Live) getString(R.string.play_livestream_button) else getString( - R.string.play_movie_button - ) - //result_plot_header?.text = - // if (d.type == TvType.Torrent) getString(R.string.torrent_plot) else getString(R.string.result_plot) - val syno = d.plot - if (!syno.isNullOrEmpty()) { - result_description?.setOnClickListener { + result_description?.setOnClickListener { view -> + view.context?.let { ctx -> val builder: AlertDialog.Builder = - AlertDialog.Builder(requireContext(), R.style.AlertDialogCustom) - builder.setMessage(syno.html()) - .setTitle(if (d.typeText == TvType.Torrent) R.string.torrent_plot else R.string.result_plot) + AlertDialog.Builder(ctx, R.style.AlertDialogCustom) + builder.setMessage(d.plotText.asString(ctx).html()) + .setTitle(d.plotHeaderText.asString(ctx)) .show() } - result_description?.text = syno.html() - } else { - result_description?.text = - if (d.typeText == TvType.Torrent) getString(R.string.torrent_no_plot) else getString( - R.string.normal_no_plot - ) } + result_tag?.removeAllViews() - //result_tag_holder?.visibility = GONE - // result_status.visibility = GONE d.comingSoon.let { soon -> result_coming_soon?.isVisible = soon @@ -1905,7 +1773,7 @@ class ResultFragment : ResultTrailerPlayer() { val tags = d.tags result_tag_holder?.isVisible = tags.isNotEmpty() - if (tags.isNotEmpty()) { + if (tags.isNotEmpty()) { //result_tag_holder?.visibility = VISIBLE val isOnTv = context?.isTrueTvSettings() == true for ((index, tag) in tags.withIndex()) { @@ -1918,6 +1786,8 @@ class ResultFragment : ResultTrailerPlayer() { } } + //TODO FIX + /* if (d.typeText.isMovieType()) { val hasDownloadSupport = api.hasDownloadSupport lateFixDownloadButton(true) @@ -1942,11 +1812,6 @@ class ResultFragment : ResultTrailerPlayer() { return@setOnLongClickListener true } -// result_options.setOnClickListener { -// val card = currentEpisodes?.first() ?: return@setOnClickListener -// handleAction(EpisodeClickEvent(ACTION_SHOW_OPTIONS, card)) -// } - result_movie_progress_downloaded_holder?.isVisible = hasDownloadSupport if (hasDownloadSupport) { val localId = d.getId() @@ -2045,19 +1910,7 @@ class ResultFragment : ResultTrailerPlayer() { } else { lateFixDownloadButton(false) } - - - when (d) { - is AnimeLoadResponse -> { - - // val preferEnglish = true - //val titleName = (if (preferEnglish) d.engName else d.japName) ?: d.name - val titleName = d.name - result_title.text = titleName - //result_toolbar.title = titleName - } - else -> result_title.text = d.name - } + */ } is Resource.Failure -> { result_error_text.text = url?.plus("\n") + data.errorString @@ -2091,7 +1944,7 @@ class ResultFragment : ResultTrailerPlayer() { val tempUrl = url if (tempUrl != null) { result_reload_connectionerror.setOnClickListener { - viewModel.load(tempUrl, apiName, showFillers) + viewModel.load(tempUrl, apiName, showFillers, DubStatus.Dubbed, 0, 0) //TODO FIX } result_reload_connection_open_in_browser?.setOnClickListener { @@ -2124,9 +1977,9 @@ class ResultFragment : ResultTrailerPlayer() { result_meta_site?.isFocusable = false } - if (restart || viewModel.result.value == null) { + if (restart || !viewModel.hasLoaded()) { //viewModel.clear() - viewModel.load(tempUrl, apiName, showFillers) + viewModel.load(tempUrl, apiName, showFillers, DubStatus.Dubbed, 0, 0) //TODO FIX } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt index 4dc5db6c..303ec20e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.ui.result +import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel @@ -7,16 +8,18 @@ import androidx.lifecycle.viewModelScope import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.getId import com.lagradost.cloudstream3.APIHolder.unixTime +import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer +import com.lagradost.cloudstream3.LoadResponse.Companion.getAniListId +import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId import com.lagradost.cloudstream3.animeproviders.GogoanimeProvider import com.lagradost.cloudstream3.animeproviders.NineAnimeProvider import com.lagradost.cloudstream3.metaproviders.SyncRedirector import com.lagradost.cloudstream3.mvvm.* +import com.lagradost.cloudstream3.syncproviders.SyncAPI +import com.lagradost.cloudstream3.syncproviders.providers.Kitsu 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 com.lagradost.cloudstream3.utils.* import kotlinx.coroutines.launch import java.util.concurrent.TimeUnit @@ -55,8 +58,20 @@ data class ResultData( val yearText: UiText?, val nextAiringDate: UiText?, val nextAiringEpisode: UiText?, + val playMovieText: UiText?, + val plotHeaderText: UiText, ) +fun txt(status: DubStatus?): UiText? { + return txt( + when (status) { + DubStatus.Dubbed -> R.string.app_dubbed_text + DubStatus.Subbed -> R.string.app_subbed_text + else -> null + } + ) +} + fun LoadResponse.toResultData(repo: APIRepository): ResultData { debugAssert({ repo.name == apiName }) { "Api returned wrong apiName" @@ -101,6 +116,20 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData { } return ResultData( + plotHeaderText = txt( + when (this.type) { + TvType.Torrent -> R.string.torrent_plot + else -> R.string.result_plot + } + ), + playMovieText = txt( + when (this.type) { + TvType.Live -> R.string.play_livestream_button + TvType.Torrent -> R.string.play_torrent_button + TvType.Movie, TvType.AnimeMovie -> R.string.play_movie_button + else -> null + } + ), nextAiringDate = nextAiringDate, nextAiringEpisode = nextAiringEpisode, posterImage = img( @@ -174,6 +203,8 @@ class ResultViewModel2 : ViewModel() { /** map>> */ private var currentEpisodes: Map> = mapOf() private var currentRanges: Map> = mapOf() + private var currentMeta: SyncAPI.SyncResult? = null + private var currentSync: Map? = null private var currentIndex: EpisodeIndexer? = null private var currentRange: EpisodeRange? = null private var currentShowFillers: Boolean = false @@ -193,20 +224,42 @@ class ResultViewModel2 : ViewModel() { MutableLiveData(Resource.Loading()) val episodes: LiveData>> = _episodes - private val _episodesCount: MutableLiveData = - MutableLiveData(0) - val episodesCount: LiveData = _episodesCount + private val _episodesCountText: MutableLiveData = + MutableLiveData(null) + val episodesCountText: LiveData = _episodesCountText private val _trailers: MutableLiveData> = MutableLiveData(mutableListOf()) val trailers: LiveData> = _trailers - private val _dubStatus: MutableLiveData = MutableLiveData(null) - val dubStatus: LiveData = _dubStatus - private val _dubSubSelections: MutableLiveData> = MutableLiveData(emptyList()) - val dubSubSelections: LiveData> = _dubSubSelections + private val _dubSubSelections: MutableLiveData>> = + MutableLiveData(emptyList()) + val dubSubSelections: LiveData>> = _dubSubSelections + + private val _rangeSelections: MutableLiveData>> = MutableLiveData(emptyList()) + val rangeSelections: LiveData>> = _rangeSelections + + private val _seasonSelections: MutableLiveData>> = MutableLiveData(emptyList()) + val seasonSelections: LiveData>> = _seasonSelections + + + private val _recommendations: MutableLiveData> = + MutableLiveData(emptyList()) + val recommendations: LiveData> = _recommendations + + private val _selectedRange: MutableLiveData = + MutableLiveData(null) + val selectedRange: LiveData = _selectedRange + + private val _selectedSeason: MutableLiveData = + MutableLiveData(null) + val selectedSeason: LiveData = _selectedSeason + + private val _selectedDubStatus: MutableLiveData = MutableLiveData(null) + val selectedDubStatus: LiveData = _selectedDubStatus companion object { + const val TAG = "RVM2" private const val EPISODE_RANGE_SIZE = 50 private const val EPISODE_RANGE_OVERLOAD = 60 @@ -324,6 +377,102 @@ class ResultViewModel2 : ViewModel() { } } + private suspend fun applyMeta( + resp: LoadResponse, + meta: SyncAPI.SyncResult?, + syncs: Map? = null + ): Pair { + if (meta == null) return resp to false + var updateEpisodes = false + val out = resp.apply { + Log.i(ResultViewModel.TAG, "applyMeta") + + duration = duration ?: meta.duration + rating = rating ?: meta.publicScore + tags = tags ?: meta.genres + plot = if (plot.isNullOrBlank()) meta.synopsis else plot + posterUrl = posterUrl ?: meta.posterUrl ?: meta.backgroundPosterUrl + actors = actors ?: meta.actors + + if (this is EpisodeResponse) { + nextAiring = nextAiring ?: meta.nextAiring + } + + for ((k, v) in syncs ?: emptyMap()) { + syncData[k] = v + } + + val realRecommendations = ArrayList() + 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 + + argamap({ + addTrailer(meta.trailers) + }, { + if (this !is AnimeLoadResponse) return@argamap + val map = + Kitsu.getEpisodesDetails(getMalId(), getAniListId(), isResponseRequired = false) + if (map.isNullOrEmpty()) return@argamap + updateEpisodes = DubStatus.values().map { dubStatus -> + val current = + this.episodes[dubStatus]?.mapIndexed { index, episode -> + episode.apply { + this.episode = this.episode ?: (index + 1) + } + }?.sortedBy { it.episode ?: 0 }?.toMutableList() + if (current.isNullOrEmpty()) return@map false + val episodeNumbers = current.map { ep -> ep.episode!! } + var updateCount = 0 + map.forEach { (episode, node) -> + episodeNumbers.binarySearch(episode).let { index -> + current.getOrNull(index)?.let { currentEp -> + current[index] = currentEp.apply { + updateCount++ + val currentBack = this + this.description = this.description ?: node.description?.en + this.name = this.name ?: node.titles?.canonical + this.episode = this.episode ?: node.num ?: episodeNumbers[index] + this.posterUrl = this.posterUrl ?: node.thumbnail?.original?.url + } + } + } + } + this.episodes[dubStatus] = current + updateCount > 0 + }.any { it } + }) + } + return out to updateEpisodes + } + + fun setMeta(meta: SyncAPI.SyncResult, syncs: Map?) = + viewModelScope.launch { + Log.i(TAG, "setMeta") + currentMeta = meta + currentSync = syncs + val (value, updateEpisodes) = Coroutines.ioWork { + currentResponse?.let { resp -> + return@ioWork applyMeta(resp, meta, syncs) + } + return@ioWork null to null + } + + postSuccessful( + value ?: return@launch, + currentRepo ?: return@launch, + updateEpisodes ?: return@launch, + false + ) + } + + private suspend fun updateFillers(name: String) { fillers = try { @@ -343,6 +492,10 @@ class ResultViewModel2 : ViewModel() { postEpisodeRange(currentIndex, range) } + fun changeSeason(season: Int) { + postEpisodeRange(currentIndex?.copy(season = season), currentRange) + } + private fun getEpisodes(indexer: EpisodeIndexer, range: EpisodeRange): List { //TODO ADD GENERATOR @@ -377,9 +530,34 @@ class ResultViewModel2 : ViewModel() { return } + val size = currentEpisodes[indexer]?.size + + _episodesCountText.postValue( + txt( + R.string.episode_format, + if (size == 1) R.string.episode else R.string.episodes, + size + ) + ) + currentIndex = indexer currentRange = range + _selectedSeason.postValue( + when (indexer.season) { + 0 -> txt(R.string.no_season) + else -> txt(R.string.season_format, R.string.season, indexer.season) //TODO FIX + } + ) + _selectedRange.postValue( + if ((currentRanges[indexer]?.size ?: 0) > 1) { + txt(R.string.episodes_range, range.startEpisode, range.endEpisode) + } else { + null + } + ) + _selectedDubStatus.postValue(txt(indexer.dubStatus)) + //TODO SET KEYS preferStartEpisode = range.startEpisode preferStartSeason = indexer.season @@ -587,10 +765,13 @@ class ResultViewModel2 : ViewModel() { // this instantly updates the metadata on the page private fun postPage(loadResponse: LoadResponse, apiRepository: APIRepository) { + _recommendations.postValue(loadResponse.recommendations ?: emptyList()) _page.postValue(Resource.Success(loadResponse.toResultData(apiRepository))) _trailers.postValue(loadResponse.trailers) } + fun hasLoaded() = currentResponse != null + fun load( url: String, apiName: String, @@ -647,7 +828,9 @@ class ResultViewModel2 : ViewModel() { _page.postValue(data) } is Resource.Success -> { - val loadResponse = data.value + val loadResponse = Coroutines.ioWork { + applyMeta(data.value, currentMeta, currentSync).first + } val mainId = loadResponse.getId() AcraApplication.setKey( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt index e55fe873..9e456156 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt @@ -7,6 +7,7 @@ import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.core.view.isGone import androidx.core.view.isVisible +import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.UIHelper.setImage @@ -18,7 +19,12 @@ sealed class UiText { ) : UiText() fun asStringNull(context: Context?): String? { - return asString(context ?: return null) + try { + return asString(context ?: return null) + } catch (e: Exception) { + logError(e) + return null + } } fun asString(context: Context): String { diff --git a/app/src/main/res/layout/fragment_result.xml b/app/src/main/res/layout/fragment_result.xml index 228a3731..11513553 100644 --- a/app/src/main/res/layout/fragment_result.xml +++ b/app/src/main/res/layout/fragment_result.xml @@ -290,15 +290,15 @@ Season + %s %d No Season Episode Episodes + %d-%d + %d %s S E No Episodes found From 64ea5e2f4bd1af01e752daa7cc9e5a6448358385 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Tue, 2 Aug 2022 02:43:42 +0200 Subject: [PATCH 03/22] compiling, not done 3 --- .../com/lagradost/cloudstream3/MainAPI.kt | 4 + .../ui/download/DownloadButtonSetup.kt | 2 +- .../ui/download/DownloadChildFragment.kt | 2 +- .../ui/download/DownloadFragment.kt | 2 +- .../cloudstream3/ui/result/EpisodeAdapter.kt | 30 +- .../cloudstream3/ui/result/ResultFragment.kt | 741 +----------------- .../cloudstream3/ui/result/ResultViewModel.kt | 627 --------------- .../ui/result/ResultViewModel2.kt | 698 ++++++++++++++++- .../cloudstream3/ui/result/SyncViewModel.kt | 12 +- .../cloudstream3/ui/result/UiText.kt | 34 +- .../cloudstream3/ui/search/SearchHelper.kt | 2 +- 11 files changed, 782 insertions(+), 1372 deletions(-) delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 0c600fc7..702ff6a1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -970,6 +970,10 @@ interface LoadResponse { private val aniListIdPrefix = aniListApi.idPrefix var isTrailersEnabled = true + fun LoadResponse.isMovie() : Boolean { + return this.type.isMovieType() + } + @JvmName("addActorNames") fun LoadResponse.addActors(actors: List?) { this.actors = actors?.map { ActorData(Actor(it)) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt index 1ab9beb2..0069be3a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt @@ -18,7 +18,7 @@ import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager object DownloadButtonSetup { - fun handleDownloadClick(activity: Activity?, headerName: String?, click: DownloadClickEvent) { + fun handleDownloadClick(activity: Activity?, click: DownloadClickEvent) { val id = click.data.id if (click.data !is VideoDownloadHelper.DownloadEpisodeCached) return when (click.action) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt index e16d2d9e..477a18e0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt @@ -84,7 +84,7 @@ class DownloadChildFragment : Fragment() { DownloadChildAdapter( ArrayList(), ) { click -> - handleDownloadClick(activity, name, click) + handleDownloadClick(activity, click) } downloadDeleteEventListener = { id: Int -> diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt index 78a5d484..f6265705 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt @@ -153,7 +153,7 @@ class DownloadFragment : Fragment() { }, { downloadClickEvent -> if (downloadClickEvent.data !is VideoDownloadHelper.DownloadEpisodeCached) return@DownloadHeaderAdapter - handleDownloadClick(activity, downloadClickEvent.data.name, downloadClickEvent) + handleDownloadClick(activity, downloadClickEvent) if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) { context?.let { ctx -> downloadsViewModel.updateList(ctx) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt index 69d2e07a..e8ddc7f4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt @@ -10,6 +10,7 @@ import androidx.annotation.LayoutRes import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.widget.ContentLoadingProgressBar +import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.google.android.material.button.MaterialButton import com.lagradost.cloudstream3.R @@ -56,7 +57,7 @@ const val ACTION_DOWNLOAD_EPISODE_SUBTITLE_MIRROR = 14 data class EpisodeClickEvent(val action: Int, val data: ResultEpisode) class EpisodeAdapter( - var cardList: List, + private var cardList: MutableList, private val hasDownloadSupport: Boolean, private val clickCallback: (EpisodeClickEvent) -> Unit, private val downloadClickCallback: (DownloadClickEvent) -> Unit, @@ -92,6 +93,17 @@ class EpisodeAdapter( } } + fun updateList(newList: List) { + val diffResult = DiffUtil.calculateDiff( + ResultDiffCallback(this.cardList, newList) + ) + + cardList.clear() + cardList.addAll(newList) + + diffResult.dispatchUpdatesTo(this) + } + @LayoutRes private var layout: Int = 0 fun updateLayout() { @@ -263,3 +275,19 @@ class EpisodeAdapter( } } } + +class ResultDiffCallback( + private val oldList: List, + private val newList: List +) : + DiffUtil.Callback() { + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = + oldList[oldItemPosition].id == newList[newItemPosition].id + + override fun getOldListSize() = oldList.size + + override fun getNewListSize() = newList.size + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = + oldList[oldItemPosition] == newList[newItemPosition] +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt index f9309da2..b8dd6e8b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt @@ -1,11 +1,6 @@ package com.lagradost.cloudstream3.ui.result import android.annotation.SuppressLint -import android.app.Activity -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.content.Context.CLIPBOARD_SERVICE import android.content.Intent import android.content.Intent.* import android.content.res.ColorStateList @@ -17,12 +12,11 @@ import android.os.Bundle import android.text.Editable import android.view.LayoutInflater import android.view.View -import android.view.View.GONE -import android.view.View.VISIBLE import android.view.ViewGroup -import android.widget.* +import android.widget.AbsListView +import android.widget.ArrayAdapter +import android.widget.Toast import androidx.appcompat.app.AlertDialog -import androidx.core.content.FileProvider import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.widget.NestedScrollView @@ -37,63 +31,43 @@ import com.google.android.gms.cast.framework.CastState import com.google.android.material.button.MaterialButton import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.getApiFromName -import com.lagradost.cloudstream3.APIHolder.getId import com.lagradost.cloudstream3.APIHolder.updateHasTrailers -import com.lagradost.cloudstream3.AcraApplication.Companion.setKey -import com.lagradost.cloudstream3.CommonActivity.getCastSession import com.lagradost.cloudstream3.CommonActivity.showToast -import com.lagradost.cloudstream3.mvvm.* +import com.lagradost.cloudstream3.mvvm.Resource +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.mvvm.normalSafeApiCall +import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.syncproviders.providers.Kitsu import com.lagradost.cloudstream3.ui.WatchType -import com.lagradost.cloudstream3.ui.download.* import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick import com.lagradost.cloudstream3.ui.player.CSPlayerEvent -import com.lagradost.cloudstream3.ui.player.GeneratorPlayer -import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator -import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchHelper import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings -import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getDownloadSubsLanguageISO639_1 -import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.html -import com.lagradost.cloudstream3.utils.AppUtils.isAppInstalled import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable -import com.lagradost.cloudstream3.utils.AppUtils.isConnectedToChromecast import com.lagradost.cloudstream3.utils.AppUtils.loadCache import com.lagradost.cloudstream3.utils.AppUtils.openBrowser -import com.lagradost.cloudstream3.utils.CastHelper.startCast -import com.lagradost.cloudstream3.utils.Coroutines.ioSafe -import com.lagradost.cloudstream3.utils.Coroutines.main -import com.lagradost.cloudstream3.utils.DataStore.getFolderName -import com.lagradost.cloudstream3.utils.DataStore.setKey +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos +import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog -import com.lagradost.cloudstream3.utils.UIHelper.checkWrite +import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute -import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard -import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIcons import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes -import com.lagradost.cloudstream3.utils.UIHelper.requestRW -import com.lagradost.cloudstream3.utils.VideoDownloadManager.getFileName -import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename import kotlinx.android.synthetic.main.fragment_result.* import kotlinx.android.synthetic.main.fragment_result_swipe.* import kotlinx.android.synthetic.main.fragment_trailer.* import kotlinx.android.synthetic.main.result_recommendations.* import kotlinx.android.synthetic.main.result_sync.* import kotlinx.android.synthetic.main.trailer_custom_layout.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.withContext -import java.io.File const val START_ACTION_NORMAL = 0 const val START_ACTION_RESUME_LATEST = 1 @@ -230,223 +204,6 @@ class ResultFragment : ResultTrailerPlayer() { } private var updateUIListener: (() -> Unit)? = null - - private fun downloadSubtitle( - context: Context?, - link: SubtitleData, - meta: VideoDownloadManager.DownloadEpisodeMetadata, - ) { - context?.let { ctx -> - val fileName = getFileName(ctx, meta) - val folder = getFolder(meta.type ?: return, meta.mainName) - downloadSubtitle( - ctx, - ExtractorSubtitleLink(link.name, link.url, ""), - fileName, - folder - ) - } - } - - private fun downloadSubtitle( - context: Context?, - link: ExtractorSubtitleLink, - fileName: String, - folder: String - ) { - ioSafe { - VideoDownloadManager.downloadThing( - context ?: return@ioSafe, - link, - "$fileName ${link.name}", - folder, - if (link.url.contains(".srt")) ".srt" else "vtt", - false, - null - ) { - // no notification - } - } - } - - private fun getMeta( - episode: ResultEpisode, - titleName: String, - apiName: String, - currentPoster: String, - currentIsMovie: Boolean, - tvType: TvType, - ): VideoDownloadManager.DownloadEpisodeMetadata { - return VideoDownloadManager.DownloadEpisodeMetadata( - episode.id, - sanitizeFilename(titleName), - apiName, - episode.poster ?: currentPoster, - episode.name, - if (currentIsMovie) null else episode.season, - if (currentIsMovie) null else episode.episode, - tvType, - ) - } - - private fun getFolder(currentType: TvType, titleName: String): String { - val sanitizedFileName = sanitizeFilename(titleName) - return when (currentType) { - TvType.Anime -> "Anime/$sanitizedFileName" - TvType.Movie -> "Movies" - TvType.AnimeMovie -> "Movies" - TvType.TvSeries -> "TVSeries/$sanitizedFileName" - TvType.OVA -> "OVA" - TvType.Cartoon -> "Cartoons/$sanitizedFileName" - TvType.Torrent -> "Torrent" - TvType.Documentary -> "Documentaries" - TvType.AsianDrama -> "AsianDrama" - TvType.Live -> "LiveStreams" - } - } - - fun startDownload( - context: Context?, - episode: ResultEpisode, - currentIsMovie: Boolean, - currentHeaderName: String, - currentType: TvType, - currentPoster: String, - apiName: String, - parentId: Int, - url: String, - links: List, - subs: List? - ) { - try { - if (context == null) return - - val meta = - getMeta( - episode, - currentHeaderName, - apiName, - currentPoster, - currentIsMovie, - currentType - ) - - val folder = getFolder(currentType, currentHeaderName) - - val src = "$DOWNLOAD_NAVIGATE_TO/$parentId" // url ?: return@let - - // SET VISUAL KEYS - setKey( - DOWNLOAD_HEADER_CACHE, - parentId.toString(), - VideoDownloadHelper.DownloadHeaderCached( - apiName, - url, - currentType, - currentHeaderName, - currentPoster, - parentId, - System.currentTimeMillis(), - ) - ) - - setKey( - getFolderName( - DOWNLOAD_EPISODE_CACHE, - parentId.toString() - ), // 3 deep folder for faster acess - episode.id.toString(), - VideoDownloadHelper.DownloadEpisodeCached( - episode.name, - episode.poster, - episode.episode, - episode.season, - episode.id, - parentId, - episode.rating, - episode.description, - System.currentTimeMillis(), - ) - ) - - // DOWNLOAD VIDEO - VideoDownloadManager.downloadEpisodeUsingWorker( - context, - src,//url ?: return, - folder, - meta, - links - ) - - // 1. Checks if the lang should be downloaded - // 2. Makes it into the download format - // 3. Downloads it as a .vtt file - val downloadList = getDownloadSubsLanguageISO639_1() - subs?.let { subsList -> - subsList.filter { - downloadList.contains( - SubtitleHelper.fromLanguageToTwoLetters( - it.name, - true - ) - ) - } - .map { ExtractorSubtitleLink(it.name, it.url, "") } - .forEach { link -> - val fileName = getFileName(context, meta) - downloadSubtitle(context, link, fileName, folder) - } - } - } catch (e: Exception) { - logError(e) - } - } - - suspend fun downloadEpisode( - activity: Activity?, - episode: ResultEpisode, - currentIsMovie: Boolean, - currentHeaderName: String, - currentType: TvType, - currentPoster: String, - apiName: String, - parentId: Int, - url: String, - ) { - safeApiCall { - val generator = RepoLinkGenerator(listOf(episode)) - val currentLinks = mutableSetOf() - val currentSubs = mutableSetOf() - generator.generateLinks(clearCache = false, isCasting = false, callback = { - it.first?.let { link -> - currentLinks.add(link) - } - }, subtitleCallback = { sub -> - currentSubs.add(sub) - }) - - if (currentLinks.isEmpty()) { - main { - showToast(activity, R.string.no_links_found_toast, Toast.LENGTH_SHORT) - } - return@safeApiCall - } - - startDownload( - activity, - episode, - currentIsMovie, - currentHeaderName, - currentType, - currentPoster, - apiName, - parentId, - url, - sortUrls(currentLinks), - sortSubs(currentSubs), - ) - } - } } private var currentLoadingCount = @@ -470,7 +227,7 @@ class ResultFragment : ResultTrailerPlayer() { override fun onDestroyView() { updateUIListener = null (result_episodes?.adapter as EpisodeAdapter?)?.killAdapter() - downloadButton?.dispose() + //downloadButton?.dispose() //TODO READD //somehow this still leaks and I dont know why???? // todo look at https://github.com/discord/OverlappingPanels/blob/70b4a7cf43c6771873b1e091029d332896d41a1a/sample_app/src/main/java/com/discord/sampleapp/MainActivity.kt PanelsChildGestureRegionObserver.Provider.get().removeGestureRegionsUpdateListener(this) @@ -502,7 +259,7 @@ class ResultFragment : ResultTrailerPlayer() { result_loading?.isVisible = false result_finish_loading?.isVisible = false result_loading_error?.isVisible = true - result_reload_connection_open_in_browser?.isVisible = url != null + result_reload_connection_open_in_browser?.isVisible = true } 2 -> { result_bookmark_fab?.isGone = result_bookmark_fab?.context?.isTvSettings() == true @@ -528,13 +285,6 @@ class ResultFragment : ResultTrailerPlayer() { } } - private var currentPoster: String? = null - private var currentId: Int? = null - private var currentIsMovie: Boolean? = null - private var episodeRanges: List? = null - private var dubRange: Set? = null - var url: String? = null - private fun fromIndexToSeasonText(selection: Int?): String { return when (selection) { null -> getString(R.string.no_season) @@ -566,42 +316,6 @@ class ResultFragment : ResultTrailerPlayer() { } } - private fun handleDownloadButton(downloadClickEvent: DownloadClickEvent) { - if (downloadClickEvent.action == DOWNLOAD_ACTION_DOWNLOAD) { - currentEpisodes?.firstOrNull()?.let { episode -> - handleAction( - EpisodeClickEvent( - ACTION_DOWNLOAD_EPISODE, - ResultEpisode( - currentHeaderName ?: return@let, - currentHeaderName, - null, - 0, - null, - episode.data, - apiName, - currentId ?: return@let, - 0, - 0L, - 0L, - null, - null, - null, - currentType ?: return@let, - currentId ?: return@let, - ) - ) - ) - } - } else { - DownloadButtonSetup.handleDownloadClick( - activity, - currentHeaderName, - downloadClickEvent - ) - } - } - private fun loadTrailer(index: Int? = null) { val isSuccess = currentTrailers.getOrNull(index ?: currentTrailerIndex)?.let { trailer -> @@ -699,420 +413,12 @@ class ResultFragment : ResultTrailerPlayer() { fixGrid() } - private fun lateFixDownloadButton(show: Boolean) { - if (!show || currentType?.isMovieType() == false) { - result_movie_parent.visibility = GONE - result_episodes_text.visibility = VISIBLE - result_episodes.visibility = VISIBLE - } else { - result_movie_parent.visibility = VISIBLE - result_episodes_text.visibility = GONE - result_episodes.visibility = GONE - } - } - private fun updateUI() { syncModel.updateUserData() viewModel.reloadEpisodes() } var apiName: String = "" - private fun handleAction(episodeClick: EpisodeClickEvent): Job = main { - if (episodeClick.action == ACTION_DOWNLOAD_EPISODE) { - val isMovie = currentIsMovie ?: return@main - val headerName = currentHeaderName ?: return@main - val tvType = currentType ?: return@main - val poster = currentPoster ?: return@main - val id = currentId ?: return@main - val curl = url ?: return@main - showToast(activity, R.string.download_started, Toast.LENGTH_SHORT) - downloadEpisode( - activity, - episodeClick.data, - isMovie, - headerName, - tvType, - poster, - apiName, - id, - curl, - ) - return@main - } - - var currentLinks: Set? = null - var currentSubs: Set? = null - - //val id = episodeClick.data.id - currentLoadingCount++ - - val showTitle = - episodeClick.data.name ?: context?.getString(R.string.episode_name_format) - ?.format( - getString(R.string.episode), - episodeClick.data.episode - ) - - - fun acquireSingleExtractorLink( - links: List, - title: String, - callback: (ExtractorLink) -> Unit - ) { - val builder = AlertDialog.Builder(requireContext(), R.style.AlertDialogCustom) - - builder.setTitle(title) - builder.setItems(links.map { "${it.name} ${Qualities.getStringByInt(it.quality)}" } - .toTypedArray()) { dia, which -> - callback.invoke(links[which]) - dia?.dismiss() - } - builder.create().show() - } - - fun acquireSingleSubtitleLink( - links: List, - title: String, - callback: (SubtitleData) -> Unit - ) { - val builder = AlertDialog.Builder(requireContext(), R.style.AlertDialogCustom) - - builder.setTitle(title) - builder.setItems(links.map { it.name }.toTypedArray()) { dia, which -> - callback.invoke(links[which]) - dia?.dismiss() - } - builder.create().show() - } - - fun acquireSingeExtractorLink(title: String, callback: (ExtractorLink) -> Unit) { - acquireSingleExtractorLink(sortUrls(currentLinks ?: return), title, callback) - } - - fun startChromecast(startIndex: Int) { - val eps = currentEpisodes ?: return - activity?.getCastSession()?.startCast( - apiName, - currentIsMovie ?: return, - currentHeaderName, - currentPoster, - episodeClick.data.index, - eps, - sortUrls(currentLinks ?: return), - sortSubs(currentSubs ?: return), - startTime = episodeClick.data.getRealPosition(), - startIndex = startIndex - ) - } - - suspend fun requireLinks(isCasting: Boolean, displayLoading: Boolean = true): Boolean { - val skipLoading = getApiFromName(apiName).instantLinkLoading - - var loadingDialog: AlertDialog? = null - val currentLoad = currentLoadingCount - - if (!skipLoading && displayLoading) { - val builder = - AlertDialog.Builder(requireContext(), R.style.AlertDialogCustomTransparent) - val customLayout = layoutInflater.inflate(R.layout.dialog_loading, null) - builder.setView(customLayout) - - loadingDialog = builder.create() - - loadingDialog.show() - loadingDialog.setOnDismissListener { - currentLoadingCount++ - } - } - - val data = viewModel.loadEpisode(episodeClick.data, isCasting) - if (currentLoadingCount != currentLoad) return false - loadingDialog?.dismissSafe(activity) - - when (data) { - is Resource.Success -> { - currentLinks = data.value.first - currentSubs = data.value.second - return true - } - is Resource.Failure -> { - showToast( - activity, - R.string.error_loading_links_toast, - Toast.LENGTH_SHORT - ) - } - else -> Unit - } - return false - } - - val isLoaded = when (episodeClick.action) { - ACTION_PLAY_EPISODE_IN_PLAYER -> true - ACTION_CLICK_DEFAULT -> true - ACTION_SHOW_TOAST -> true - ACTION_DOWNLOAD_EPISODE -> { - showToast(activity, R.string.download_started, Toast.LENGTH_SHORT) - requireLinks(false, false) - } - ACTION_CHROME_CAST_EPISODE -> requireLinks(true) - ACTION_CHROME_CAST_MIRROR -> requireLinks(true) - ACTION_SHOW_DESCRIPTION -> true - else -> requireLinks(false) - } - if (!isLoaded) return@main // CANT LOAD - - when (episodeClick.action) { - ACTION_SHOW_TOAST -> { - showToast(activity, R.string.play_episode_toast, Toast.LENGTH_SHORT) - } - - ACTION_SHOW_DESCRIPTION -> { - val builder: AlertDialog.Builder = - AlertDialog.Builder(requireContext(), R.style.AlertDialogCustom) - builder.setMessage(episodeClick.data.description ?: return@main) - .setTitle(R.string.torrent_plot) - .show() - } - - ACTION_CLICK_DEFAULT -> { - context?.let { ctx -> - if (ctx.isConnectedToChromecast()) { - handleAction( - EpisodeClickEvent( - ACTION_CHROME_CAST_EPISODE, - episodeClick.data - ) - ) - } else { - handleAction( - EpisodeClickEvent( - ACTION_PLAY_EPISODE_IN_PLAYER, - episodeClick.data - ) - ) - } - } - } - - ACTION_DOWNLOAD_EPISODE_SUBTITLE -> { - acquireSingleSubtitleLink( - sortSubs( - currentSubs ?: return@main - ),//(currentLinks ?: return@main).filter { !it.isM3u8 }, - getString(R.string.episode_action_download_subtitle) - ) { link -> - downloadSubtitle( - context, - link, - getMeta( - episodeClick.data, - currentHeaderName ?: return@acquireSingleSubtitleLink, - apiName, - currentPoster ?: return@acquireSingleSubtitleLink, - currentIsMovie ?: return@acquireSingleSubtitleLink, - currentType ?: return@acquireSingleSubtitleLink - ) - ) - showToast(activity, R.string.download_started, Toast.LENGTH_SHORT) - } - } - - ACTION_SHOW_OPTIONS -> { - context?.let { ctx -> - val builder = AlertDialog.Builder(ctx, R.style.AlertDialogCustom) - var dialog: AlertDialog? = null - builder.setTitle(showTitle) - val options = - requireContext().resources.getStringArray(R.array.episode_long_click_options) - val optionsValues = - requireContext().resources.getIntArray(R.array.episode_long_click_options_values) - - val verifiedOptions = ArrayList() - val verifiedOptionsValues = ArrayList() - - val hasDownloadSupport = getApiFromName(apiName).hasDownloadSupport - - for (i in options.indices) { - val opv = optionsValues[i] - val op = options[i] - - val isConnected = ctx.isConnectedToChromecast() - val add = when (opv) { - ACTION_CHROME_CAST_EPISODE -> isConnected - ACTION_CHROME_CAST_MIRROR -> isConnected - ACTION_DOWNLOAD_EPISODE_SUBTITLE -> !currentSubs.isNullOrEmpty() - ACTION_DOWNLOAD_EPISODE -> hasDownloadSupport - ACTION_DOWNLOAD_MIRROR -> hasDownloadSupport - ACTION_PLAY_EPISODE_IN_VLC_PLAYER -> context?.isAppInstalled( - VLC_PACKAGE - ) ?: false - else -> true - } - if (add) { - verifiedOptions.add(op) - verifiedOptionsValues.add(opv) - } - } - - builder.setItems( - verifiedOptions.toTypedArray() - ) { _, which -> - handleAction( - EpisodeClickEvent( - verifiedOptionsValues[which], - episodeClick.data - ) - ) - dialog?.dismissSafe(activity) - } - - dialog = builder.create() - dialog.show() - } - } - ACTION_COPY_LINK -> { - activity?.let { act -> - try { - acquireSingeExtractorLink(act.getString(R.string.episode_action_copy_link)) { link -> - val serviceClipboard = - (act.getSystemService(CLIPBOARD_SERVICE) as? ClipboardManager?) - ?: return@acquireSingeExtractorLink - val clip = ClipData.newPlainText(link.name, link.url) - serviceClipboard.setPrimaryClip(clip) - showToast(act, R.string.copy_link_toast, Toast.LENGTH_SHORT) - } - } catch (e: Exception) { - showToast(act, e.toString(), Toast.LENGTH_LONG) - logError(e) - } - } - } - - ACTION_PLAY_EPISODE_IN_BROWSER -> { - acquireSingeExtractorLink(getString(R.string.episode_action_play_in_browser)) { link -> - try { - val i = Intent(ACTION_VIEW) - i.data = Uri.parse(link.url) - startActivity(i) - } catch (e: Exception) { - logError(e) - } - } - } - - ACTION_CHROME_CAST_MIRROR -> { - acquireSingeExtractorLink(getString(R.string.episode_action_chromecast_mirror)) { link -> - val mirrorIndex = currentLinks?.indexOf(link) ?: -1 - startChromecast(if (mirrorIndex == -1) 0 else mirrorIndex) - } - } - - ACTION_CHROME_CAST_EPISODE -> { - startChromecast(0) - } - - ACTION_PLAY_EPISODE_IN_VLC_PLAYER -> { - activity?.let { act -> - try { - if (!act.checkWrite()) { - act.requestRW() - if (act.checkWrite()) return@main - } - val data = currentLinks ?: return@main - val subs = currentSubs ?: return@main - - val outputDir = act.cacheDir - val outputFile = withContext(Dispatchers.IO) { - File.createTempFile("mirrorlist", ".m3u8", outputDir) - } - var text = "#EXTM3U" - for (sub in sortSubs(subs)) { - text += "\n#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"${sub.name}\",DEFAULT=NO,AUTOSELECT=NO,FORCED=NO,LANGUAGE=\"${sub.name}\",URI=\"${sub.url}\"" - } - for (link in data.sortedBy { -it.quality }) { - text += "\n#EXTINF:, ${link.name}\n${link.url}" - } - outputFile.writeText(text) - - val vlcIntent = Intent(VLC_INTENT_ACTION_RESULT) - - vlcIntent.setPackage(VLC_PACKAGE) - vlcIntent.addFlags(FLAG_GRANT_PERSISTABLE_URI_PERMISSION) - vlcIntent.addFlags(FLAG_GRANT_PREFIX_URI_PERMISSION) - vlcIntent.addFlags(FLAG_GRANT_READ_URI_PERMISSION) - vlcIntent.addFlags(FLAG_GRANT_WRITE_URI_PERMISSION) - - vlcIntent.setDataAndType( - FileProvider.getUriForFile( - act, - act.applicationContext.packageName + ".provider", - outputFile - ), "video/*" - ) - - val startId = VLC_FROM_PROGRESS - - var position = startId - if (startId == VLC_FROM_START) { - position = 1 - } else if (startId == VLC_FROM_PROGRESS) { - position = 0 - } - - vlcIntent.putExtra("position", position) - - vlcIntent.component = VLC_COMPONENT - act.setKey(VLC_LAST_ID_KEY, episodeClick.data.id) - act.startActivityForResult(vlcIntent, VLC_REQUEST_CODE) - } catch (e: Exception) { - logError(e) - showToast(act, e.toString(), Toast.LENGTH_LONG) - } - } - } - - ACTION_PLAY_EPISODE_IN_PLAYER -> { - viewModel.getGenerator(episodeClick.data) - ?.let { generator -> - activity?.navigate( - R.id.global_to_navigation_player, - GeneratorPlayer.newInstance( - generator, syncdata?.let { HashMap(it) } - ) - ) - } - } - - ACTION_RELOAD_EPISODE -> { - viewModel.loadEpisode(episodeClick.data, false, clearCache = true) - } - - ACTION_DOWNLOAD_MIRROR -> { - acquireSingleExtractorLink( - sortUrls( - currentLinks ?: return@main - ),//(currentLinks ?: return@main).filter { !it.isM3u8 }, - context?.getString(R.string.episode_action_download_mirror) ?: "" - ) { link -> - startDownload( - context, - episodeClick.data, - currentIsMovie ?: return@acquireSingleExtractorLink, - currentHeaderName ?: return@acquireSingleExtractorLink, - currentType ?: return@acquireSingleExtractorLink, - currentPoster ?: return@acquireSingleExtractorLink, - apiName, - currentId ?: return@acquireSingleExtractorLink, - url ?: return@acquireSingleExtractorLink, - listOf(link), - sortSubs(currentSubs ?: return@acquireSingleExtractorLink), - ) - showToast(activity, R.string.download_started, Toast.LENGTH_SHORT) - } - } - } - } @SuppressLint("SetTextI18n") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -1159,7 +465,7 @@ class ResultFragment : ResultTrailerPlayer() { // activity?.fixPaddingStatusbar(result_toolbar) - url = arguments?.getString(URL_BUNDLE) + val url = arguments?.getString(URL_BUNDLE) apiName = arguments?.getString(API_NAME_BUNDLE) ?: return startAction = arguments?.getInt(START_ACTION_BUNDLE) ?: START_ACTION_NORMAL startValue = arguments?.getInt(START_VALUE_BUNDLE) @@ -1218,10 +524,10 @@ class ResultFragment : ResultTrailerPlayer() { ArrayList(), api.hasDownloadSupport, { episodeClick -> - handleAction(episodeClick) + viewModel.handleAction(activity, episodeClick) }, { downloadClickEvent -> - handleDownloadClick(activity, currentHeaderName, downloadClickEvent) + handleDownloadClick(activity, downloadClickEvent) } ) @@ -1350,10 +656,6 @@ class ResultFragment : ResultTrailerPlayer() { (result_mini_sync?.adapter as? ImageAdapter?)?.updateList(newList.mapNotNull { it.icon }) } - observe(syncModel.syncIds) { - syncdata = it - } - var currentSyncProgress = 0 fun setSyncMaxEpisodes(totalEpisodes: Int?) { @@ -1376,7 +678,7 @@ class ResultFragment : ResultTrailerPlayer() { val d = meta.value result_sync_episodes?.progress = currentSyncProgress * 1000 setSyncMaxEpisodes(d.totalEpisodes) - viewModel.setMeta(d, syncdata) + viewModel.setMeta(d, syncModel.getSyncs()) } is Resource.Loading -> { result_sync_max_episodes?.text = @@ -1571,11 +873,7 @@ class ResultFragment : ResultTrailerPlayer() { is Resource.Success -> { //result_episodes?.isVisible = true result_episode_loading?.isVisible = false - if (result_episodes == null || result_episodes.adapter == null) return@observe - currentEpisodes = episodes.value - (result_episodes?.adapter as? EpisodeAdapter?)?.cardList = episodes.value - (result_episodes?.adapter as? EpisodeAdapter?)?.updateLayout() - (result_episodes?.adapter as? EpisodeAdapter?)?.notifyDataSetChanged() + (result_episodes?.adapter as? EpisodeAdapter?)?.updateList(episodes.value) } } } @@ -1705,12 +1003,12 @@ class ResultFragment : ResultTrailerPlayer() { 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) result_next_airing.setText(d.nextAiringEpisode) result_next_airing_time.setText(d.nextAiringDate) result_poster.setImage(d.posterImage) + result_play_movie.setText(d.playMovieText) result_cast_items?.isVisible = d.actors != null @@ -1751,8 +1049,7 @@ class ResultFragment : ResultTrailerPlayer() { syncModel.addFromUrl(d.url) } - result_play_movie.setText(d.playMovieText) - + result_description.setTextHtml(d.plotText) result_description?.setOnClickListener { view -> view.context?.let { ctx -> val builder: AlertDialog.Builder = diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt deleted file mode 100644 index 03f8b802..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt +++ /dev/null @@ -1,627 +0,0 @@ -package com.lagradost.cloudstream3.ui.result - -import android.util.Log -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.getApiDubstatusSettings -import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull -import com.lagradost.cloudstream3.APIHolder.getApiFromUrlNull -import com.lagradost.cloudstream3.APIHolder.getId -import com.lagradost.cloudstream3.AcraApplication.Companion.context -import com.lagradost.cloudstream3.AcraApplication.Companion.setKey -import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer -import com.lagradost.cloudstream3.LoadResponse.Companion.getAniListId -import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId -import com.lagradost.cloudstream3.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.logError -import com.lagradost.cloudstream3.mvvm.safeApiCall -import com.lagradost.cloudstream3.syncproviders.SyncAPI -import com.lagradost.cloudstream3.syncproviders.providers.Kitsu.getEpisodesDetails -import com.lagradost.cloudstream3.ui.APIRepository -import com.lagradost.cloudstream3.ui.WatchType -import com.lagradost.cloudstream3.ui.player.IGenerator -import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator -import com.lagradost.cloudstream3.ui.player.SubtitleData -import com.lagradost.cloudstream3.utils.Coroutines.ioWork -import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE -import com.lagradost.cloudstream3.utils.DataStoreHelper -import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData -import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultSeason -import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultWatchState -import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos -import com.lagradost.cloudstream3.utils.DataStoreHelper.setBookmarkedData -import com.lagradost.cloudstream3.utils.DataStoreHelper.setDub -import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultSeason -import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultWatchState -import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.FillerEpisodeCheck.getFillerEpisodes -import com.lagradost.cloudstream3.utils.VideoDownloadHelper -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlin.collections.set - -const val EPISODE_RANGE_SIZE = 50 -const val EPISODE_RANGE_OVERLOAD = 60 - -class ResultViewModel : ViewModel() { - private var repo: APIRepository? = null - private var generator: IGenerator? = null - - private val _resultResponse: MutableLiveData> = MutableLiveData() - private val _episodes: MutableLiveData> = MutableLiveData() - private val episodeById: MutableLiveData> = - MutableLiveData() // lookup by ID to get Index - - private val _publicEpisodes: MutableLiveData>> = MutableLiveData() - private val _publicEpisodesCount: MutableLiveData = MutableLiveData() // before the sorting - private val _rangeOptions: MutableLiveData> = MutableLiveData() - val selectedRange: MutableLiveData = MutableLiveData() - private val selectedRangeInt: MutableLiveData = MutableLiveData() - val rangeOptions: LiveData> = _rangeOptions - - val result: LiveData> get() = _resultResponse - - val episodes: LiveData> get() = _episodes - val publicEpisodes: LiveData>> get() = _publicEpisodes - val publicEpisodesCount: LiveData get() = _publicEpisodesCount - - val dubStatus: LiveData get() = _dubStatus - private val _dubStatus: MutableLiveData = MutableLiveData() - - val id: MutableLiveData = MutableLiveData() - val selectedSeason: MutableLiveData = MutableLiveData(-2) - val seasonSelections: MutableLiveData>> = MutableLiveData() - - val dubSubSelections: LiveData> get() = _dubSubSelections - private val _dubSubSelections: MutableLiveData> = MutableLiveData() - - val dubSubEpisodes: LiveData>?> get() = _dubSubEpisodes - private val _dubSubEpisodes: MutableLiveData>?> = - MutableLiveData() - - private val _watchStatus: MutableLiveData = MutableLiveData() - val watchStatus: LiveData get() = _watchStatus - - fun updateWatchStatus(status: WatchType) = viewModelScope.launch { - val currentId = id.value ?: return@launch - _watchStatus.postValue(status) - val resultPage = _resultResponse.value - - withContext(Dispatchers.IO) { - setResultWatchState(currentId, status.internalId) - if (resultPage != null && resultPage is Resource.Success) { - val resultPageData = resultPage.value - val current = getBookmarkedData(currentId) - val currentTime = System.currentTimeMillis() - setBookmarkedData( - currentId, - DataStoreHelper.BookmarkedData( - currentId, - current?.bookmarkedTime ?: currentTime, - currentTime, - resultPageData.name, - resultPageData.url, - resultPageData.apiName, - resultPageData.type, - resultPageData.posterUrl, - resultPageData.year - ) - ) - } - } - } - - companion object { - const val TAG = "RVM" - } - - var lastMeta: SyncAPI.SyncResult? = null - var lastSync: Map? = null - - private suspend fun applyMeta( - resp: LoadResponse, - meta: SyncAPI.SyncResult?, - syncs: Map? = null - ): Pair { - if (meta == null) return resp to false - var updateEpisodes = false - val out = resp.apply { - Log.i(TAG, "applyMeta") - - duration = duration ?: meta.duration - rating = rating ?: meta.publicScore - tags = tags ?: meta.genres - plot = if (plot.isNullOrBlank()) meta.synopsis else plot - posterUrl = posterUrl ?: meta.posterUrl ?: meta.backgroundPosterUrl - actors = actors ?: meta.actors - - if (this is EpisodeResponse) { - nextAiring = nextAiring ?: meta.nextAiring - } - - for ((k, v) in syncs ?: emptyMap()) { - syncData[k] = v - } - - val realRecommendations = ArrayList() - 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 - - argamap({ - addTrailer(meta.trailers) - }, { - if (this !is AnimeLoadResponse) return@argamap - val map = getEpisodesDetails(getMalId(), getAniListId(), isResponseRequired = false) - if (map.isNullOrEmpty()) return@argamap - updateEpisodes = DubStatus.values().map { dubStatus -> - val current = - this.episodes[dubStatus]?.mapIndexed { index, episode -> - episode.apply { - this.episode = this.episode ?: (index + 1) - } - }?.sortedBy { it.episode ?: 0 }?.toMutableList() - if (current.isNullOrEmpty()) return@map false - val episodeNumbers = current.map { ep -> ep.episode!! } - var updateCount = 0 - map.forEach { (episode, node) -> - episodeNumbers.binarySearch(episode).let { index -> - current.getOrNull(index)?.let { currentEp -> - current[index] = currentEp.apply { - updateCount++ - val currentBack = this - this.description = this.description ?: node.description?.en - this.name = this.name ?: node.titles?.canonical - this.episode = this.episode ?: node.num ?: episodeNumbers[index] - this.posterUrl = this.posterUrl ?: node.thumbnail?.original?.url - } - } - } - } - this.episodes[dubStatus] = current - updateCount > 0 - }.any { it } - }) - } - return out to updateEpisodes - } - - fun setMeta(meta: SyncAPI.SyncResult, syncs: Map?) = - viewModelScope.launch { - Log.i(TAG, "setMeta") - lastMeta = meta - lastSync = syncs - val (value, updateEpisodes) = ioWork { - (result.value as? Resource.Success?)?.value?.let { resp -> - return@ioWork applyMeta(resp, meta, syncs) - } - return@ioWork null to null - } - _resultResponse.postValue(Resource.Success(value ?: return@launch)) - if (updateEpisodes ?: return@launch) updateEpisodes(value, lastShowFillers) - } - - private fun loadWatchStatus(localId: Int? = null) { - val currentId = localId ?: id.value ?: return - val currentWatch = getResultWatchState(currentId) - _watchStatus.postValue(currentWatch) - } - - private fun filterEpisodes(list: List?, selection: Int?, range: Int?) { - if (list == null) return - val seasonTypes = HashMap() - for (i in list) { - if (!seasonTypes.containsKey(i.season)) { - seasonTypes[i.season] = true - } - } - val seasons = seasonTypes.toList().map { null to it.first }.sortedBy { it.second } - - - seasonSelections.postValue(seasons) - if (seasons.isEmpty()) { // WHAT THE FUCK DID YOU DO????? HOW DID YOU DO THIS - _publicEpisodes.postValue(Resource.Success(emptyList())) - return - } - - val realSelection = - if (!seasonTypes.containsKey(selection)) seasons.first().second else selection - val internalId = id.value - - if (internalId != null) setResultSeason(internalId, realSelection) - - selectedSeason.postValue(realSelection ?: -2) - - var currentList = list.filter { it.season == realSelection } - _publicEpisodesCount.postValue(currentList.size) - - val rangeList = ArrayList() - for (i in currentList.indices step EPISODE_RANGE_SIZE) { - if (i + EPISODE_RANGE_SIZE < currentList.size) { - rangeList.add("${i + 1}-${i + EPISODE_RANGE_SIZE}") - } else { - rangeList.add("${i + 1}-${currentList.size}") - } - } - - val cRange = range ?: if (selection != null) { - 0 - } else { - selectedRangeInt.value ?: 0 - } - - val realRange = if (cRange * EPISODE_RANGE_SIZE > currentList.size) { - currentList.size / EPISODE_RANGE_SIZE - } else { - cRange - } - - if (currentList.size > EPISODE_RANGE_OVERLOAD) { - currentList = currentList.subList( - realRange * EPISODE_RANGE_SIZE, - minOf(currentList.size, (realRange + 1) * EPISODE_RANGE_SIZE) - ) - _rangeOptions.postValue(rangeList) - selectedRangeInt.postValue(realRange) - selectedRange.postValue(rangeList[realRange]) - } else { - val allRange = "1-${currentList.size}" - _rangeOptions.postValue(listOf(allRange)) - selectedRangeInt.postValue(0) - selectedRange.postValue(allRange) - } - - _publicEpisodes.postValue(Resource.Success(currentList)) - } - - fun changeSeason(selection: Int?) { - filterEpisodes(_episodes.value, selection, null) - } - - fun changeRange(range: Int?) { - filterEpisodes(_episodes.value, null, range) - } - - fun changeDubStatus(status: DubStatus?) { - if (status == null) return - dubSubEpisodes.value?.get(status)?.let { episodes -> - id.value?.let { - setDub(it, status) - } - _dubStatus.postValue(status!!) - updateEpisodes(null, episodes, null) - } - } - - suspend fun loadEpisode( - episode: ResultEpisode, - isCasting: Boolean, - clearCache: Boolean = false - ): Resource, Set>> { - return safeApiCall { - val index = _episodes.value?.indexOf(episode) ?: episode.index - - val currentLinks = mutableSetOf() - val currentSubs = mutableSetOf() - - generator?.goto(index) - generator?.generateLinks(clearCache, isCasting, { - it.first?.let { link -> - currentLinks.add(link) - } - }, { sub -> - currentSubs.add(sub) - }) - - return@safeApiCall Pair( - currentLinks.toSet(), - currentSubs.toSet() - ) - } - } - - fun getGenerator(episode: ResultEpisode): IGenerator? { - val index = _episodes.value?.indexOf(episode) ?: episode.index - - generator?.goto(index) - return generator - } - - private fun updateEpisodes(localId: Int?, list: List, selection: Int?) { - _episodes.postValue(list) - generator = RepoLinkGenerator(list) - - val set = HashMap() - val range = selectedRangeInt.value - - list.withIndex().forEach { set[it.value.id] = it.index } - episodeById.postValue(set) - - filterEpisodes( - list, - if (selection == -1) getResultSeason(localId ?: id.value ?: return) else selection, - range - ) - } - - fun reloadEpisodes() { - val current = _episodes.value ?: return - val copy = current.map { - val posDur = getViewPos(it.id) - it.copy(position = posDur?.position ?: 0, duration = posDur?.duration ?: 0) - } - updateEpisodes(null, copy, selectedSeason.value) - } - - 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 - } - - var lastShowFillers = false - private suspend fun updateEpisodes(loadResponse: LoadResponse, showFillers: Boolean) { - Log.i(TAG, "updateEpisodes") - try { - lastShowFillers = showFillers - val mainId = loadResponse.getId() - - when (loadResponse) { - is AnimeLoadResponse -> { - if (loadResponse.episodes.isEmpty()) { - _dubSubEpisodes.postValue(emptyMap()) - return - } - - 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 :( - val preferDub = context?.getApiDubstatusSettings() - ?.contains(DubStatus.Dubbed) == true - - // 3 statements because there can be only dub even if you do not prefer it. - val dubStatus = - if (preferDub && statuses.contains(DubStatus.Dubbed)) DubStatus.Dubbed - else if (!preferDub && statuses.contains(DubStatus.Subbed)) DubStatus.Subbed - else statuses.first() - - val fillerEpisodes = - if (showFillers) safeApiCall { getFillerEpisodes(loadResponse.name) } else null - - val existingEpisodes = HashSet() - val res = loadResponse.episodes.map { ep -> - val episodes = ArrayList() - 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) - episodes.add(buildResultEpisode( - loadResponse.name, - filterName(i.name), - i.posterUrl, - episode, - i.season, - i.data, - loadResponse.apiName, - id, - index, - i.rating, - i.description, - if (fillerEpisodes is Resource.Success) fillerEpisodes.value?.let { - it.contains(episode) && it[episode] == true - } ?: false else false, - loadResponse.type, - mainId - )) - } - } - - Pair(ep.key, episodes) - }.toMap() - - // These posts needs to be in this order as to make the preferDub in ResultFragment work - _dubSubEpisodes.postValue(res) - res[dubStatus]?.let { episodes -> - updateEpisodes(mainId, episodes, -1) - } - - _dubStatus.postValue(dubStatus) - _dubSubSelections.postValue(loadResponse.episodes.keys) - } - - is TvSeriesLoadResponse -> { - val episodes = ArrayList() - val existingEpisodes = HashSet() - 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) - episodes.add( - buildResultEpisode( - loadResponse.name, - filterName(episode.name), - episode.posterUrl, - episodeIndex, - episode.season, - episode.data, - loadResponse.apiName, - id, - index, - episode.rating, - episode.description, - null, - loadResponse.type, - mainId - ) - ) - } - } - updateEpisodes(mainId, episodes, -1) - } - is MovieLoadResponse -> { - buildResultEpisode( - loadResponse.name, - loadResponse.name, - null, - 0, - null, - loadResponse.dataUrl, - loadResponse.apiName, - (mainId), // HAS SAME ID - 0, - null, - null, - null, - loadResponse.type, - mainId - ).let { - updateEpisodes(mainId, listOf(it), -1) - } - } - is LiveStreamLoadResponse -> { - buildResultEpisode( - loadResponse.name, - loadResponse.name, - null, - 0, - null, - loadResponse.dataUrl, - loadResponse.apiName, - (mainId), // HAS SAME ID - 0, - null, - null, - null, - loadResponse.type, - mainId - ).let { - updateEpisodes(mainId, listOf(it), -1) - } - } - is TorrentLoadResponse -> { - updateEpisodes( - mainId, listOf( - buildResultEpisode( - loadResponse.name, - loadResponse.name, - null, - 0, - null, - loadResponse.torrent ?: loadResponse.magnet ?: "", - loadResponse.apiName, - (mainId), // HAS SAME ID - 0, - null, - null, - null, - loadResponse.type, - mainId - ) - ), -1 - ) - } - } - } catch (e: Exception) { - logError(e) - } - } - - fun load(url: String, apiName: String, showFillers: Boolean) = viewModelScope.launch { - _publicEpisodes.postValue(Resource.Loading()) - _resultResponse.postValue(Resource.Loading(url)) - - val api = getApiFromNameNull(apiName) ?: getApiFromUrlNull(url) - if (api == null) { - _resultResponse.postValue( - Resource.Failure( - false, - null, - null, - "This provider does not exist" - ) - ) - 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) - - val data = repo?.load(validUrl) ?: return@launch - - _resultResponse.postValue(data) - - when (data) { - is Resource.Success -> { - val loadResponse = if (lastMeta != null || lastSync != null) ioWork { - applyMeta(data.value, lastMeta, lastSync).first - } else data.value - _resultResponse.postValue(Resource.Success(loadResponse)) - val mainId = loadResponse.getId() - id.postValue(mainId) - loadWatchStatus(mainId) - - setKey( - DOWNLOAD_HEADER_CACHE, - mainId.toString(), - VideoDownloadHelper.DownloadHeaderCached( - apiName, - validUrl, - loadResponse.type, - loadResponse.name, - loadResponse.posterUrl, - mainId, - System.currentTimeMillis(), - ) - ) - updateEpisodes(loadResponse, showFillers) - } - else -> Unit - } - } - - private var _apiName: MutableLiveData = MutableLiveData() - val apiName: LiveData get() = _apiName - -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt index 303ec20e..544445a3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt @@ -1,6 +1,14 @@ package com.lagradost.cloudstream3.ui.result +import android.app.Activity +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.net.Uri import android.util.Log +import android.widget.Toast +import androidx.core.content.FileProvider import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel @@ -8,9 +16,11 @@ import androidx.lifecycle.viewModelScope import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.getId import com.lagradost.cloudstream3.APIHolder.unixTime +import com.lagradost.cloudstream3.CommonActivity.getCastSession import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer import com.lagradost.cloudstream3.LoadResponse.Companion.getAniListId import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId +import com.lagradost.cloudstream3.LoadResponse.Companion.isMovie import com.lagradost.cloudstream3.animeproviders.GogoanimeProvider import com.lagradost.cloudstream3.animeproviders.NineAnimeProvider import com.lagradost.cloudstream3.metaproviders.SyncRedirector @@ -18,9 +28,26 @@ import com.lagradost.cloudstream3.mvvm.* import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.syncproviders.providers.Kitsu import com.lagradost.cloudstream3.ui.APIRepository +import com.lagradost.cloudstream3.ui.WatchType +import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO +import com.lagradost.cloudstream3.ui.player.GeneratorPlayer import com.lagradost.cloudstream3.ui.player.IGenerator +import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator +import com.lagradost.cloudstream3.ui.player.SubtitleData +import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.utils.AppUtils.isConnectedToChromecast +import com.lagradost.cloudstream3.utils.CastHelper.startCast +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import com.lagradost.cloudstream3.utils.DataStore.setKey +import com.lagradost.cloudstream3.utils.UIHelper.checkWrite +import com.lagradost.cloudstream3.utils.UIHelper.navigate +import com.lagradost.cloudstream3.utils.UIHelper.requestRW +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.File import java.util.concurrent.TimeUnit @@ -43,6 +70,7 @@ data class ResultData( val comingSoon: Boolean, val backgroundPosterUrl: String?, val title: String, + var syncData: Map, val posterImage: UiImage?, val plotText: UiText, @@ -73,7 +101,7 @@ fun txt(status: DubStatus?): UiText? { } fun LoadResponse.toResultData(repo: APIRepository): ResultData { - debugAssert({ repo.name == apiName }) { + debugAssert({ repo.name != apiName }) { "Api returned wrong apiName" } @@ -116,6 +144,7 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData { } return ResultData( + syncData = syncData, plotHeaderText = txt( when (this.type) { TvType.Torrent -> R.string.torrent_plot @@ -165,7 +194,7 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData { ), yearText = txt(year), apiName = txt(apiName), - ratingText = rating?.div(1000f)?.let { UiText.StringResource(R.string.rating_format, it) }, + ratingText = rating?.div(1000f)?.let { txt(R.string.rating_format, it) }, vpnText = txt( when (repo.vpnStatus) { VPNStatus.None -> null @@ -192,6 +221,48 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData { ) } + +data class LinkProgress( + val linksLoaded: Int, + val subsLoaded: Int, +) + +data class LinkLoadingResult( + val links: List, + val subs: List, +) + +sealed class SelectPopup { + data class SelectText( + val text: UiText, + val options: List, + val callback: (Int?) -> Unit + ) : SelectPopup() + + data class SelectArray( + val text: UiText, + val options: Int, + val map: Int?, + val callback: (Int?) -> Unit + ) : SelectPopup() + + fun SelectPopup.transformResult(context: Context, input: Int?): Int? { + if (input == null) return null + return when (this) { + is SelectArray -> context.resources.getIntArray(map ?: return input).getOrNull(input) + ?: input + is SelectText -> input + } + } + + fun SelectPopup.getOptions(context: Context): List { + return when (this) { + is SelectArray -> context.resources.getStringArray(options).toList() + is SelectText -> options.map { it.asString(context) } + } + } +} + class ResultViewModel2 : ViewModel() { private var currentResponse: LoadResponse? = null @@ -215,6 +286,9 @@ class ResultViewModel2 : ViewModel() { private var preferDubStatus: DubStatus? = null private var preferStartEpisode: Int? = null private var preferStartSeason: Int? = null + //private val currentIsMovie get() = currentResponse?.isEpisodeBased() == false + //private val currentHeaderName get() = currentResponse?.name + private val _page: MutableLiveData> = MutableLiveData(Resource.Loading()) @@ -236,10 +310,12 @@ class ResultViewModel2 : ViewModel() { MutableLiveData(emptyList()) val dubSubSelections: LiveData>> = _dubSubSelections - private val _rangeSelections: MutableLiveData>> = MutableLiveData(emptyList()) + private val _rangeSelections: MutableLiveData>> = + MutableLiveData(emptyList()) val rangeSelections: LiveData>> = _rangeSelections - private val _seasonSelections: MutableLiveData>> = MutableLiveData(emptyList()) + private val _seasonSelections: MutableLiveData>> = + MutableLiveData(emptyList()) val seasonSelections: LiveData>> = _seasonSelections @@ -258,6 +334,9 @@ class ResultViewModel2 : ViewModel() { private val _selectedDubStatus: MutableLiveData = MutableLiveData(null) val selectedDubStatus: LiveData = _selectedDubStatus + private val _loadedLinks: MutableLiveData = MutableLiveData(null) + val loadedLinks: LiveData = _loadedLinks + companion object { const val TAG = "RVM2" private const val EPISODE_RANGE_SIZE = 50 @@ -375,6 +454,607 @@ class ResultViewModel2 : ViewModel() { index to list }.toMap() } + + private fun downloadSubtitle( + context: Context?, + link: ExtractorSubtitleLink, + fileName: String, + folder: String + ) { + ioSafe { + VideoDownloadManager.downloadThing( + context ?: return@ioSafe, + link, + "$fileName ${link.name}", + folder, + if (link.url.contains(".srt")) ".srt" else "vtt", + false, + null + ) { + // no notification + } + } + } + + private fun getFolder(currentType: TvType, titleName: String): String { + val sanitizedFileName = VideoDownloadManager.sanitizeFilename(titleName) + return when (currentType) { + TvType.Anime -> "Anime/$sanitizedFileName" + TvType.Movie -> "Movies" + TvType.AnimeMovie -> "Movies" + TvType.TvSeries -> "TVSeries/$sanitizedFileName" + TvType.OVA -> "OVA" + TvType.Cartoon -> "Cartoons/$sanitizedFileName" + TvType.Torrent -> "Torrent" + TvType.Documentary -> "Documentaries" + TvType.AsianDrama -> "AsianDrama" + TvType.Live -> "LiveStreams" + } + } + + private fun downloadSubtitle( + context: Context?, + link: SubtitleData, + meta: VideoDownloadManager.DownloadEpisodeMetadata, + ) { + context?.let { ctx -> + val fileName = VideoDownloadManager.getFileName(ctx, meta) + val folder = getFolder(meta.type ?: return, meta.mainName) + downloadSubtitle( + ctx, + ExtractorSubtitleLink(link.name, link.url, ""), + fileName, + folder + ) + } + } + + fun startDownload( + context: Context?, + episode: ResultEpisode, + currentIsMovie: Boolean, + currentHeaderName: String, + currentType: TvType, + currentPoster: String?, + apiName: String, + parentId: Int, + url: String, + links: List, + subs: List? + ) { + try { + if (context == null) return + + val meta = + getMeta( + episode, + currentHeaderName, + apiName, + currentPoster, + currentIsMovie, + currentType + ) + + val folder = getFolder(currentType, currentHeaderName) + + val src = "$DOWNLOAD_NAVIGATE_TO/$parentId" // url ?: return@let + + // SET VISUAL KEYS + AcraApplication.setKey( + DOWNLOAD_HEADER_CACHE, + parentId.toString(), + VideoDownloadHelper.DownloadHeaderCached( + apiName, + url, + currentType, + currentHeaderName, + currentPoster, + parentId, + System.currentTimeMillis(), + ) + ) + + AcraApplication.setKey( + DataStore.getFolderName( + DOWNLOAD_EPISODE_CACHE, + parentId.toString() + ), // 3 deep folder for faster acess + episode.id.toString(), + VideoDownloadHelper.DownloadEpisodeCached( + episode.name, + episode.poster, + episode.episode, + episode.season, + episode.id, + parentId, + episode.rating, + episode.description, + System.currentTimeMillis(), + ) + ) + + // DOWNLOAD VIDEO + VideoDownloadManager.downloadEpisodeUsingWorker( + context, + src,//url ?: return, + folder, + meta, + links + ) + + // 1. Checks if the lang should be downloaded + // 2. Makes it into the download format + // 3. Downloads it as a .vtt file + val downloadList = SubtitlesFragment.getDownloadSubsLanguageISO639_1() + subs?.let { subsList -> + subsList.filter { + downloadList.contains( + SubtitleHelper.fromLanguageToTwoLetters( + it.name, + true + ) + ) + } + .map { ExtractorSubtitleLink(it.name, it.url, "") } + .forEach { link -> + val fileName = VideoDownloadManager.getFileName(context, meta) + downloadSubtitle(context, link, fileName, folder) + } + } + } catch (e: Exception) { + logError(e) + } + } + + suspend fun downloadEpisode( + activity: Activity?, + episode: ResultEpisode, + currentIsMovie: Boolean, + currentHeaderName: String, + currentType: TvType, + currentPoster: String?, + apiName: String, + parentId: Int, + url: String, + ) { + safeApiCall { + val generator = RepoLinkGenerator(listOf(episode)) + val currentLinks = mutableSetOf() + val currentSubs = mutableSetOf() + generator.generateLinks(clearCache = false, isCasting = false, callback = { + it.first?.let { link -> + currentLinks.add(link) + } + }, subtitleCallback = { sub -> + currentSubs.add(sub) + }) + + if (currentLinks.isEmpty()) { + Coroutines.main { + CommonActivity.showToast( + activity, + R.string.no_links_found_toast, + Toast.LENGTH_SHORT + ) + } + return@safeApiCall + } + + startDownload( + activity, + episode, + currentIsMovie, + currentHeaderName, + currentType, + currentPoster, + apiName, + parentId, + url, + sortUrls(currentLinks), + sortSubs(currentSubs), + ) + } + } + + private fun getMeta( + episode: ResultEpisode, + titleName: String, + apiName: String, + currentPoster: String?, + currentIsMovie: Boolean, + tvType: TvType, + ): VideoDownloadManager.DownloadEpisodeMetadata { + return VideoDownloadManager.DownloadEpisodeMetadata( + episode.id, + VideoDownloadManager.sanitizeFilename(titleName), + apiName, + episode.poster ?: currentPoster, + episode.name, + if (currentIsMovie) null else episode.season, + if (currentIsMovie) null else episode.episode, + tvType, + ) + } + } + + private val _watchStatus: MutableLiveData = MutableLiveData(WatchType.NONE) + val watchStatus: LiveData get() = _watchStatus + + private val _selectPopup: MutableLiveData = MutableLiveData(null) + val selectPopup: LiveData get() = _selectPopup + + fun updateWatchStatus(status: WatchType) { + val currentId = currentId ?: return + val resultPage = currentResponse ?: return + _watchStatus.postValue(status) + + DataStoreHelper.setResultWatchState(currentId, status.internalId) + val current = DataStoreHelper.getBookmarkedData(currentId) + val currentTime = System.currentTimeMillis() + DataStoreHelper.setBookmarkedData( + currentId, + DataStoreHelper.BookmarkedData( + currentId, + current?.bookmarkedTime ?: currentTime, + currentTime, + resultPage.name, + resultPage.url, + resultPage.apiName, + resultPage.type, + resultPage.posterUrl, + resultPage.year + ) + ) + } + + private suspend fun startChromecast( + activity: Activity?, + result: ResultEpisode, + isVisible: Boolean = true + ) { + if (activity == null) return + val data = loadLinks(result, isVisible = isVisible, isCasting = true) + startChromecast(activity, result, data.links, data.subs, 0) + } + + private fun startChromecast( + activity: Activity?, + result: ResultEpisode, + links: List, + subs: List, + startIndex: Int, + ) { + if (activity == null) return + val response = currentResponse ?: return + val eps = currentEpisodes[currentIndex ?: return] ?: return + + activity.getCastSession()?.startCast( + response.apiName, + response.isMovie(), + response.name, + response.posterUrl, + result.index, + eps, + links, + subs, + startTime = result.getRealPosition(), + startIndex = startIndex + ) + } + + private val popupCallback: ((Int) -> Unit)? = null + + fun cancelLinks() { + currentLoadLinkJob?.cancel() + _loadedLinks.postValue(null) + } + + private var currentLoadLinkJob: Job? = null + private suspend fun acquireSingleLink( + result: ResultEpisode, + isCasting: Boolean, + text: UiText, + callback: (Pair) -> Unit, + ) { + currentLoadLinkJob = viewModelScope.launch { + val links = loadLinks(result, isVisible = true, isCasting = isCasting) + + _selectPopup.postValue( + SelectPopup.SelectText( + text, + links.links.map { txt("${it.name} ${Qualities.getStringByInt(it.quality)}") }) { + callback.invoke(links to (it ?: return@SelectText)) + }) + } + } + + private suspend fun acquireSingleSubtitle( + result: ResultEpisode, + isCasting: Boolean, + text: UiText, + callback: (Pair) -> Unit, + ) { + currentLoadLinkJob = viewModelScope.launch { + val links = loadLinks(result, isVisible = true, isCasting = isCasting) + + _selectPopup.postValue( + SelectPopup.SelectText( + text, + links.subs.map { txt(it.name) }) { + callback.invoke(links to (it ?: return@SelectText)) + }) + } + } + + suspend fun loadLinks( + result: ResultEpisode, + isVisible: Boolean, + isCasting: Boolean, + clearCache: Boolean = false, + ): LinkLoadingResult { + val tempGenerator = RepoLinkGenerator(listOf(result)) + + val links: MutableSet = mutableSetOf() + val subs: MutableSet = mutableSetOf() + fun updatePage() { + if (isVisible) { + _loadedLinks.postValue(LinkProgress(links.size, subs.size)) + } + } + try { + tempGenerator.generateLinks(clearCache, isCasting, { (link, _) -> + if (link != null) { + links += link + updatePage() + } + }, { sub -> + subs += sub + updatePage() + }) + } catch (e: Exception) { + logError(e) + } finally { + _loadedLinks.postValue(null) + } + + return LinkLoadingResult(sortUrls(links), sortSubs(subs)) + } + + private fun playWithVlc(act: Activity?, data: LinkLoadingResult, id: Int) = ioSafe { + if (act == null) return@ioSafe + try { + if (!act.checkWrite()) { + act.requestRW() + if (act.checkWrite()) return@ioSafe + } + + val outputDir = act.cacheDir + val outputFile = withContext(Dispatchers.IO) { + File.createTempFile("mirrorlist", ".m3u8", outputDir) + } + var text = "#EXTM3U" + for (sub in data.subs) { + text += "\n#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"${sub.name}\",DEFAULT=NO,AUTOSELECT=NO,FORCED=NO,LANGUAGE=\"${sub.name}\",URI=\"${sub.url}\"" + } + for (link in data.links) { + text += "\n#EXTINF:, ${link.name}\n${link.url}" + } + outputFile.writeText(text) + + val vlcIntent = Intent(VLC_INTENT_ACTION_RESULT) + + vlcIntent.setPackage(VLC_PACKAGE) + vlcIntent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) + vlcIntent.addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION) + vlcIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + vlcIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + + vlcIntent.setDataAndType( + FileProvider.getUriForFile( + act, + act.applicationContext.packageName + ".provider", + outputFile + ), "video/*" + ) + + val startId = VLC_FROM_PROGRESS + + var position = startId + if (startId == VLC_FROM_START) { + position = 1 + } else if (startId == VLC_FROM_PROGRESS) { + position = 0 + } + + vlcIntent.putExtra("position", position) + + vlcIntent.component = VLC_COMPONENT + act.setKey(VLC_LAST_ID_KEY, id) + act.startActivityForResult(vlcIntent, VLC_REQUEST_CODE) + } catch (e: Exception) { + logError(e) + CommonActivity.showToast(act, e.toString(), Toast.LENGTH_LONG) + } + } + + fun handleAction(activity: Activity?, click: EpisodeClickEvent) = viewModelScope.launch { + handleEpisodeClickEvent(activity, click) + } + + private suspend fun handleEpisodeClickEvent(activity: Activity?, click: EpisodeClickEvent) { + when (click.action) { + ACTION_SHOW_OPTIONS -> { + _selectPopup.postValue( + SelectPopup.SelectArray( + txt(""), // TODO FIX + R.array.episode_long_click_options, + R.array.episode_long_click_options_values + ) { result -> + if (result == null) return@SelectArray + viewModelScope.launch { + handleEpisodeClickEvent( + activity, + click.copy(action = result) + ) + } + }) + } + ACTION_CLICK_DEFAULT -> { + activity?.let { ctx -> + if (ctx.isConnectedToChromecast()) { + handleEpisodeClickEvent( + activity, + click.copy(action = ACTION_CHROME_CAST_EPISODE) + ) + } else { + handleEpisodeClickEvent( + activity, + click.copy(action = ACTION_PLAY_EPISODE_IN_PLAYER) + ) + } + } + } + ACTION_DOWNLOAD_EPISODE_SUBTITLE -> { + val response = currentResponse ?: return + + acquireSingleSubtitle( + click.data, + false, + txt(R.string.episode_action_download_subtitle) + ) { (links, index) -> + downloadSubtitle( + activity, + links.subs[index], + getMeta( + click.data, + response.name, + response.apiName, + response.posterUrl, + response.isMovie(), + response.type + ) + ) + CommonActivity.showToast( + activity, + R.string.download_started, + Toast.LENGTH_SHORT + ) + } + } + ACTION_SHOW_TOAST -> { + CommonActivity.showToast(activity, R.string.play_episode_toast, Toast.LENGTH_SHORT) + } + ACTION_DOWNLOAD_EPISODE -> { + val response = currentResponse ?: return + downloadEpisode( + activity, + click.data, + response.isMovie(), + response.name, + response.type, + response.posterUrl, + response.apiName, + response.getId(), + response.url + ) + } + ACTION_DOWNLOAD_MIRROR -> { + val response = currentResponse ?: return + acquireSingleLink( + click.data, + false, + txt(R.string.episode_action_download_mirror) + ) { (result, index) -> + startDownload( + activity, + click.data, + response.isMovie(), + response.name, + response.type, + response.posterUrl, + response.apiName, + response.getId(), + response.url, + listOf(result.links[index]), + result.subs, + ) + CommonActivity.showToast( + activity, + R.string.download_started, + Toast.LENGTH_SHORT + ) + } + } + ACTION_RELOAD_EPISODE -> { + loadLinks(click.data, isVisible = false, isCasting = false, clearCache = true) + } + ACTION_CHROME_CAST_MIRROR -> { + acquireSingleLink( + click.data, + false, + txt(R.string.episode_action_chromecast_mirror) + ) { (result, index) -> + startChromecast(activity, click.data, result.links, result.subs, index) + } + } + ACTION_PLAY_EPISODE_IN_BROWSER -> acquireSingleLink( + click.data, + false, + txt(R.string.episode_action_play_in_browser) + ) { (result, index) -> + try { + val i = Intent(Intent.ACTION_VIEW) + i.data = Uri.parse(result.links[index].url) + activity?.startActivity(i) + } catch (e: Exception) { + logError(e) + } + } + ACTION_COPY_LINK -> { + acquireSingleLink( + click.data, + false, + txt(R.string.episode_action_copy_link) + ) { (result, index) -> + val act = activity ?: return@acquireSingleLink + val serviceClipboard = + (act.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager?) + ?: return@acquireSingleLink + val link = result.links[index] + val clip = ClipData.newPlainText(link.name, link.url) + serviceClipboard.setPrimaryClip(clip) + CommonActivity.showToast(act, R.string.copy_link_toast, Toast.LENGTH_SHORT) + } + } + ACTION_CHROME_CAST_EPISODE -> { + startChromecast(activity, click.data) + } + ACTION_PLAY_EPISODE_IN_VLC_PLAYER -> { + currentLoadLinkJob = viewModelScope.launch { + playWithVlc(activity, loadLinks(click.data, true, true), click.data.id) + } + } + ACTION_PLAY_EPISODE_IN_PLAYER -> { + val data = currentResponse?.syncData?.toList() ?: emptyList() + val list = + HashMap().apply { putAll(data) } + + activity?.navigate( + R.id.global_to_navigation_player, + GeneratorPlayer.newInstance( + generator?.also { + it.getAll() // I know kinda shit to itterate all, but it is 100% sure to work + ?.indexOfFirst { value -> value is ResultEpisode && value.id == click.data.id } + ?.let { index -> + if (index > 0) + it.goto(index) + } + + } ?: return, list + ) + ) + } + } } private suspend fun applyMeta( @@ -385,7 +1065,7 @@ class ResultViewModel2 : ViewModel() { if (meta == null) return resp to false var updateEpisodes = false val out = resp.apply { - Log.i(ResultViewModel.TAG, "applyMeta") + Log.i(TAG, "applyMeta") duration = duration ?: meta.duration rating = rating ?: meta.publicScore @@ -497,8 +1177,6 @@ class ResultViewModel2 : ViewModel() { } private fun getEpisodes(indexer: EpisodeIndexer, range: EpisodeRange): List { - //TODO ADD GENERATOR - val startIndex = range.startIndex val length = range.length @@ -535,7 +1213,7 @@ class ResultViewModel2 : ViewModel() { _episodesCountText.postValue( txt( R.string.episode_format, - if (size == 1) R.string.episode else R.string.episodes, + txt(if (size == 1) R.string.episode else R.string.episodes), size ) ) @@ -563,6 +1241,10 @@ class ResultViewModel2 : ViewModel() { preferStartSeason = indexer.season preferDubStatus = indexer.dubStatus + generator = currentEpisodes[indexer]?.let { list -> + RepoLinkGenerator(list) + } + val ret = getEpisodes(indexer, range) _episodes.postValue(Resource.Success(ret)) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt index 097da927..40ad3913 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt @@ -44,9 +44,13 @@ class SyncViewModel : ViewModel() { // prefix, id private var syncs = mutableMapOf() - private val _syncIds: MutableLiveData> = - MutableLiveData(mutableMapOf()) - val syncIds: LiveData> get() = _syncIds + //private val _syncIds: MutableLiveData> = + // MutableLiveData(mutableMapOf()) + //val syncIds: LiveData> get() = _syncIds + + fun getSyncs() : Map { + return syncs + } private val _currentSynced: MutableLiveData> = MutableLiveData(getMissing()) @@ -76,7 +80,7 @@ class SyncViewModel : ViewModel() { Log.i(TAG, "addSync $idPrefix = $id") syncs[idPrefix] = id - _syncIds.postValue(syncs) + //_syncIds.postValue(syncs) return true } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt index 9e456156..6ec18b3a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.ui.result import android.content.Context +import android.util.Log import android.widget.ImageView import android.widget.TextView import androidx.annotation.DrawableRes @@ -12,16 +13,25 @@ import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.UIHelper.setImage sealed class UiText { - data class DynamicString(val value: String) : UiText() + companion object { + const val TAG = "UiText" + } + + data class DynamicString(val value: String) : UiText() { + override fun toString(): String = value + } class StringResource( @StringRes val resId: Int, - vararg val args: Any - ) : UiText() + val args: List + ) : UiText() { + override fun toString(): String = "resId = $resId\nargs = ${args.toList().map { "(${it::class} = $it)" }}" + } fun asStringNull(context: Context?): String? { try { return asString(context ?: return null) } catch (e: Exception) { + Log.e(TAG, "Got invalid data from $this") logError(e) return null } @@ -30,7 +40,19 @@ sealed class UiText { fun asString(context: Context): String { return when (this) { is DynamicString -> value - is StringResource -> context.getString(resId, *args) + is StringResource -> { + val str = context.getString(resId) + if (args.isEmpty()) { + str + } else { + str.format(*args.map { + when (it) { + is UiText -> it.asString(context) + else -> it + } + }.toTypedArray()) + } + } } } } @@ -98,7 +120,7 @@ fun txt(value: String?): UiText? { } fun txt(@StringRes resId: Int, vararg args: Any): UiText { - return UiText.StringResource(resId, args) + return UiText.StringResource(resId, args.toList()) } @JvmName("txtNull") @@ -106,7 +128,7 @@ fun txt(@StringRes resId: Int?, vararg args: Any?): UiText? { if (resId == null || args.any { it == null }) { return null } - return UiText.StringResource(resId, args) + return UiText.StringResource(resId, args.filterNotNull().toList()) } fun TextView?.setText(text: UiText?) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt index adfe151e..1de89809 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt @@ -27,7 +27,7 @@ object SearchHelper { } else { if (card.isFromDownload) { handleDownloadClick( - activity, card.name, DownloadClickEvent( + activity, DownloadClickEvent( DOWNLOAD_ACTION_PLAY_FILE, VideoDownloadHelper.DownloadEpisodeCached( card.name, From a99713fe0c2f33f3ef0c519d1ba951f65d9dc4ce Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Wed, 3 Aug 2022 02:04:03 +0200 Subject: [PATCH 04/22] viewmodel dialogs --- .../com/lagradost/cloudstream3/MainAPI.kt | 1 + .../lagradost/cloudstream3/MainActivity.kt | 5 +- .../animeproviders/GogoanimeProvider.kt | 2 +- .../cloudstream3/mvvm/ArchComponentExt.kt | 26 ++ .../cloudstream3/ui/player/GeneratorPlayer.kt | 3 + .../cloudstream3/ui/result/ResultFragment.kt | 167 ++++++- .../ui/result/ResultViewModel2.kt | 436 +++++++++++++----- .../cloudstream3/ui/result/UiText.kt | 9 + .../lagradost/cloudstream3/utils/AppUtils.kt | 10 +- .../cloudstream3/utils/Coroutines.kt | 4 +- .../utils/SingleSelectionHelper.kt | 76 ++- app/src/main/res/layout/bottom_loading.xml | 33 ++ .../layout/bottom_selection_dialog_direct.xml | 34 ++ app/src/main/res/layout/fragment_result.xml | 1 - app/src/main/res/layout/result_poster.xml | 2 +- ...sort_bottom_single_choice_no_checkmark.xml | 22 + app/src/main/res/values/styles.xml | 16 +- 17 files changed, 655 insertions(+), 192 deletions(-) create mode 100644 app/src/main/res/layout/bottom_loading.xml create mode 100644 app/src/main/res/layout/bottom_selection_dialog_direct.xml create mode 100644 app/src/main/res/layout/sort_bottom_single_choice_no_checkmark.xml diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 702ff6a1..b34fc6d7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -1114,6 +1114,7 @@ data class NextAiring( data class SeasonData( val season: Int, val name: String? = null, + val displaySeason : Int? = null, // will use season if null ) interface EpisodeResponse { diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 86a0aafe..8719936e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -332,6 +332,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { if (str.contains(appString)) { for (api in OAuth2Apis) { if (str.contains("/${api.redirectUrl}")) { + val activity = this ioSafe { Log.i(TAG, "handleAppIntent $str") val isSuccessful = api.handleRedirect(str) @@ -342,10 +343,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { Log.i(TAG, "failed to authenticate ${api.name}") } - this.runOnUiThread { + activity.runOnUiThread { try { showToast( - this, + activity, getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail).format( api.name ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/animeproviders/GogoanimeProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/animeproviders/GogoanimeProvider.kt index b1a9a629..7aaab91d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/animeproviders/GogoanimeProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/animeproviders/GogoanimeProvider.kt @@ -54,7 +54,7 @@ class GogoanimeProvider : MainAPI() { secretKeyString: String, encrypt: Boolean = true ): String { - println("IV: $iv, Key: $secretKeyString, encrypt: $encrypt, Message: $string") + //println("IV: $iv, Key: $secretKeyString, encrypt: $encrypt, Message: $string") val ivParameterSpec = IvParameterSpec(iv.toByteArray()) val secretKey = SecretKeySpec(secretKeyString.toByteArray(), "AES") val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") diff --git a/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt b/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt index 2a8c49f7..c831884f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt @@ -51,6 +51,32 @@ fun LifecycleOwner.observeDirectly(liveData: LiveData, action: (t: T) -> action(currentValue) } +inline fun some(value: T?): Some { + return if (value == null) { + Some.None + } else { + Some.Success(value) + } +} + +sealed class Some { + data class Success(val value: T) : Some() + object None : Some() + + override fun toString(): String { + return when(this) { + is None -> "None" + is Success -> "Some(${value.toString()})" + } + } +} + +sealed class ResourceSome { + data class Success(val value: T) : ResourceSome() + object None : ResourceSome() + data class Loading(val data: Any? = null) : ResourceSome() +} + sealed class Resource { data class Success(val value: T) : Resource() data class Failure( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index 1973d7bd..d0b03774 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -125,6 +125,7 @@ class GeneratorPlayer : FullScreenPlayer() { private fun loadExtractorJob(extractorLink: ExtractorLink?) { currentVerifyLink?.cancel() + extractorLink?.let { currentVerifyLink = ioSafe { if (it.extractorData != null) { @@ -488,7 +489,9 @@ class GeneratorPlayer : FullScreenPlayer() { .setView(R.layout.player_select_source_and_subs) val sourceDialog = sourceBuilder.create() + selectSourceDialog = sourceDialog + sourceDialog.show() val providerList = sourceDialog.sort_providers val subtitleList = sourceDialog.sort_subtitles diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt index b8dd6e8b..170e9da7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.ui.result import android.annotation.SuppressLint +import android.app.Dialog import android.content.Intent import android.content.Intent.* import android.content.res.ColorStateList @@ -15,6 +16,7 @@ import android.view.View import android.view.ViewGroup import android.widget.AbsListView import android.widget.ArrayAdapter +import android.widget.ImageView import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.core.view.isGone @@ -28,15 +30,13 @@ import com.discord.panels.PanelsChildGestureRegionObserver import com.google.android.gms.cast.framework.CastButtonFactory import com.google.android.gms.cast.framework.CastContext import com.google.android.gms.cast.framework.CastState +import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.button.MaterialButton import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.getApiFromName import com.lagradost.cloudstream3.APIHolder.updateHasTrailers import com.lagradost.cloudstream3.CommonActivity.showToast -import com.lagradost.cloudstream3.mvvm.Resource -import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.mvvm.normalSafeApiCall -import com.lagradost.cloudstream3.mvvm.observe +import com.lagradost.cloudstream3.mvvm.* import com.lagradost.cloudstream3.syncproviders.providers.Kitsu import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick @@ -54,8 +54,10 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog +import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute +import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard @@ -68,6 +70,7 @@ import kotlinx.android.synthetic.main.fragment_trailer.* import kotlinx.android.synthetic.main.result_recommendations.* import kotlinx.android.synthetic.main.result_sync.* import kotlinx.android.synthetic.main.trailer_custom_layout.* +import kotlinx.coroutines.runBlocking const val START_ACTION_NORMAL = 0 const val START_ACTION_RESUME_LATEST = 1 @@ -206,8 +209,6 @@ class ResultFragment : ResultTrailerPlayer() { private var updateUIListener: (() -> Unit)? = null } - private var currentLoadingCount = - 0 // THIS IS USED TO PREVENT LATE EVENTS, AFTER DISMISS WAS CLICKED private lateinit var viewModel: ResultViewModel2 //by activityViewModels() private lateinit var syncModel: SyncViewModel @@ -418,7 +419,8 @@ class ResultFragment : ResultTrailerPlayer() { viewModel.reloadEpisodes() } - var apiName: String = "" + var loadingDialog: Dialog? = null + var popupDialog: Dialog? = null @SuppressLint("SetTextI18n") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -466,7 +468,7 @@ class ResultFragment : ResultTrailerPlayer() { // activity?.fixPaddingStatusbar(result_toolbar) val url = arguments?.getString(URL_BUNDLE) - apiName = arguments?.getString(API_NAME_BUNDLE) ?: return + val apiName = arguments?.getString(API_NAME_BUNDLE) ?: return startAction = arguments?.getInt(START_ACTION_BUNDLE) ?: START_ACTION_NORMAL startValue = arguments?.getInt(START_VALUE_BUNDLE) val resumeEpisode = arguments?.getInt(EPISODE_BUNDLE) @@ -862,16 +864,16 @@ class ResultFragment : ResultTrailerPlayer() { */ observe(viewModel.episodes) { episodes -> when (episodes) { - is Resource.Failure -> { + is ResourceSome.None -> { result_episode_loading?.isVisible = false - //result_episodes?.isVisible = false + result_episodes?.isVisible = false } - is Resource.Loading -> { + is ResourceSome.Loading -> { result_episode_loading?.isVisible = true - // result_episodes?.isVisible = false + result_episodes?.isVisible = false } - is Resource.Success -> { - //result_episodes?.isVisible = true + is ResourceSome.Success -> { + result_episodes?.isVisible = true result_episode_loading?.isVisible = false (result_episodes?.adapter as? EpisodeAdapter?)?.updateList(episodes.value) } @@ -879,7 +881,7 @@ class ResultFragment : ResultTrailerPlayer() { } observe(viewModel.selectedSeason) { text -> - result_season_button?.setText(text) + result_season_button.setText(text) // If the season button is visible the result season button will be next focus down if (result_season_button?.isVisible == true) @@ -901,6 +903,70 @@ class ResultFragment : ResultTrailerPlayer() { } } + observe(viewModel.selectPopup) { popup -> + println("POPUPSTATUS:$popup") + when (popup) { + is Some.Success -> { + popupDialog?.dismissSafe(activity) + + popupDialog = activity?.let { act -> + val pop = popup.value + val options = pop.getOptions(act) + val title = pop.getTitle(act) + + act.showBottomDialogInstant( + options, title, { + popupDialog = null + pop.callback(context ?: return@showBottomDialogInstant, null) + }, { + popupDialog = null + pop.callback(context ?: return@showBottomDialogInstant, it) + } + ) + } + } + is Some.None -> { + popupDialog?.dismissSafe(activity) + popupDialog = null + } + } + + //showBottomDialogInstant + } + + observe(viewModel.loadedLinks) { load -> + + when (load) { + is Some.Success -> { + if(loadingDialog?.isShowing != true) { + loadingDialog?.dismissSafe(activity) + loadingDialog = null + } + loadingDialog = loadingDialog ?: context?.let { ctx -> + val builder = + BottomSheetDialog(ctx) + builder.setContentView(R.layout.bottom_loading) + builder.setOnDismissListener { + loadingDialog = null + viewModel.cancelLinks() + } + //builder.setOnCancelListener { + // it?.dismiss() + //} + builder.setCanceledOnTouchOutside(true) + + builder.show() + + builder + } + } + is Some.None -> { + loadingDialog?.dismissSafe(activity) + loadingDialog = null + } + } + } + observe(viewModel.selectedRange) { range -> result_episode_select.setText(range) @@ -933,7 +999,8 @@ class ResultFragment : ResultTrailerPlayer() { } observe(viewModel.rangeSelections) { range -> - result_episode_select.setOnClickListener { view -> + println("RANGE:$range") + result_episode_select?.setOnClickListener { view -> view?.context?.let { ctx -> val names = range .mapNotNull { (text, r) -> @@ -987,6 +1054,33 @@ class ResultFragment : ResultTrailerPlayer() { setRecommendations(recommendations, null) } + observe(viewModel.movie) { data -> + when (data) { + is ResourceSome.Success -> { + data.value.let { (text, ep) -> + result_play_movie.setText(text) + result_play_movie?.setOnClickListener { + viewModel.handleAction( + activity, + EpisodeClickEvent(ACTION_CLICK_DEFAULT, ep) + ) + } + result_play_movie?.setOnLongClickListener { + viewModel.handleAction( + activity, + EpisodeClickEvent(ACTION_SHOW_OPTIONS, ep) + ) + return@setOnLongClickListener true + } + } + } + else -> { + result_play_movie?.isVisible = false + + } + } + } + observe(viewModel.page) { data -> when (data) { is Resource.Success -> { @@ -1006,9 +1100,32 @@ class ResultFragment : ResultTrailerPlayer() { result_cast_text.setText(d.actorsText) result_next_airing.setText(d.nextAiringEpisode) result_next_airing_time.setText(d.nextAiringDate) - result_poster.setImage(d.posterImage) - result_play_movie.setText(d.playMovieText) + + if (d.posterImage != null && context?.isTrueTvSettings() == false) + result_poster_holder?.setOnClickListener { + try { + context?.let { ctx -> + runBlocking { + val sourceBuilder = AlertDialog.Builder(ctx) + sourceBuilder.setView(R.layout.result_poster) + + val sourceDialog = sourceBuilder.create() + sourceDialog.show() + + sourceDialog.findViewById(R.id.imgPoster) + ?.apply { + setImage(d.posterImage) + setOnClickListener { + sourceDialog.dismissSafe() + } + } + } + } + } catch (e: Exception) { + logError(e) + } + } result_cast_items?.isVisible = d.actors != null @@ -1016,6 +1133,7 @@ class ResultFragment : ResultTrailerPlayer() { updateList(d.actors ?: emptyList()) } + result_open_in_browser?.isGone = d.url.isBlank() result_open_in_browser?.setOnClickListener { val i = Intent(ACTION_VIEW) i.data = Uri.parse(d.url) @@ -1238,15 +1356,14 @@ class ResultFragment : ResultTrailerPlayer() { Kitsu.isEnabled = settingsManager.getBoolean(ctx.getString(R.string.show_kitsu_posters_key), true) - val tempUrl = url - if (tempUrl != null) { + if (url != null) { result_reload_connectionerror.setOnClickListener { - viewModel.load(tempUrl, apiName, showFillers, DubStatus.Dubbed, 0, 0) //TODO FIX + viewModel.load(url, apiName, showFillers, DubStatus.Dubbed, 0, 0) //TODO FIX } result_reload_connection_open_in_browser?.setOnClickListener { val i = Intent(ACTION_VIEW) - i.data = Uri.parse(tempUrl) + i.data = Uri.parse(url) try { startActivity(i) } catch (e: Exception) { @@ -1256,7 +1373,7 @@ class ResultFragment : ResultTrailerPlayer() { result_open_in_browser?.setOnClickListener { val i = Intent(ACTION_VIEW) - i.data = Uri.parse(tempUrl) + i.data = Uri.parse(url) try { startActivity(i) } catch (e: Exception) { @@ -1267,7 +1384,7 @@ class ResultFragment : ResultTrailerPlayer() { // bloats the navigation on tv if (context?.isTrueTvSettings() == false) { result_meta_site?.setOnClickListener { - it.context?.openBrowser(tempUrl) + it.context?.openBrowser(url) } result_meta_site?.isFocusable = true } else { @@ -1276,7 +1393,7 @@ class ResultFragment : ResultTrailerPlayer() { if (restart || !viewModel.hasLoaded()) { //viewModel.clear() - viewModel.load(tempUrl, apiName, showFillers, DubStatus.Dubbed, 0, 0) //TODO FIX + viewModel.load(url, apiName, showFillers, DubStatus.Dubbed, 0, 0) //TODO FIX } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt index 544445a3..d6ee4a31 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt @@ -36,17 +36,16 @@ import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.utils.AppUtils.getNameFull import com.lagradost.cloudstream3.utils.AppUtils.isConnectedToChromecast import com.lagradost.cloudstream3.utils.CastHelper.startCast import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.DataStore.setKey +import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultWatchState import com.lagradost.cloudstream3.utils.UIHelper.checkWrite import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.requestRW -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import kotlinx.coroutines.* import java.io.File import java.util.concurrent.TimeUnit @@ -86,7 +85,6 @@ data class ResultData( val yearText: UiText?, val nextAiringDate: UiText?, val nextAiringEpisode: UiText?, - val playMovieText: UiText?, val plotHeaderText: UiText, ) @@ -118,7 +116,7 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData { val hours: Long = TimeUnit.SECONDS.toHours(seconds) - days * 24 val minute = TimeUnit.SECONDS.toMinutes(seconds) - TimeUnit.SECONDS.toHours(seconds) * 60 - nextAiringEpisode = when { + nextAiringDate = when { days > 0 -> { txt( R.string.next_episode_time_day_format, @@ -138,11 +136,11 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData { ) else -> null }?.also { - nextAiringDate = txt(R.string.next_episode_format, airing.episode) + nextAiringEpisode = txt(R.string.next_episode_format, airing.episode) } } } - + val dur = duration return ResultData( syncData = syncData, plotHeaderText = txt( @@ -151,14 +149,6 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData { else -> R.string.result_plot } ), - playMovieText = txt( - when (this.type) { - TvType.Live -> R.string.play_livestream_button - TvType.Torrent -> R.string.play_torrent_button - TvType.Movie, TvType.AnimeMovie -> R.string.play_movie_button - else -> null - } - ), nextAiringDate = nextAiringDate, nextAiringEpisode = nextAiringEpisode, posterImage = img( @@ -192,7 +182,7 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData { TvType.Live -> R.string.live_singular } ), - yearText = txt(year), + yearText = txt(year?.toString()), apiName = txt(apiName), ratingText = rating?.div(1000f)?.let { txt(R.string.rating_format, it) }, vpnText = txt( @@ -204,7 +194,10 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData { ), metaText = if (repo.providerType == ProviderType.MetaProvider) txt(R.string.provider_info_meta) else null, - durationText = txt(R.string.duration_format, duration), + durationText = if (dur == null || dur <= 0) null else txt( + R.string.duration_format, + dur + ), onGoingText = if (this is EpisodeResponse) { txt( when (showStatus) { @@ -245,21 +238,43 @@ sealed class SelectPopup { val map: Int?, val callback: (Int?) -> Unit ) : SelectPopup() +} - fun SelectPopup.transformResult(context: Context, input: Int?): Int? { - if (input == null) return null - return when (this) { - is SelectArray -> context.resources.getIntArray(map ?: return input).getOrNull(input) - ?: input - is SelectText -> input - } +fun SelectPopup.callback(context: Context, input: Int?) { + val ret = transformResult(context, input) + return when (this) { + is SelectPopup.SelectArray -> callback(ret) + is SelectPopup.SelectText -> callback(ret) } +} - fun SelectPopup.getOptions(context: Context): List { - return when (this) { - is SelectArray -> context.resources.getStringArray(options).toList() - is SelectText -> options.map { it.asString(context) } +fun SelectPopup.transformResult(context: Context, input: Int?): Int? { + if (input == null) return null + return when (this) { + is SelectPopup.SelectArray -> context.resources.getIntArray(map ?: return input) + .getOrNull(input) + ?: input + is SelectPopup.SelectText -> input + } +} + +fun SelectPopup.getTitle(context: Context): String { + return when (this) { + is SelectPopup.SelectArray -> text.asString(context) + is SelectPopup.SelectText -> text.asString(context) + } +} + +fun SelectPopup.getOptions(context: Context): List { + return when (this) { + is SelectPopup.SelectArray -> { + val cmap = this.map?.let { context.resources.getIntArray(it) } + context.resources.getStringArray(options).toList().filterIndexed { index, s -> + + true + } } + is SelectPopup.SelectText -> options.map { it.asString(context) } } } @@ -274,6 +289,8 @@ class ResultViewModel2 : ViewModel() { /** map>> */ private var currentEpisodes: Map> = mapOf() private var currentRanges: Map> = mapOf() + private var currentSeasons: Set = setOf() + private var currentDubStatus: Set = setOf() private var currentMeta: SyncAPI.SyncResult? = null private var currentSync: Map? = null private var currentIndex: EpisodeIndexer? = null @@ -294,13 +311,17 @@ class ResultViewModel2 : ViewModel() { MutableLiveData(Resource.Loading()) val page: LiveData> = _page - private val _episodes: MutableLiveData>> = - MutableLiveData(Resource.Loading()) - val episodes: LiveData>> = _episodes + private val _episodes: MutableLiveData>> = + MutableLiveData(ResourceSome.Loading()) + val episodes: LiveData>> = _episodes - private val _episodesCountText: MutableLiveData = - MutableLiveData(null) - val episodesCountText: LiveData = _episodesCountText + private val _movie: MutableLiveData>> = + MutableLiveData(ResourceSome.None) + val movie: LiveData>> = _movie + + private val _episodesCountText: MutableLiveData> = + MutableLiveData(Some.None) + val episodesCountText: LiveData> = _episodesCountText private val _trailers: MutableLiveData> = MutableLiveData(mutableListOf()) val trailers: LiveData> = _trailers @@ -318,24 +339,23 @@ class ResultViewModel2 : ViewModel() { MutableLiveData(emptyList()) val seasonSelections: LiveData>> = _seasonSelections - private val _recommendations: MutableLiveData> = MutableLiveData(emptyList()) val recommendations: LiveData> = _recommendations - private val _selectedRange: MutableLiveData = - MutableLiveData(null) - val selectedRange: LiveData = _selectedRange + private val _selectedRange: MutableLiveData> = + MutableLiveData(Some.None) + val selectedRange: LiveData> = _selectedRange - private val _selectedSeason: MutableLiveData = - MutableLiveData(null) - val selectedSeason: LiveData = _selectedSeason + private val _selectedSeason: MutableLiveData> = + MutableLiveData(Some.None) + val selectedSeason: LiveData> = _selectedSeason - private val _selectedDubStatus: MutableLiveData = MutableLiveData(null) - val selectedDubStatus: LiveData = _selectedDubStatus + private val _selectedDubStatus: MutableLiveData> = MutableLiveData(Some.None) + val selectedDubStatus: LiveData> = _selectedDubStatus - private val _loadedLinks: MutableLiveData = MutableLiveData(null) - val loadedLinks: LiveData = _loadedLinks + private val _loadedLinks: MutableLiveData> = MutableLiveData(Some.None) + val loadedLinks: LiveData> = _loadedLinks companion object { const val TAG = "RVM2" @@ -680,8 +700,8 @@ class ResultViewModel2 : ViewModel() { private val _watchStatus: MutableLiveData = MutableLiveData(WatchType.NONE) val watchStatus: LiveData get() = _watchStatus - private val _selectPopup: MutableLiveData = MutableLiveData(null) - val selectPopup: LiveData get() = _selectPopup + private val _selectPopup: MutableLiveData> = MutableLiveData(Some.None) + val selectPopup: LiveData> get() = _selectPopup fun updateWatchStatus(status: WatchType) { val currentId = currentId ?: return @@ -707,14 +727,15 @@ class ResultViewModel2 : ViewModel() { ) } - private suspend fun startChromecast( + private fun startChromecast( activity: Activity?, result: ResultEpisode, isVisible: Boolean = true ) { if (activity == null) return - val data = loadLinks(result, isVisible = isVisible, isCasting = true) - startChromecast(activity, result, data.links, data.subs, 0) + loadLinks(result, isVisible = isVisible, isCasting = true) { data -> + startChromecast(activity, result, data.links, data.subs, 0) + } } private fun startChromecast( @@ -742,51 +763,100 @@ class ResultViewModel2 : ViewModel() { ) } - private val popupCallback: ((Int) -> Unit)? = null - fun cancelLinks() { + println("called::cancelLinks") currentLoadLinkJob?.cancel() - _loadedLinks.postValue(null) + currentLoadLinkJob = null + _loadedLinks.postValue(Some.None) + } + + private fun postPopup(text: UiText, options: List, callback: suspend (Int?) -> Unit) { + _selectPopup.postValue( + some(SelectPopup.SelectText( + text, + options + ) { value -> + viewModelScope.launch { + _selectPopup.postValue(Some.None) + callback.invoke(value) + } + }) + ) + } + + private fun postPopup( + text: UiText, + options: Int, + values: Int, + callback: suspend (Int?) -> Unit + ) { + _selectPopup.postValue( + some(SelectPopup.SelectArray( + text, + options, + values + ) { value -> + viewModelScope.launch { + _selectPopup.value = Some.None + callback.invoke(value) + } + }) + ) + } + + fun loadLinks( + result: ResultEpisode, + isVisible: Boolean, + isCasting: Boolean, + clearCache: Boolean = false, + work: suspend (CoroutineScope.(LinkLoadingResult) -> Unit) + ) { + currentLoadLinkJob?.cancel() + currentLoadLinkJob = ioSafe { + val links = loadLinks( + result, + isVisible = isVisible, + isCasting = isCasting, + clearCache = clearCache + ) + if (!this.isActive) return@ioSafe + work(links) + } } private var currentLoadLinkJob: Job? = null - private suspend fun acquireSingleLink( + private fun acquireSingleLink( result: ResultEpisode, isCasting: Boolean, text: UiText, callback: (Pair) -> Unit, ) { - currentLoadLinkJob = viewModelScope.launch { - val links = loadLinks(result, isVisible = true, isCasting = isCasting) - - _selectPopup.postValue( - SelectPopup.SelectText( - text, - links.links.map { txt("${it.name} ${Qualities.getStringByInt(it.quality)}") }) { - callback.invoke(links to (it ?: return@SelectText)) - }) + loadLinks(result, isVisible = true, isCasting = isCasting) { links -> + postPopup( + text, + links.links.map { txt("${it.name} ${Qualities.getStringByInt(it.quality)}") }) { + callback.invoke(links to (it ?: return@postPopup)) + } } } - private suspend fun acquireSingleSubtitle( + private fun acquireSingleSubtitle( result: ResultEpisode, isCasting: Boolean, text: UiText, callback: (Pair) -> Unit, ) { - currentLoadLinkJob = viewModelScope.launch { - val links = loadLinks(result, isVisible = true, isCasting = isCasting) - - _selectPopup.postValue( - SelectPopup.SelectText( - text, - links.subs.map { txt(it.name) }) { - callback.invoke(links to (it ?: return@SelectText)) - }) + loadLinks(result, isVisible = true, isCasting = isCasting) { links -> + postPopup( + text, + links.subs.map { txt(it.name) }) + { + callback.invoke(links to (it ?: return@postPopup)) + } } } - suspend fun loadLinks( + suspend fun CoroutineScope.loadLinks( result: ResultEpisode, isVisible: Boolean, isCasting: Boolean, @@ -797,11 +867,12 @@ class ResultViewModel2 : ViewModel() { val links: MutableSet = mutableSetOf() val subs: MutableSet = mutableSetOf() fun updatePage() { - if (isVisible) { - _loadedLinks.postValue(LinkProgress(links.size, subs.size)) + if (isVisible && isActive) { + _loadedLinks.postValue(some(LinkProgress(links.size, subs.size))) } } try { + updatePage() tempGenerator.generateLinks(clearCache, isCasting, { (link, _) -> if (link != null) { links += link @@ -814,7 +885,7 @@ class ResultViewModel2 : ViewModel() { } catch (e: Exception) { logError(e) } finally { - _loadedLinks.postValue(null) + _loadedLinks.postValue(Some.None) } return LinkLoadingResult(sortUrls(links), sortSubs(subs)) @@ -884,20 +955,22 @@ class ResultViewModel2 : ViewModel() { private suspend fun handleEpisodeClickEvent(activity: Activity?, click: EpisodeClickEvent) { when (click.action) { ACTION_SHOW_OPTIONS -> { - _selectPopup.postValue( - SelectPopup.SelectArray( - txt(""), // TODO FIX - R.array.episode_long_click_options, - R.array.episode_long_click_options_values - ) { result -> - if (result == null) return@SelectArray - viewModelScope.launch { - handleEpisodeClickEvent( - activity, - click.copy(action = result) - ) - } - }) + postPopup( + txt( + activity?.getNameFull( + click.data.name, + click.data.episode, + click.data.season + ) ?: "" + ), // TODO FIX + R.array.episode_long_click_options, + R.array.episode_long_click_options_values + ) { result -> + handleEpisodeClickEvent( + activity, + click.copy(action = result ?: return@postPopup) + ) + } } ACTION_CLICK_DEFAULT -> { activity?.let { ctx -> @@ -986,7 +1059,14 @@ class ResultViewModel2 : ViewModel() { } } ACTION_RELOAD_EPISODE -> { - loadLinks(click.data, isVisible = false, isCasting = false, clearCache = true) + ioSafe { + loadLinks( + click.data, + isVisible = false, + isCasting = false, + clearCache = true + ) + } } ACTION_CHROME_CAST_MIRROR -> { acquireSingleLink( @@ -1030,8 +1110,12 @@ class ResultViewModel2 : ViewModel() { startChromecast(activity, click.data) } ACTION_PLAY_EPISODE_IN_VLC_PLAYER -> { - currentLoadLinkJob = viewModelScope.launch { - playWithVlc(activity, loadLinks(click.data, true, true), click.data.id) + loadLinks(click.data, isVisible = true, isCasting = true) { links -> + playWithVlc( + activity, + links, + click.data.id + ) } } ACTION_PLAY_EPISODE_IN_PLAYER -> { @@ -1176,6 +1260,13 @@ class ResultViewModel2 : ViewModel() { postEpisodeRange(currentIndex?.copy(season = season), currentRange) } + private fun getMovie(): ResultEpisode? { + return currentEpisodes.entries.firstOrNull()?.value?.firstOrNull()?.let { ep -> + val posDur = DataStoreHelper.getViewPos(ep.id) + ep.copy(position = posDur?.position ?: 0, duration = posDur?.duration ?: 0) + } + } + private fun getEpisodes(indexer: EpisodeIndexer, range: EpisodeRange): List { val startIndex = range.startIndex val length = range.length @@ -1192,15 +1283,50 @@ class ResultViewModel2 : ViewModel() { ?: emptyList() } + private fun postMovie() { + val response = currentResponse + _episodes.postValue(ResourceSome.None) + + if (response == null) { + _movie.postValue(ResourceSome.None) + return + } + + val text = txt( + when (response.type) { + TvType.Torrent -> R.string.play_torrent_button + else -> { + if (response.type.isLiveStream()) + R.string.play_livestream_button + else if (response.type.isMovieType()) // this wont break compatibility as you only need to override isMovieType + R.string.play_movie_button + else null + } + } + ) + val data = getMovie() + _episodes.postValue(ResourceSome.None) + if (text == null || data == null) { + _movie.postValue(ResourceSome.None) + } else { + _movie.postValue(ResourceSome.Success(text to data)) + } + } + fun reloadEpisodes() { - _episodes.postValue( - Resource.Success( - getEpisodes( - currentIndex ?: return, - currentRange ?: return + if (currentResponse?.isMovie() == true) { + postMovie() + } else { + _episodes.postValue( + ResourceSome.Success( + getEpisodes( + currentIndex ?: return, + currentRange ?: return + ) ) ) - ) + _movie.postValue(ResourceSome.None) + } } private fun postEpisodeRange(indexer: EpisodeIndexer?, range: EpisodeRange?) { @@ -1208,45 +1334,79 @@ class ResultViewModel2 : ViewModel() { return } - val size = currentEpisodes[indexer]?.size - - _episodesCountText.postValue( - txt( - R.string.episode_format, - txt(if (size == 1) R.string.episode else R.string.episodes), - size - ) - ) - + val episodes = currentEpisodes[indexer] + val ranges = currentRanges[indexer] + val size = episodes?.size + val isMovie = currentResponse?.isMovie() == true currentIndex = indexer currentRange = range + + _rangeSelections.postValue(ranges?.map { r -> + val text = txt(R.string.episodes_range, r.startEpisode, r.endEpisode) + text to r + } ?: emptyList()) + + _episodesCountText.postValue( + some( + if (isMovie) null else + txt( + R.string.episode_format, + size, + txt(if (size == 1) R.string.episode else R.string.episodes), + ) + ) + ) + _selectedSeason.postValue( - when (indexer.season) { - 0 -> txt(R.string.no_season) - else -> txt(R.string.season_format, R.string.season, indexer.season) //TODO FIX - } + some( + if (isMovie || currentSeasons.size <= 1) null else + when (indexer.season) { + 0 -> txt(R.string.no_season) + else -> txt( + R.string.season_format, + txt(R.string.season), + indexer.season + ) //TODO FIX DISPLAYNAME + } + ) ) + _selectedRange.postValue( - if ((currentRanges[indexer]?.size ?: 0) > 1) { - txt(R.string.episodes_range, range.startEpisode, range.endEpisode) - } else { - null - } + some( + if (isMovie) null else if ((currentRanges[indexer]?.size ?: 0) > 1) { + txt(R.string.episodes_range, range.startEpisode, range.endEpisode) + } else { + null + } + ) + ) + _selectedDubStatus.postValue( + some( + if (isMovie || currentDubStatus.size <= 1) null else + txt(indexer.dubStatus) + ) ) - _selectedDubStatus.postValue(txt(indexer.dubStatus)) //TODO SET KEYS preferStartEpisode = range.startEpisode preferStartSeason = indexer.season preferDubStatus = indexer.dubStatus - generator = currentEpisodes[indexer]?.let { list -> - RepoLinkGenerator(list) + generator = if (isMovie) { + getMovie()?.let { RepoLinkGenerator(listOf(it)) } + } else { + episodes?.let { list -> + RepoLinkGenerator(list) + } } - val ret = getEpisodes(indexer, range) - _episodes.postValue(Resource.Success(ret)) + if (isMovie) { + postMovie() + } else { + val ret = getEpisodes(indexer, range) + _episodes.postValue(ResourceSome.Success(ret)) + } } private suspend fun postSuccessful( @@ -1262,11 +1422,13 @@ class ResultViewModel2 : ViewModel() { } private suspend fun postEpisodes(loadResponse: LoadResponse, updateFillers: Boolean) { - _episodes.postValue(Resource.Loading()) + _episodes.postValue(ResourceSome.Loading()) val mainId = loadResponse.getId() currentId = mainId + _watchStatus.postValue(getResultWatchState(mainId)) + if (updateFillers && loadResponse is AnimeLoadResponse) { updateFillers(loadResponse.name) } @@ -1425,10 +1587,30 @@ class ResultViewModel2 : ViewModel() { } } + val seasonsSelection = mutableSetOf() + val dubSelection = mutableSetOf() + allEpisodes.keys.forEach { key -> + seasonsSelection += key.season + dubSelection += key.dubStatus + } + currentDubStatus = dubSelection + currentSeasons = seasonsSelection + _dubSubSelections.postValue(dubSelection.map { txt(it) to it }) + if (loadResponse is EpisodeResponse) { + _seasonSelections.postValue(seasonsSelection.map { seasonNumber -> + val name = + loadResponse.seasonNames?.firstOrNull { it.season == seasonNumber }?.name?.let { seasonData -> + txt(seasonData) + } ?: txt(R.string.season_format, txt(R.string.season), seasonNumber) + name to seasonNumber + }) + } + 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( @@ -1464,7 +1646,7 @@ class ResultViewModel2 : ViewModel() { ) = viewModelScope.launch { _page.postValue(Resource.Loading(url)) - _episodes.postValue(Resource.Loading(url)) + _episodes.postValue(ResourceSome.Loading()) preferDubStatus = dubStatus currentShowFillers = showFillers diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt index 6ec18b3a..03212133 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt @@ -8,6 +8,7 @@ import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.core.view.isGone import androidx.core.view.isVisible +import com.lagradost.cloudstream3.mvvm.Some import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.UIHelper.setImage @@ -152,3 +153,11 @@ fun TextView?.setTextHtml(text: UiText?) { this.text = str.html() } } + +fun TextView?.setTextHtml(text: Some) { + setTextHtml(if(text is Some.Success) text.value else null) +} + +fun TextView?.setText(text: Some) { + setText(if(text is Some.Success) text.value else null) +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt index 5b7223d3..b543fb66 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt @@ -179,21 +179,21 @@ object AppUtils { @WorkerThread fun Context.addProgramsToContinueWatching(data: List) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return - + val context = this ioSafe { data.forEach { episodeInfo -> try { - val (program, id) = getWatchNextProgramByVideoId(episodeInfo.url, this) - val nextProgram = buildWatchNextProgramUri(this, episodeInfo) + val (program, id) = getWatchNextProgramByVideoId(episodeInfo.url, context) + val nextProgram = buildWatchNextProgramUri(context, episodeInfo) // If the program is already in the Watch Next row, update it if (program != null && id != null) { - PreviewChannelHelper(this).updateWatchNextProgram( + PreviewChannelHelper(context).updateWatchNextProgram( nextProgram, id, ) } else { - PreviewChannelHelper(this) + PreviewChannelHelper(context) .publishWatchNextProgram(nextProgram) } } catch (e: Exception) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/Coroutines.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/Coroutines.kt index d5cb06f7..978b2720 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/Coroutines.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/Coroutines.kt @@ -12,7 +12,7 @@ object Coroutines { } } - fun ioSafe(work: suspend (() -> Unit)): Job { + fun ioSafe(work: suspend (CoroutineScope.() -> Unit)): Job { return CoroutineScope(Dispatchers.IO).launch { try { work() @@ -22,7 +22,7 @@ object Coroutines { } } - suspend fun ioWork(work: suspend (() -> T)): T { + suspend fun ioWork(work: suspend (CoroutineScope.() -> T)): T { return withContext(Dispatchers.IO) { work() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt index 814cf95b..b228fe77 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt @@ -12,6 +12,9 @@ import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSet import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes import com.lagradost.cloudstream3.utils.UIHelper.setImage +import kotlinx.android.synthetic.main.add_account_input.* +import kotlinx.android.synthetic.main.add_account_input.text1 +import kotlinx.android.synthetic.main.bottom_selection_dialog_direct.* object SingleSelectionHelper { fun Activity?.showOptionSelectStringRes( @@ -21,7 +24,7 @@ object SingleSelectionHelper { tvOptions: List = listOf(), callback: (Pair) -> Unit ) { - if(this == null) return + if (this == null) return this.showOptionSelect( view, @@ -39,7 +42,7 @@ object SingleSelectionHelper { tvOptions: List, callback: (Pair) -> Unit ) { - if(this == null) return + if (this == null) return if (this.isTvSettings()) { val builder = @@ -86,42 +89,44 @@ object SingleSelectionHelper { showApply: Boolean, isMultiSelect: Boolean, callback: (List) -> Unit, - dismissCallback: () -> Unit + dismissCallback: () -> Unit, + itemLayout: Int = R.layout.sort_bottom_single_choice ) { - if(this == null) return + if (this == null) return val realShowApply = showApply || isMultiSelect - val listView = dialog.findViewById(R.id.listview1)!! - val textView = dialog.findViewById(R.id.text1)!! - val applyButton = dialog.findViewById(R.id.apply_btt)!! - val cancelButton = dialog.findViewById(R.id.cancel_btt)!! - val applyHolder = dialog.findViewById(R.id.apply_btt_holder)!! + val listView = dialog.listview1//.findViewById(R.id.listview1)!! + val textView = dialog.text1//.findViewById(R.id.text1)!! + val applyButton = dialog.apply_btt//.findViewById(R.id.apply_btt) + val cancelButton = dialog.cancel_btt//findViewById(R.id.cancel_btt) + val applyHolder = dialog.apply_btt_holder//.findViewById(R.id.apply_btt_holder) - applyHolder.isVisible = realShowApply + applyHolder?.isVisible = realShowApply if (!realShowApply) { val params = listView.layoutParams as LinearLayout.LayoutParams params.setMargins(listView.marginLeft, listView.marginTop, listView.marginRight, 0) listView.layoutParams = params } - textView.text = name + textView?.text = name + textView?.isGone = name.isBlank() - val arrayAdapter = ArrayAdapter(this, R.layout.sort_bottom_single_choice) + val arrayAdapter = ArrayAdapter(this, itemLayout) arrayAdapter.addAll(items) - listView.adapter = arrayAdapter + listView?.adapter = arrayAdapter if (isMultiSelect) { - listView.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE + listView?.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE } else { - listView.choiceMode = AbsListView.CHOICE_MODE_SINGLE + listView?.choiceMode = AbsListView.CHOICE_MODE_SINGLE } for (select in selectedIndex) { - listView.setItemChecked(select, true) + listView?.setItemChecked(select, true) } selectedIndex.minOrNull()?.let { - listView.setSelection(it) + listView?.setSelection(it) } // var lastSelectedIndex = if(selectedIndex.isNotEmpty()) selectedIndex.first() else -1 @@ -130,7 +135,7 @@ object SingleSelectionHelper { dismissCallback.invoke() } - listView.setOnItemClickListener { _, _, which, _ -> + listView?.setOnItemClickListener { _, _, which, _ -> // lastSelectedIndex = which if (realShowApply) { if (!isMultiSelect) { @@ -142,7 +147,7 @@ object SingleSelectionHelper { } } if (realShowApply) { - applyButton.setOnClickListener { + applyButton?.setOnClickListener { val list = ArrayList() for (index in 0 until listView.count) { if (listView.checkedItemPositions[index]) @@ -151,7 +156,7 @@ object SingleSelectionHelper { callback.invoke(list) dialog.dismissSafe(this) } - cancelButton.setOnClickListener { + cancelButton?.setOnClickListener { dialog.dismissSafe(this) } } @@ -166,7 +171,7 @@ object SingleSelectionHelper { callback: (String) -> Unit, dismissCallback: () -> Unit ) { - if(this == null) return + if (this == null) return val inputView = dialog.findViewById(R.id.nginx_text_input)!! val textView = dialog.findViewById(R.id.text1)!! @@ -205,7 +210,7 @@ object SingleSelectionHelper { dismissCallback: () -> Unit, callback: (List) -> Unit, ) { - if(this == null) return + if (this == null) return val builder = AlertDialog.Builder(this, R.style.AlertDialogCustom) @@ -224,7 +229,7 @@ object SingleSelectionHelper { dismissCallback: () -> Unit, callback: (Int) -> Unit, ) { - if(this == null) return + if (this == null) return val builder = AlertDialog.Builder(this, R.style.AlertDialogCustom) @@ -271,6 +276,31 @@ object SingleSelectionHelper { ) } + fun Activity.showBottomDialogInstant( + items: List, + name: String, + dismissCallback: () -> Unit, + callback: (Int) -> Unit, + ): BottomSheetDialog { + val builder = + BottomSheetDialog(this) + builder.setContentView(R.layout.bottom_selection_dialog_direct) + + builder.show() + showDialog( + builder, + items, + listOf(), + name, + showApply = false, + isMultiSelect = false, + callback = { if (it.isNotEmpty()) callback.invoke(it.first()) }, + dismissCallback = dismissCallback, + itemLayout = R.layout.sort_bottom_single_choice_no_checkmark + ) + return builder + } + fun Activity.showNginxTextInputDialog( name: String, value: String, diff --git a/app/src/main/res/layout/bottom_loading.xml b/app/src/main/res/layout/bottom_loading.xml new file mode 100644 index 00000000..6f9def0b --- /dev/null +++ b/app/src/main/res/layout/bottom_loading.xml @@ -0,0 +1,33 @@ + + + + + + + + diff --git a/app/src/main/res/layout/bottom_selection_dialog_direct.xml b/app/src/main/res/layout/bottom_selection_dialog_direct.xml new file mode 100644 index 00000000..0d179ebb --- /dev/null +++ b/app/src/main/res/layout/bottom_selection_dialog_direct.xml @@ -0,0 +1,34 @@ + + + + + + + diff --git a/app/src/main/res/layout/fragment_result.xml b/app/src/main/res/layout/fragment_result.xml index 11513553..c9c218c2 100644 --- a/app/src/main/res/layout/fragment_result.xml +++ b/app/src/main/res/layout/fragment_result.xml @@ -843,7 +843,6 @@ diff --git a/app/src/main/res/layout/result_poster.xml b/app/src/main/res/layout/result_poster.xml index 1345d279..0675da7e 100644 --- a/app/src/main/res/layout/result_poster.xml +++ b/app/src/main/res/layout/result_poster.xml @@ -12,6 +12,6 @@ android:scaleType="fitCenter" android:adjustViewBounds="true" android:src="@drawable/default_cover" - android:background="#fffff0" + android:background="?attr/primaryGrayBackground" android:contentDescription="@string/poster_image" /> \ No newline at end of file diff --git a/app/src/main/res/layout/sort_bottom_single_choice_no_checkmark.xml b/app/src/main/res/layout/sort_bottom_single_choice_no_checkmark.xml new file mode 100644 index 00000000..0938ad1f --- /dev/null +++ b/app/src/main/res/layout/sort_bottom_single_choice_no_checkmark.xml @@ -0,0 +1,22 @@ + + + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 3f2cc6c0..9da8d721 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -448,7 +448,15 @@ ?attr/textColor - + + + + @@ -334,6 +335,9 @@ scrollable--> + + - - + +