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] 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,