compiling, not done 3

This commit is contained in:
LagradOst 2022-08-02 02:43:42 +02:00
parent f57b12d89c
commit 64ea5e2f4b
11 changed files with 782 additions and 1372 deletions

View file

@ -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)) }

View file

@ -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) {

View file

@ -84,7 +84,7 @@ class DownloadChildFragment : Fragment() {
DownloadChildAdapter(
ArrayList(),
) { click ->
handleDownloadClick(activity, name, click)
handleDownloadClick(activity, click)
}
downloadDeleteEventListener = { id: Int ->

View file

@ -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)

View file

@ -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]
}

View file

@ -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 =

View file

@ -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
}

View file

@ -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))
}

View file

@ -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
}

View file

@ -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?) {

View file

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