mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
compiling, not done 3
This commit is contained in:
parent
f57b12d89c
commit
64ea5e2f4b
11 changed files with 782 additions and 1372 deletions
|
@ -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<String>?) {
|
||||
this.actors = actors?.map { ActorData(Actor(it)) }
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -84,7 +84,7 @@ class DownloadChildFragment : Fragment() {
|
|||
DownloadChildAdapter(
|
||||
ArrayList(),
|
||||
) { click ->
|
||||
handleDownloadClick(activity, name, click)
|
||||
handleDownloadClick(activity, click)
|
||||
}
|
||||
|
||||
downloadDeleteEventListener = { id: Int ->
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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<ResultEpisode>,
|
||||
private var cardList: MutableList<ResultEpisode>,
|
||||
private val hasDownloadSupport: Boolean,
|
||||
private val clickCallback: (EpisodeClickEvent) -> Unit,
|
||||
private val downloadClickCallback: (DownloadClickEvent) -> Unit,
|
||||
|
@ -92,6 +93,17 @@ class EpisodeAdapter(
|
|||
}
|
||||
}
|
||||
|
||||
fun updateList(newList: List<ResultEpisode>) {
|
||||
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<ResultEpisode>,
|
||||
private val newList: List<ResultEpisode>
|
||||
) :
|
||||
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]
|
||||
}
|
||||
|
|
|
@ -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<ExtractorLink>,
|
||||
subs: List<SubtitleData>?
|
||||
) {
|
||||
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<ExtractorLink>()
|
||||
val currentSubs = mutableSetOf<SubtitleData>()
|
||||
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<String>? = null
|
||||
private var dubRange: Set<DubStatus>? = 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<ExtractorLink>? = null
|
||||
var currentSubs: Set<SubtitleData>? = 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<ExtractorLink>,
|
||||
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<SubtitleData>,
|
||||
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<String>()
|
||||
val verifiedOptionsValues = ArrayList<Int>()
|
||||
|
||||
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 =
|
||||
|
|
|
@ -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<Resource<LoadResponse>> = MutableLiveData()
|
||||
private val _episodes: MutableLiveData<List<ResultEpisode>> = MutableLiveData()
|
||||
private val episodeById: MutableLiveData<HashMap<Int, Int>> =
|
||||
MutableLiveData() // lookup by ID to get Index
|
||||
|
||||
private val _publicEpisodes: MutableLiveData<Resource<List<ResultEpisode>>> = MutableLiveData()
|
||||
private val _publicEpisodesCount: MutableLiveData<Int> = MutableLiveData() // before the sorting
|
||||
private val _rangeOptions: MutableLiveData<List<String>> = MutableLiveData()
|
||||
val selectedRange: MutableLiveData<String> = MutableLiveData()
|
||||
private val selectedRangeInt: MutableLiveData<Int> = MutableLiveData()
|
||||
val rangeOptions: LiveData<List<String>> = _rangeOptions
|
||||
|
||||
val result: LiveData<Resource<LoadResponse>> get() = _resultResponse
|
||||
|
||||
val episodes: LiveData<List<ResultEpisode>> get() = _episodes
|
||||
val publicEpisodes: LiveData<Resource<List<ResultEpisode>>> get() = _publicEpisodes
|
||||
val publicEpisodesCount: LiveData<Int> get() = _publicEpisodesCount
|
||||
|
||||
val dubStatus: LiveData<DubStatus> get() = _dubStatus
|
||||
private val _dubStatus: MutableLiveData<DubStatus> = MutableLiveData()
|
||||
|
||||
val id: MutableLiveData<Int> = MutableLiveData()
|
||||
val selectedSeason: MutableLiveData<Int> = MutableLiveData(-2)
|
||||
val seasonSelections: MutableLiveData<List<Pair<String?, Int?>>> = MutableLiveData()
|
||||
|
||||
val dubSubSelections: LiveData<Set<DubStatus>> get() = _dubSubSelections
|
||||
private val _dubSubSelections: MutableLiveData<Set<DubStatus>> = MutableLiveData()
|
||||
|
||||
val dubSubEpisodes: LiveData<Map<DubStatus, List<ResultEpisode>>?> get() = _dubSubEpisodes
|
||||
private val _dubSubEpisodes: MutableLiveData<Map<DubStatus, List<ResultEpisode>>?> =
|
||||
MutableLiveData()
|
||||
|
||||
private val _watchStatus: MutableLiveData<WatchType> = MutableLiveData()
|
||||
val watchStatus: LiveData<WatchType> 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<String, String>? = null
|
||||
|
||||
private suspend fun applyMeta(
|
||||
resp: LoadResponse,
|
||||
meta: SyncAPI.SyncResult?,
|
||||
syncs: Map<String, String>? = null
|
||||
): Pair<LoadResponse, Boolean> {
|
||||
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<SearchResponse>()
|
||||
val apiNames = listOf(GogoanimeProvider().name, NineAnimeProvider().name)
|
||||
meta.recommendations?.forEach { rec ->
|
||||
apiNames.forEach { name ->
|
||||
realRecommendations.add(rec.copy(apiName = name))
|
||||
}
|
||||
}
|
||||
|
||||
recommendations = recommendations?.union(realRecommendations)?.toList()
|
||||
?: realRecommendations
|
||||
|
||||
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<String, String>?) =
|
||||
viewModelScope.launch {
|
||||
Log.i(TAG, "setMeta")
|
||||
lastMeta = meta
|
||||
lastSync = syncs
|
||||
val (value, updateEpisodes) = ioWork {
|
||||
(result.value as? Resource.Success<LoadResponse>?)?.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<ResultEpisode>?, selection: Int?, range: Int?) {
|
||||
if (list == null) return
|
||||
val seasonTypes = HashMap<Int?, Boolean>()
|
||||
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<String>()
|
||||
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<Pair<Set<ExtractorLink>, Set<SubtitleData>>> {
|
||||
return safeApiCall {
|
||||
val index = _episodes.value?.indexOf(episode) ?: episode.index
|
||||
|
||||
val currentLinks = mutableSetOf<ExtractorLink>()
|
||||
val currentSubs = mutableSetOf<SubtitleData>()
|
||||
|
||||
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<ResultEpisode>, selection: Int?) {
|
||||
_episodes.postValue(list)
|
||||
generator = RepoLinkGenerator(list)
|
||||
|
||||
val set = HashMap<Int, Int>()
|
||||
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<Int>()
|
||||
val res = loadResponse.episodes.map { ep ->
|
||||
val episodes = ArrayList<ResultEpisode>()
|
||||
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<ResultEpisode>()
|
||||
val existingEpisodes = HashSet<Int>()
|
||||
for ((index, episode) in loadResponse.episodes.sortedBy {
|
||||
(it.season?.times(10000) ?: 0) + (it.episode ?: 0)
|
||||
}.withIndex()) {
|
||||
val episodeIndex = episode.episode ?: (index + 1)
|
||||
val id =
|
||||
mainId + (episode.season?.times(100000) ?: 0) + episodeIndex + 1
|
||||
if (!existingEpisodes.contains(id)) {
|
||||
existingEpisodes.add(id)
|
||||
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<String> = MutableLiveData()
|
||||
val apiName: LiveData<String> get() = _apiName
|
||||
|
||||
}
|
|
@ -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<String, String>,
|
||||
|
||||
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<ExtractorLink>,
|
||||
val subs: List<SubtitleData>,
|
||||
)
|
||||
|
||||
sealed class SelectPopup {
|
||||
data class SelectText(
|
||||
val text: UiText,
|
||||
val options: List<UiText>,
|
||||
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<String> {
|
||||
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<Resource<ResultData>> =
|
||||
MutableLiveData(Resource.Loading())
|
||||
|
@ -236,10 +310,12 @@ class ResultViewModel2 : ViewModel() {
|
|||
MutableLiveData(emptyList())
|
||||
val dubSubSelections: LiveData<List<Pair<UiText?, DubStatus>>> = _dubSubSelections
|
||||
|
||||
private val _rangeSelections: MutableLiveData<List<Pair<UiText?, EpisodeRange>>> = MutableLiveData(emptyList())
|
||||
private val _rangeSelections: MutableLiveData<List<Pair<UiText?, EpisodeRange>>> =
|
||||
MutableLiveData(emptyList())
|
||||
val rangeSelections: LiveData<List<Pair<UiText?, EpisodeRange>>> = _rangeSelections
|
||||
|
||||
private val _seasonSelections: MutableLiveData<List<Pair<UiText?, Int>>> = MutableLiveData(emptyList())
|
||||
private val _seasonSelections: MutableLiveData<List<Pair<UiText?, Int>>> =
|
||||
MutableLiveData(emptyList())
|
||||
val seasonSelections: LiveData<List<Pair<UiText?, Int>>> = _seasonSelections
|
||||
|
||||
|
||||
|
@ -258,6 +334,9 @@ class ResultViewModel2 : ViewModel() {
|
|||
private val _selectedDubStatus: MutableLiveData<UiText?> = MutableLiveData(null)
|
||||
val selectedDubStatus: LiveData<UiText?> = _selectedDubStatus
|
||||
|
||||
private val _loadedLinks: MutableLiveData<LinkProgress?> = MutableLiveData(null)
|
||||
val loadedLinks: LiveData<LinkProgress?> = _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<ExtractorLink>,
|
||||
subs: List<SubtitleData>?
|
||||
) {
|
||||
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<ExtractorLink>()
|
||||
val currentSubs = mutableSetOf<SubtitleData>()
|
||||
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<WatchType> = MutableLiveData(WatchType.NONE)
|
||||
val watchStatus: LiveData<WatchType> get() = _watchStatus
|
||||
|
||||
private val _selectPopup: MutableLiveData<SelectPopup?> = MutableLiveData(null)
|
||||
val selectPopup: LiveData<SelectPopup?> 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<ExtractorLink>,
|
||||
subs: List<SubtitleData>,
|
||||
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<LinkLoadingResult, Int>) -> 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<LinkLoadingResult, Int>) -> 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<ExtractorLink> = mutableSetOf()
|
||||
val subs: MutableSet<SubtitleData> = 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<String, String>().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<ResultEpisode> {
|
||||
//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))
|
||||
}
|
||||
|
|
|
@ -44,9 +44,13 @@ class SyncViewModel : ViewModel() {
|
|||
|
||||
// prefix, id
|
||||
private var syncs = mutableMapOf<String, String>()
|
||||
private val _syncIds: MutableLiveData<MutableMap<String, String>> =
|
||||
MutableLiveData(mutableMapOf())
|
||||
val syncIds: LiveData<MutableMap<String, String>> get() = _syncIds
|
||||
//private val _syncIds: MutableLiveData<MutableMap<String, String>> =
|
||||
// MutableLiveData(mutableMapOf())
|
||||
//val syncIds: LiveData<MutableMap<String, String>> get() = _syncIds
|
||||
|
||||
fun getSyncs() : Map<String,String> {
|
||||
return syncs
|
||||
}
|
||||
|
||||
private val _currentSynced: MutableLiveData<List<CurrentSynced>> =
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Any>
|
||||
) : 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?) {
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue