diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 24af5c7f..0259ef46 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -198,7 +198,7 @@ object APIHolder { return null } - fun getLoadResponseIdFromUrl(url: String, apiName: String): Int { + private fun getLoadResponseIdFromUrl(url: String, apiName: String): Int { return url.replace(getApiFromName(apiName).mainUrl, "").replace("/", "").hashCode() } @@ -645,6 +645,7 @@ enum class ShowStatus { } enum class DubStatus(val id: Int) { + None(-1), Dubbed(1), Subbed(0), } @@ -979,6 +980,10 @@ interface LoadResponse { private val aniListIdPrefix = aniListApi.idPrefix var isTrailersEnabled = true + fun LoadResponse.isMovie() : Boolean { + return this.type.isMovieType() + } + @JvmName("addActorNames") fun LoadResponse.addActors(actors: List?) { this.actors = actors?.map { ActorData(Actor(it)) } @@ -1119,6 +1124,7 @@ data class NextAiring( data class SeasonData( val season: Int, val name: String? = null, + val displaySeason : Int? = null, // will use season if null ) interface EpisodeResponse { diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 86a0aafe..8719936e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -332,6 +332,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { if (str.contains(appString)) { for (api in OAuth2Apis) { if (str.contains("/${api.redirectUrl}")) { + val activity = this ioSafe { Log.i(TAG, "handleAppIntent $str") val isSuccessful = api.handleRedirect(str) @@ -342,10 +343,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { Log.i(TAG, "failed to authenticate ${api.name}") } - this.runOnUiThread { + activity.runOnUiThread { try { showToast( - this, + activity, getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail).format( api.name ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/animeproviders/AnimeflvProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/animeproviders/AnimeflvnetProvider.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/animeproviders/AnimeflvProvider.kt rename to app/src/main/java/com/lagradost/cloudstream3/animeproviders/AnimeflvnetProvider.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/animeproviders/GogoanimeProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/animeproviders/GogoanimeProvider.kt index b1a9a629..7aaab91d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/animeproviders/GogoanimeProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/animeproviders/GogoanimeProvider.kt @@ -54,7 +54,7 @@ class GogoanimeProvider : MainAPI() { secretKeyString: String, encrypt: Boolean = true ): String { - println("IV: $iv, Key: $secretKeyString, encrypt: $encrypt, Message: $string") + //println("IV: $iv, Key: $secretKeyString, encrypt: $encrypt, Message: $string") val ivParameterSpec = IvParameterSpec(iv.toByteArray()) val secretKey = SecretKeySpec(secretKeyString.toByteArray(), "AES") val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") diff --git a/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt b/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt index 2a8c49f7..c831884f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt @@ -51,6 +51,32 @@ fun LifecycleOwner.observeDirectly(liveData: LiveData, action: (t: T) -> action(currentValue) } +inline fun some(value: T?): Some { + return if (value == null) { + Some.None + } else { + Some.Success(value) + } +} + +sealed class Some { + data class Success(val value: T) : Some() + object None : Some() + + override fun toString(): String { + return when(this) { + is None -> "None" + is Success -> "Some(${value.toString()})" + } + } +} + +sealed class ResourceSome { + data class Success(val value: T) : ResourceSome() + object None : ResourceSome() + data class Loading(val data: Any? = null) : ResourceSome() +} + sealed class Resource { data class Success(val value: T) : Resource() data class Failure( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt index 0283760d..34cb262c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt @@ -31,6 +31,8 @@ class APIRepository(val api: MainAPI) { val mainUrl = api.mainUrl val mainPage = api.mainPage val hasQuickSearch = api.hasQuickSearch + val vpnStatus = api.vpnStatus + val providerType = api.providerType suspend fun load(url: String): Resource { return safeApiCall { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt index 1ab9beb2..0069be3a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt @@ -18,7 +18,7 @@ import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager object DownloadButtonSetup { - fun handleDownloadClick(activity: Activity?, headerName: String?, click: DownloadClickEvent) { + fun handleDownloadClick(activity: Activity?, click: DownloadClickEvent) { val id = click.data.id if (click.data !is VideoDownloadHelper.DownloadEpisodeCached) return when (click.action) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt index e16d2d9e..477a18e0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt @@ -84,7 +84,7 @@ class DownloadChildFragment : Fragment() { DownloadChildAdapter( ArrayList(), ) { click -> - handleDownloadClick(activity, name, click) + handleDownloadClick(activity, click) } downloadDeleteEventListener = { id: Int -> diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt index 78a5d484..f6265705 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt @@ -153,7 +153,7 @@ class DownloadFragment : Fragment() { }, { downloadClickEvent -> if (downloadClickEvent.data !is VideoDownloadHelper.DownloadEpisodeCached) return@DownloadHeaderAdapter - handleDownloadClick(activity, downloadClickEvent.data.name, downloadClickEvent) + handleDownloadClick(activity, downloadClickEvent) if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) { context?.let { ctx -> downloadsViewModel.updateList(ctx) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index 1973d7bd..d0b03774 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -125,6 +125,7 @@ class GeneratorPlayer : FullScreenPlayer() { private fun loadExtractorJob(extractorLink: ExtractorLink?) { currentVerifyLink?.cancel() + extractorLink?.let { currentVerifyLink = ioSafe { if (it.extractorData != null) { @@ -488,7 +489,9 @@ class GeneratorPlayer : FullScreenPlayer() { .setView(R.layout.player_select_source_and_subs) val sourceDialog = sourceBuilder.create() + selectSourceDialog = sourceDialog + sourceDialog.show() val providerList = sourceDialog.sort_providers val subtitleList = sourceDialog.sort_subtitles diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt index 69d2e07a..6d6586c0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt @@ -10,6 +10,7 @@ import androidx.annotation.LayoutRes import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.widget.ContentLoadingProgressBar +import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.google.android.material.button.MaterialButton import com.lagradost.cloudstream3.R @@ -56,7 +57,7 @@ const val ACTION_DOWNLOAD_EPISODE_SUBTITLE_MIRROR = 14 data class EpisodeClickEvent(val action: Int, val data: ResultEpisode) class EpisodeAdapter( - var cardList: List, + private var cardList: MutableList, private val hasDownloadSupport: Boolean, private val clickCallback: (EpisodeClickEvent) -> Unit, private val downloadClickCallback: (DownloadClickEvent) -> Unit, @@ -92,13 +93,15 @@ class EpisodeAdapter( } } - @LayoutRes - private var layout: Int = 0 - fun updateLayout() { - // layout = - // if (cardList.filter { it.poster != null }.size >= cardList.size / 2f) // If over half has posters then use the large layout - // R.layout.result_episode_large - // else R.layout.result_episode + fun updateList(newList: List) { + val diffResult = DiffUtil.calculateDiff( + ResultDiffCallback(this.cardList, newList) + ) + + cardList.clear() + cardList.addAll(newList) + + diffResult.dispatchUpdatesTo(this) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { @@ -263,3 +266,19 @@ class EpisodeAdapter( } } } + +class ResultDiffCallback( + private val oldList: List, + private val newList: List +) : + DiffUtil.Callback() { + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = + oldList[oldItemPosition].id == newList[newItemPosition].id + + override fun getOldListSize() = oldList.size + + override fun getNewListSize() = newList.size + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = + oldList[oldItemPosition] == newList[newItemPosition] +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt index e5b13abf..5ce63a8a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt @@ -1,11 +1,7 @@ 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.app.Dialog import android.content.Intent import android.content.Intent.* import android.content.res.ColorStateList @@ -17,13 +13,12 @@ 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 androidx.annotation.StringRes +import android.widget.AbsListView +import android.widget.ArrayAdapter +import android.widget.ImageView +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 @@ -35,83 +30,62 @@ import com.discord.panels.PanelsChildGestureRegionObserver import com.google.android.gms.cast.framework.CastButtonFactory import com.google.android.gms.cast.framework.CastContext import com.google.android.gms.cast.framework.CastState +import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.button.MaterialButton import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings import com.lagradost.cloudstream3.APIHolder.getApiFromName -import com.lagradost.cloudstream3.APIHolder.getId -import com.lagradost.cloudstream3.APIHolder.unixTime import com.lagradost.cloudstream3.APIHolder.updateHasTrailers -import com.lagradost.cloudstream3.AcraApplication.Companion.setKey -import com.lagradost.cloudstream3.CommonActivity.getCastSession import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.mvvm.* 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.DOWNLOAD_ACTION_DOWNLOAD import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick +import com.lagradost.cloudstream3.ui.download.EasyDownloadButton 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.getNameFull 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.ioWork 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.getViewPos import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog -import com.lagradost.cloudstream3.utils.UIHelper.checkWrite +import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant 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.UIHelper.setImage -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.runBlocking -import kotlinx.coroutines.withContext -import java.io.File -import java.util.concurrent.TimeUnit -const val START_ACTION_NORMAL = 0 const val START_ACTION_RESUME_LATEST = 1 const val START_ACTION_LOAD_EP = 2 -const val START_VALUE_NORMAL = 0 - data class ResultEpisode( val headerName: String, val name: String?, val poster: String?, val episode: Int, - val season: Int?, + val seasonIndex: Int?, // this is the "season" index used season names + val season: Int?, // this is the display val data: String, val apiName: String, val id: Int, @@ -146,6 +120,7 @@ fun buildResultEpisode( name: String? = null, poster: String? = null, episode: Int, + seasonIndex: Int? = null, season: Int? = null, data: String, apiName: String, @@ -163,6 +138,7 @@ fun buildResultEpisode( name, poster, episode, + seasonIndex, season, data, apiName, @@ -199,7 +175,6 @@ class ResultFragment : ResultTrailerPlayer() { putString(URL_BUNDLE, card.url) putString(API_NAME_BUNDLE, card.apiName) if (card is DataStoreHelper.ResumeWatchingResult) { -// println("CARD::::: $card") if (card.season != null) putInt(SEASON_BUNDLE, card.season) if (card.episode != null) @@ -208,6 +183,8 @@ class ResultFragment : ResultTrailerPlayer() { putInt(START_ACTION_BUNDLE, startAction) if (startValue != null) putInt(START_VALUE_BUNDLE, startValue) + + putBoolean(RESTART_BUNDLE, true) } } @@ -232,234 +209,10 @@ class ResultFragment : ResultTrailerPlayer() { } private var updateUIListener: (() -> Unit)? = null - - private fun downloadSubtitle( - context: Context?, - link: SubtitleData, - meta: VideoDownloadManager.DownloadEpisodeMetadata, - ) { - context?.let { ctx -> - val fileName = getFileName(ctx, meta) - val folder = getFolder(meta.type ?: return, meta.mainName) - downloadSubtitle( - ctx, - ExtractorSubtitleLink(link.name, link.url, ""), - fileName, - folder - ) - } - } - - private fun downloadSubtitle( - context: Context?, - link: ExtractorSubtitleLink, - fileName: String, - folder: String - ) { - ioSafe { - VideoDownloadManager.downloadThing( - context ?: return@ioSafe, - link, - "$fileName ${link.name}", - folder, - if (link.url.contains(".srt")) ".srt" else "vtt", - false, - null - ) { - // no notification - } - } - } - - private fun getMeta( - episode: ResultEpisode, - titleName: String, - apiName: String, - currentPoster: String, - currentIsMovie: Boolean, - tvType: TvType, - ): VideoDownloadManager.DownloadEpisodeMetadata { - return VideoDownloadManager.DownloadEpisodeMetadata( - episode.id, - sanitizeFilename(titleName), - apiName, - episode.poster ?: currentPoster, - episode.name, - if (currentIsMovie) null else episode.season, - if (currentIsMovie) null else episode.episode, - tvType, - ) - } - - private fun getFolder(currentType: TvType, titleName: String): String { - val sanitizedFileName = sanitizeFilename(titleName) - return when (currentType) { - TvType.Anime -> "Anime/$sanitizedFileName" - TvType.Movie -> "Movies" - TvType.AnimeMovie -> "Movies" - TvType.TvSeries -> "TVSeries/$sanitizedFileName" - TvType.OVA -> "OVA" - TvType.Cartoon -> "Cartoons/$sanitizedFileName" - TvType.Torrent -> "Torrent" - TvType.Documentary -> "Documentaries" - TvType.AsianDrama -> "AsianDrama" - TvType.Live -> "LiveStreams" - } - } - - fun startDownload( - context: Context?, - episode: ResultEpisode, - currentIsMovie: Boolean, - currentHeaderName: String, - currentType: TvType, - currentPoster: String, - apiName: String, - parentId: Int, - url: String, - links: List, - subs: List? - ) { - try { - if (context == null) return - - val meta = - getMeta( - episode, - currentHeaderName, - apiName, - currentPoster, - currentIsMovie, - currentType - ) - - val folder = getFolder(currentType, currentHeaderName) - - val src = "$DOWNLOAD_NAVIGATE_TO/$parentId" // url ?: return@let - - // SET VISUAL KEYS - setKey( - DOWNLOAD_HEADER_CACHE, - parentId.toString(), - VideoDownloadHelper.DownloadHeaderCached( - apiName, - url, - currentType, - currentHeaderName, - currentPoster, - parentId, - System.currentTimeMillis(), - ) - ) - - setKey( - getFolderName( - DOWNLOAD_EPISODE_CACHE, - parentId.toString() - ), // 3 deep folder for faster acess - episode.id.toString(), - VideoDownloadHelper.DownloadEpisodeCached( - episode.name, - episode.poster, - episode.episode, - episode.season, - episode.id, - parentId, - episode.rating, - episode.description, - System.currentTimeMillis(), - ) - ) - - // DOWNLOAD VIDEO - VideoDownloadManager.downloadEpisodeUsingWorker( - context, - src,//url ?: return, - folder, - meta, - links - ) - - // 1. Checks if the lang should be downloaded - // 2. Makes it into the download format - // 3. Downloads it as a .vtt file - val downloadList = getDownloadSubsLanguageISO639_1() - subs?.let { subsList -> - subsList.filter { - downloadList.contains( - SubtitleHelper.fromLanguageToTwoLetters( - it.name, - true - ) - ) - } - .map { ExtractorSubtitleLink(it.name, it.url, "") } - .forEach { link -> - val fileName = getFileName(context, meta) - downloadSubtitle(context, link, fileName, folder) - } - } - } catch (e: Exception) { - logError(e) - } - } - - suspend fun downloadEpisode( - activity: Activity?, - episode: ResultEpisode, - currentIsMovie: Boolean, - currentHeaderName: String, - currentType: TvType, - currentPoster: String, - apiName: String, - parentId: Int, - url: String, - ) { - safeApiCall { - val generator = RepoLinkGenerator(listOf(episode)) - val currentLinks = mutableSetOf() - val currentSubs = mutableSetOf() - generator.generateLinks(clearCache = false, isCasting = false, callback = { - it.first?.let { link -> - currentLinks.add(link) - } - }, subtitleCallback = { sub -> - currentSubs.add(sub) - }) - - if (currentLinks.isEmpty()) { - main { - showToast(activity, R.string.no_links_found_toast, Toast.LENGTH_SHORT) - } - return@safeApiCall - } - - startDownload( - activity, - episode, - currentIsMovie, - currentHeaderName, - currentType, - currentPoster, - apiName, - parentId, - url, - sortUrls(currentLinks), - sortSubs(currentSubs), - ) - } - } } - private var currentLoadingCount = - 0 // THIS IS USED TO PREVENT LATE EVENTS, AFTER DISMISS WAS CLICKED - private lateinit var viewModel: ResultViewModel //by activityViewModels() + private lateinit var viewModel: ResultViewModel2 //by activityViewModels() private lateinit var syncModel: SyncViewModel - private var currentHeaderName: String? = null - private var currentType: TvType? = null - private var currentEpisodes: List? = null - private var downloadButton: EasyDownloadButton? = null - private var syncdata: Map? = null override fun onCreateView( inflater: LayoutInflater, @@ -467,13 +220,14 @@ class ResultFragment : ResultTrailerPlayer() { savedInstanceState: Bundle?, ): View? { viewModel = - ViewModelProvider(this)[ResultViewModel::class.java] + ViewModelProvider(this)[ResultViewModel2::class.java] syncModel = ViewModelProvider(this)[SyncViewModel::class.java] return inflater.inflate(R.layout.fragment_result_swipe, container, false) } + private var downloadButton: EasyDownloadButton? = null override fun onDestroyView() { updateUIListener = null (result_episodes?.adapter as EpisodeAdapter?)?.killAdapter() @@ -487,13 +241,6 @@ class ResultFragment : ResultTrailerPlayer() { super.onDestroyView() } - override fun onDestroy() { - //requireActivity().viewModelStore.clear() // REMEMBER THE CLEAR - - - super.onDestroy() - } - override fun onResume() { super.onResume() activity?.let { @@ -516,7 +263,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 @@ -542,70 +289,6 @@ class ResultFragment : ResultTrailerPlayer() { } } - private var currentPoster: String? = null - private var currentId: Int? = null - private var currentIsMovie: Boolean? = null - private var episodeRanges: List? = null - private var dubRange: Set? = null - var url: String? = null - - private fun fromIndexToSeasonText(selection: Int?): String { - return when (selection) { - null -> getString(R.string.no_season) - -2 -> getString(R.string.no_season) - else -> "${getString(R.string.season)} $selection" - } - } - - var startAction: Int? = null - private var startValue: Int? = null - - private fun setFormatText(textView: TextView?, @StringRes format: Int, arg: Any?) { - // java.util.IllegalFormatConversionException: f != java.lang.Integer - // This can fail with malformed formatting - normalSafeApiCall { - if (arg == null) { - textView?.isVisible = false - } else { - val text = context?.getString(format)?.format(arg) - if (text == null) { - textView?.isVisible = false - } else { - textView?.isVisible = true - textView?.text = text - } - } - } - } - - private fun setDuration(duration: Int?) { - setFormatText(result_meta_duration, R.string.duration_format, duration) - } - - private fun setShow(showStatus: ShowStatus?) { - val status = when (showStatus) { - null -> null - ShowStatus.Ongoing -> R.string.status_ongoing - ShowStatus.Completed -> R.string.status_completed - } - - if (status == null) { - result_meta_status?.isVisible = false - } else { - context?.getString(status)?.let { - result_meta_status?.text = it - } - } - } - - private fun setYear(year: Int?) { - setFormatText(result_meta_year, R.string.year_format, year) - } - - private fun setRating(rating: Int?) { - setFormatText(result_meta_rating, R.string.rating_format, rating?.div(1000f)) - } - var currentTrailers: List = emptyList() var currentTrailerIndex = 0 @@ -626,42 +309,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 -> @@ -703,77 +350,6 @@ class ResultFragment : ResultTrailerPlayer() { loadTrailer() } - private fun setNextEpisode(nextAiring: NextAiring?) { - result_next_airing_holder?.isVisible = - if (nextAiring == null || nextAiring.episode <= 0 || nextAiring.unixTime <= unixTime) { - false - } else { - val seconds = nextAiring.unixTime - unixTime - val days = TimeUnit.SECONDS.toDays(seconds) - val hours: Long = TimeUnit.SECONDS.toHours(seconds) - days * 24 - val minute = - TimeUnit.SECONDS.toMinutes(seconds) - TimeUnit.SECONDS.toHours(seconds) * 60 - // val second = - // TimeUnit.SECONDS.toSeconds(seconds) - TimeUnit.SECONDS.toMinutes(seconds) * 60 - try { - val ctx = context - if (ctx == null) { - false - } else { - when { - days > 0 -> { - ctx.getString(R.string.next_episode_time_day_format).format( - days, - hours, - minute - ) - } - hours > 0 -> ctx.getString(R.string.next_episode_time_hour_format) - .format( - hours, - minute - ) - minute > 0 -> ctx.getString(R.string.next_episode_time_min_format) - .format( - minute - ) - else -> null - }?.also { text -> - result_next_airing_time?.text = text - result_next_airing?.text = - ctx.getString(R.string.next_episode_format) - .format(nextAiring.episode) - } != null - } - } catch (e: Exception) { // mistranslation - result_next_airing_holder?.isVisible = false - logError(e) - false - } - } - } - - private fun setActors(actors: List?) { - if (actors.isNullOrEmpty()) { - result_cast_text?.isVisible = false - result_cast_items?.isVisible = false - } else { - val isImage = actors.first().actor.image != null - if (isImage) { - (result_cast_items?.adapter as ActorAdaptor?)?.apply { - updateList(actors) - } - result_cast_text?.isVisible = false - result_cast_items?.isVisible = true - } else { - result_cast_text?.isVisible = true - result_cast_items?.isVisible = false - setFormatText(result_cast_text, R.string.cast_format, - actors.joinToString { it.actor.name }) - } - } - } - private fun setRecommendations(rec: List?, validApiName: String?) { val isInvalid = rec.isNullOrEmpty() result_recommendations?.isGone = isInvalid @@ -830,420 +406,13 @@ class ResultFragment : ResultTrailerPlayer() { fixGrid() } - private fun lateFixDownloadButton(show: Boolean) { - if (!show || currentType?.isMovieType() == false) { - result_movie_parent.visibility = GONE - result_episodes_text.visibility = VISIBLE - result_episodes.visibility = VISIBLE - } else { - result_movie_parent.visibility = VISIBLE - result_episodes_text.visibility = GONE - result_episodes.visibility = GONE - } - } - private fun updateUI() { syncModel.updateUserData() viewModel.reloadEpisodes() } - var apiName: String = "" - private fun handleAction(episodeClick: EpisodeClickEvent): Job = main { - if (episodeClick.action == ACTION_DOWNLOAD_EPISODE) { - val isMovie = currentIsMovie ?: return@main - val headerName = currentHeaderName ?: return@main - val tvType = currentType ?: return@main - val poster = currentPoster ?: return@main - val id = currentId ?: return@main - val curl = url ?: return@main - showToast(activity, R.string.download_started, Toast.LENGTH_SHORT) - downloadEpisode( - activity, - episodeClick.data, - isMovie, - headerName, - tvType, - poster, - apiName, - id, - curl, - ) - return@main - } - - var currentLinks: Set? = null - var currentSubs: Set? = null - - //val id = episodeClick.data.id - currentLoadingCount++ - - val showTitle = - episodeClick.data.name ?: context?.getString(R.string.episode_name_format) - ?.format( - getString(R.string.episode), - episodeClick.data.episode - ) - - - fun acquireSingleExtractorLink( - links: List, - title: String, - callback: (ExtractorLink) -> Unit - ) { - val builder = AlertDialog.Builder(requireContext(), R.style.AlertDialogCustom) - - builder.setTitle(title) - builder.setItems(links.map { "${it.name} ${Qualities.getStringByInt(it.quality)}" } - .toTypedArray()) { dia, which -> - callback.invoke(links[which]) - dia?.dismiss() - } - builder.create().show() - } - - fun acquireSingleSubtitleLink( - links: List, - title: String, - callback: (SubtitleData) -> Unit - ) { - val builder = AlertDialog.Builder(requireContext(), R.style.AlertDialogCustom) - - builder.setTitle(title) - builder.setItems(links.map { it.name }.toTypedArray()) { dia, which -> - callback.invoke(links[which]) - dia?.dismiss() - } - builder.create().show() - } - - fun acquireSingeExtractorLink(title: String, callback: (ExtractorLink) -> Unit) { - acquireSingleExtractorLink(sortUrls(currentLinks ?: return), title, callback) - } - - fun startChromecast(startIndex: Int) { - val eps = currentEpisodes ?: return - activity?.getCastSession()?.startCast( - apiName, - currentIsMovie ?: return, - currentHeaderName, - currentPoster, - episodeClick.data.index, - eps, - sortUrls(currentLinks ?: return), - sortSubs(currentSubs ?: return), - startTime = episodeClick.data.getRealPosition(), - startIndex = startIndex - ) - } - - suspend fun requireLinks(isCasting: Boolean, displayLoading: Boolean = true): Boolean { - val skipLoading = getApiFromName(apiName).instantLinkLoading - - var loadingDialog: AlertDialog? = null - val currentLoad = currentLoadingCount - - if (!skipLoading && displayLoading) { - val builder = - AlertDialog.Builder(requireContext(), R.style.AlertDialogCustomTransparent) - val customLayout = layoutInflater.inflate(R.layout.dialog_loading, null) - builder.setView(customLayout) - - loadingDialog = builder.create() - - loadingDialog.show() - loadingDialog.setOnDismissListener { - currentLoadingCount++ - } - } - - val data = viewModel.loadEpisode(episodeClick.data, isCasting) - if (currentLoadingCount != currentLoad) return false - loadingDialog?.dismissSafe(activity) - - when (data) { - is Resource.Success -> { - currentLinks = data.value.first - currentSubs = data.value.second - return true - } - is Resource.Failure -> { - showToast( - activity, - R.string.error_loading_links_toast, - Toast.LENGTH_SHORT - ) - } - else -> Unit - } - return false - } - - val isLoaded = when (episodeClick.action) { - ACTION_PLAY_EPISODE_IN_PLAYER -> true - ACTION_CLICK_DEFAULT -> true - ACTION_SHOW_TOAST -> true - ACTION_DOWNLOAD_EPISODE -> { - showToast(activity, R.string.download_started, Toast.LENGTH_SHORT) - requireLinks(false, false) - } - ACTION_CHROME_CAST_EPISODE -> requireLinks(true) - ACTION_CHROME_CAST_MIRROR -> requireLinks(true) - ACTION_SHOW_DESCRIPTION -> true - else -> requireLinks(false) - } - if (!isLoaded) return@main // CANT LOAD - - when (episodeClick.action) { - ACTION_SHOW_TOAST -> { - showToast(activity, R.string.play_episode_toast, Toast.LENGTH_SHORT) - } - - ACTION_SHOW_DESCRIPTION -> { - val builder: AlertDialog.Builder = - AlertDialog.Builder(requireContext(), R.style.AlertDialogCustom) - builder.setMessage(episodeClick.data.description ?: return@main) - .setTitle(R.string.torrent_plot) - .show() - } - - ACTION_CLICK_DEFAULT -> { - context?.let { ctx -> - if (ctx.isConnectedToChromecast()) { - handleAction( - EpisodeClickEvent( - ACTION_CHROME_CAST_EPISODE, - episodeClick.data - ) - ) - } else { - handleAction( - EpisodeClickEvent( - ACTION_PLAY_EPISODE_IN_PLAYER, - episodeClick.data - ) - ) - } - } - } - - ACTION_DOWNLOAD_EPISODE_SUBTITLE -> { - acquireSingleSubtitleLink( - sortSubs( - currentSubs ?: return@main - ),//(currentLinks ?: return@main).filter { !it.isM3u8 }, - getString(R.string.episode_action_download_subtitle) - ) { link -> - downloadSubtitle( - context, - link, - getMeta( - episodeClick.data, - currentHeaderName ?: return@acquireSingleSubtitleLink, - apiName, - currentPoster ?: return@acquireSingleSubtitleLink, - currentIsMovie ?: return@acquireSingleSubtitleLink, - currentType ?: return@acquireSingleSubtitleLink - ) - ) - showToast(activity, R.string.download_started, Toast.LENGTH_SHORT) - } - } - - ACTION_SHOW_OPTIONS -> { - context?.let { ctx -> - val builder = AlertDialog.Builder(ctx, R.style.AlertDialogCustom) - var dialog: AlertDialog? = null - builder.setTitle(showTitle) - val options = - requireContext().resources.getStringArray(R.array.episode_long_click_options) - val optionsValues = - requireContext().resources.getIntArray(R.array.episode_long_click_options_values) - - val verifiedOptions = ArrayList() - val verifiedOptionsValues = ArrayList() - - val hasDownloadSupport = getApiFromName(apiName).hasDownloadSupport - - for (i in options.indices) { - val opv = optionsValues[i] - val op = options[i] - - val isConnected = ctx.isConnectedToChromecast() - val add = when (opv) { - ACTION_CHROME_CAST_EPISODE -> isConnected - ACTION_CHROME_CAST_MIRROR -> isConnected - ACTION_DOWNLOAD_EPISODE_SUBTITLE -> !currentSubs.isNullOrEmpty() - ACTION_DOWNLOAD_EPISODE -> hasDownloadSupport - ACTION_DOWNLOAD_MIRROR -> hasDownloadSupport - ACTION_PLAY_EPISODE_IN_VLC_PLAYER -> context?.isAppInstalled( - VLC_PACKAGE - ) ?: false - else -> true - } - if (add) { - verifiedOptions.add(op) - verifiedOptionsValues.add(opv) - } - } - - builder.setItems( - verifiedOptions.toTypedArray() - ) { _, which -> - handleAction( - EpisodeClickEvent( - verifiedOptionsValues[which], - episodeClick.data - ) - ) - dialog?.dismissSafe(activity) - } - - dialog = builder.create() - dialog.show() - } - } - ACTION_COPY_LINK -> { - activity?.let { act -> - try { - acquireSingeExtractorLink(act.getString(R.string.episode_action_copy_link)) { link -> - val serviceClipboard = - (act.getSystemService(CLIPBOARD_SERVICE) as? ClipboardManager?) - ?: return@acquireSingeExtractorLink - val clip = ClipData.newPlainText(link.name, link.url) - serviceClipboard.setPrimaryClip(clip) - showToast(act, R.string.copy_link_toast, Toast.LENGTH_SHORT) - } - } catch (e: Exception) { - showToast(act, e.toString(), Toast.LENGTH_LONG) - logError(e) - } - } - } - - ACTION_PLAY_EPISODE_IN_BROWSER -> { - acquireSingeExtractorLink(getString(R.string.episode_action_play_in_browser)) { link -> - try { - val i = Intent(ACTION_VIEW) - i.data = Uri.parse(link.url) - startActivity(i) - } catch (e: Exception) { - logError(e) - } - } - } - - ACTION_CHROME_CAST_MIRROR -> { - acquireSingeExtractorLink(getString(R.string.episode_action_chromecast_mirror)) { link -> - val mirrorIndex = currentLinks?.indexOf(link) ?: -1 - startChromecast(if (mirrorIndex == -1) 0 else mirrorIndex) - } - } - - ACTION_CHROME_CAST_EPISODE -> { - startChromecast(0) - } - - ACTION_PLAY_EPISODE_IN_VLC_PLAYER -> { - activity?.let { act -> - try { - if (!act.checkWrite()) { - act.requestRW() - if (act.checkWrite()) return@main - } - val data = currentLinks ?: return@main - val subs = currentSubs ?: return@main - - val outputDir = act.cacheDir - val outputFile = withContext(Dispatchers.IO) { - File.createTempFile("mirrorlist", ".m3u8", outputDir) - } - var text = "#EXTM3U" - for (sub in sortSubs(subs)) { - text += "\n#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"${sub.name}\",DEFAULT=NO,AUTOSELECT=NO,FORCED=NO,LANGUAGE=\"${sub.name}\",URI=\"${sub.url}\"" - } - for (link in data.sortedBy { -it.quality }) { - text += "\n#EXTINF:, ${link.name}\n${link.url}" - } - outputFile.writeText(text) - - val vlcIntent = Intent(VLC_INTENT_ACTION_RESULT) - - vlcIntent.setPackage(VLC_PACKAGE) - vlcIntent.addFlags(FLAG_GRANT_PERSISTABLE_URI_PERMISSION) - vlcIntent.addFlags(FLAG_GRANT_PREFIX_URI_PERMISSION) - vlcIntent.addFlags(FLAG_GRANT_READ_URI_PERMISSION) - vlcIntent.addFlags(FLAG_GRANT_WRITE_URI_PERMISSION) - - vlcIntent.setDataAndType( - FileProvider.getUriForFile( - act, - act.applicationContext.packageName + ".provider", - outputFile - ), "video/*" - ) - - val startId = VLC_FROM_PROGRESS - - var position = startId - if (startId == VLC_FROM_START) { - position = 1 - } else if (startId == VLC_FROM_PROGRESS) { - position = 0 - } - - vlcIntent.putExtra("position", position) - - vlcIntent.component = VLC_COMPONENT - act.setKey(VLC_LAST_ID_KEY, episodeClick.data.id) - act.startActivityForResult(vlcIntent, VLC_REQUEST_CODE) - } catch (e: Exception) { - logError(e) - showToast(act, e.toString(), Toast.LENGTH_LONG) - } - } - } - - ACTION_PLAY_EPISODE_IN_PLAYER -> { - viewModel.getGenerator(episodeClick.data) - ?.let { generator -> - activity?.navigate( - R.id.global_to_navigation_player, - GeneratorPlayer.newInstance( - generator, syncdata?.let { HashMap(it) } - ) - ) - } - } - - ACTION_RELOAD_EPISODE -> { - viewModel.loadEpisode(episodeClick.data, false, clearCache = true) - } - - ACTION_DOWNLOAD_MIRROR -> { - acquireSingleExtractorLink( - sortUrls( - currentLinks ?: return@main - ),//(currentLinks ?: return@main).filter { !it.isM3u8 }, - context?.getString(R.string.episode_action_download_mirror) ?: "" - ) { link -> - startDownload( - context, - episodeClick.data, - currentIsMovie ?: return@acquireSingleExtractorLink, - currentHeaderName ?: return@acquireSingleExtractorLink, - currentType ?: return@acquireSingleExtractorLink, - currentPoster ?: return@acquireSingleExtractorLink, - apiName, - currentId ?: return@acquireSingleExtractorLink, - url ?: return@acquireSingleExtractorLink, - listOf(link), - sortSubs(currentSubs ?: return@acquireSingleExtractorLink), - ) - showToast(activity, R.string.download_started, Toast.LENGTH_SHORT) - } - } - } - } + var loadingDialog: Dialog? = null + var popupDialog: Dialog? = null @SuppressLint("SetTextI18n") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -1290,12 +459,18 @@ class ResultFragment : ResultTrailerPlayer() { // activity?.fixPaddingStatusbar(result_toolbar) - 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) - val resumeEpisode = arguments?.getInt(EPISODE_BUNDLE) - val resumeSeason = arguments?.getInt(SEASON_BUNDLE) + val url = arguments?.getString(URL_BUNDLE) + val apiName = arguments?.getString(API_NAME_BUNDLE) ?: return + val startAction = arguments?.getInt(START_ACTION_BUNDLE) + val start = startAction?.let { action -> + val startValue = arguments?.getInt(START_VALUE_BUNDLE) + val resumeEpisode = arguments?.getInt(EPISODE_BUNDLE) + val resumeSeason = arguments?.getInt(SEASON_BUNDLE) + + arguments?.remove(START_VALUE_BUNDLE) + arguments?.remove(START_ACTION_BUNDLE) + AutoResume(startAction = action, id = startValue, episode = resumeEpisode, season = resumeSeason) + } syncModel.addFromUrl(url) val api = getApiFromName(apiName) @@ -1349,10 +524,10 @@ class ResultFragment : ResultTrailerPlayer() { ArrayList(), api.hasDownloadSupport, { episodeClick -> - handleAction(episodeClick) + viewModel.handleAction(activity, episodeClick) }, { downloadClickEvent -> - handleDownloadClick(activity, currentHeaderName, downloadClickEvent) + handleDownloadClick(activity, downloadClickEvent) } ) @@ -1407,49 +582,7 @@ class ResultFragment : ResultTrailerPlayer() { result_episode_select?.isFocusableInTouchMode = context?.isTvSettings() == true result_dub_select?.isFocusableInTouchMode = context?.isTvSettings() == true - observe(viewModel.selectedSeason) { season -> - result_season_button?.text = fromIndexToSeasonText(season) - } - observe(viewModel.seasonSelections) { seasonList -> - result_season_button?.visibility = if (seasonList.size <= 1) GONE else VISIBLE.also { - - // If the season button is visible the result season button will be next focus down - if (result_series_parent?.isVisible == true) - setFocusUpAndDown(result_resume_series_button, result_season_button) - else - setFocusUpAndDown(result_bookmark_button, result_season_button) - } - - result_season_button?.setOnClickListener { - result_season_button?.popupMenuNoIconsAndNoStringRes( - items = seasonList - .map { Pair(it ?: -2, fromIndexToSeasonText(it)) }, - ) { - val id = this.itemId - - viewModel.changeSeason(if (id == -2) null else id) - } - } - } - - observe(viewModel.selectedRange) { range -> - result_episode_select?.text = range - } - - observe(viewModel.rangeOptions) { range -> - episodeRanges = range - result_episode_select?.visibility = if (range.size <= 1) GONE else VISIBLE.also { - - // If Season button is invisible then the bookmark button next focus is episode select - if (result_season_button?.isVisible != true) { - if (result_series_parent?.isVisible == true) - setFocusUpAndDown(result_resume_series_button, result_episode_select) - else - setFocusUpAndDown(result_bookmark_button, result_episode_select) - } - } - } context?.let { ctx -> val arrayAdapter = ArrayAdapter(ctx, R.layout.sort_bottom_single_choice) @@ -1523,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?) { @@ -1549,7 +678,8 @@ 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 = @@ -1609,169 +739,217 @@ class ResultFragment : ResultTrailerPlayer() { result_overlapping_panels?.setStartPanelLockState(if (closed) OverlappingPanelsLayout.LockState.CLOSE else OverlappingPanelsLayout.LockState.UNLOCKED) } - observe(viewModel.episodes) { episodeList -> - lateFixDownloadButton(episodeList.size <= 1) // movies can have multible parts but still be *movies* this will fix this - var isSeriesVisible = false - var isProgressVisible = false - DataStoreHelper.getLastWatched(currentId)?.let { resume -> - if (currentIsMovie == false) { - isSeriesVisible = true + observe(viewModel.resumeWatching) { resume -> + when (resume) { + is Some.Success -> { + result_resume_parent?.isVisible = true + val value = resume.value + value.progress?.let { progress -> + result_resume_series_title?.apply { + isVisible = !value.isMovie + text = + if (value.isMovie) null else activity?.getNameFull( + value.result.name, + value.result.episode, + value.result.season + ) + } + result_resume_series_progress_text.setText(progress.progressLeft) + result_resume_series_progress?.apply { + isVisible = true + this.max = progress.maxProgress + this.progress = progress.progress + } + result_resume_progress_holder?.isVisible = true + } ?: run { + result_resume_progress_holder?.isVisible = false + result_resume_series_progress?.isVisible = false + result_resume_series_title?.isVisible = false + result_resume_series_progress_text?.isVisible = false + } + + result_resume_series_button?.isVisible = !value.isMovie result_resume_series_button?.setOnClickListener { - episodeList.firstOrNull { it.id == resume.episodeId }?.let { - handleAction(EpisodeClickEvent(ACTION_PLAY_EPISODE_IN_PLAYER, it)) - } - } - - result_resume_series_title?.text = - if (resume.season == null) - "${getString(R.string.episode)} ${resume.episode}" - else - " \"${getString(R.string.season_short)}${resume.season}:${getString(R.string.episode_short)}${resume.episode}\"" - } - - getViewPos(resume.episodeId)?.let { viewPos -> - if (viewPos.position > 30_000L || currentIsMovie == false) { // first 30s will not show for movies - result_resume_series_progress?.apply { - max = (viewPos.duration / 1000).toInt() - progress = (viewPos.position / 1000).toInt() - } - result_resume_series_progress_text?.text = - getString(R.string.resume_time_left).format((viewPos.duration - viewPos.position) / (60_000)) - isProgressVisible = true - } else { - isProgressVisible = false - isSeriesVisible = false - } - } ?: run { - isProgressVisible = false - isSeriesVisible = false - } - } - - result_series_parent?.isVisible = isSeriesVisible - if (isSeriesVisible && activity?.currentFocus?.id == R.id.result_back && context?.isTrueTvSettings() == true) { - result_resume_series_button?.requestFocus() - } - - if (isSeriesVisible) { - val down = when { - result_season_button?.isVisible == true -> result_season_button - result_episode_select?.isVisible == true -> result_episode_select - result_dub_select?.isVisible == true -> result_dub_select - else -> null - } - setFocusUpAndDown(result_resume_series_button, down) - setFocusUpAndDown(result_bookmark_button, result_resume_series_button) - } - - result_resume_progress_holder?.isVisible = isProgressVisible - context?.getString( - when { - currentType?.isLiveStream() == true -> R.string.play_livestream_button - isProgressVisible -> R.string.resume - else -> R.string.play_movie_button - } - )?.let { - result_play_movie?.text = it - } - //println("startAction = $startAction") - - when (startAction) { - START_ACTION_RESUME_LATEST -> { - for (ep in episodeList) { - //println("WATCH STATUS::: S${ep.season} E ${ep.episode} - ${ep.getWatchProgress()}") - if (ep.getWatchProgress() > 0.90f) { // watched too much - continue - } - handleAction(EpisodeClickEvent(ACTION_PLAY_EPISODE_IN_PLAYER, ep)) - break - } - } - START_ACTION_LOAD_EP -> { - if (episodeList.size == 1) { - handleAction( + viewModel.handleAction( + activity, EpisodeClickEvent( - ACTION_PLAY_EPISODE_IN_PLAYER, - episodeList.first() + ACTION_PLAY_EPISODE_IN_PLAYER, value.result ) ) - } else { - var found = false - for (ep in episodeList) { - if (ep.id == startValue) { // watched too much - //println("WATCH STATUS::: START_ACTION_LOAD_EP S${ep.season} E ${ep.episode} - ${ep.getWatchProgress()}") - handleAction(EpisodeClickEvent(ACTION_PLAY_EPISODE_IN_PLAYER, ep)) - found = true - break - } - } - if (!found) - for (ep in episodeList) { - if (ep.episode == resumeEpisode && ep.season == resumeSeason) { - //println("WATCH STATUS::: START_ACTION_LOAD_EP S${ep.season} E ${ep.episode} - ${ep.getWatchProgress()}") - handleAction( - EpisodeClickEvent( - ACTION_PLAY_EPISODE_IN_PLAYER, - ep - ) - ) - break - } - } } - } - else -> Unit + is Some.None -> { + result_resume_parent?.isVisible = false + } } - arguments?.remove("startValue") - arguments?.remove("startAction") - startAction = null - startValue = null } - observe(viewModel.publicEpisodes) { episodes -> + observe(viewModel.episodes) { episodes -> when (episodes) { - is Resource.Failure -> { + is ResourceSome.None -> { result_episode_loading?.isVisible = false - //result_episodes?.isVisible = false + result_episodes?.isVisible = false } - is Resource.Loading -> { + is ResourceSome.Loading -> { result_episode_loading?.isVisible = true - // result_episodes?.isVisible = false + result_episodes?.isVisible = false } - is Resource.Success -> { - //result_episodes?.isVisible = true + is ResourceSome.Success -> { + result_episodes?.isVisible = true result_episode_loading?.isVisible = false - 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) } } } - observe(viewModel.dubStatus) { status -> - result_dub_select?.text = status.toString() + observe(viewModel.selectedSeason) { text -> + result_season_button.setText(text) + + // If the season button is visible the result season button will be next focus down + if (result_season_button?.isVisible == true) + if (result_resume_parent?.isVisible == true) + setFocusUpAndDown(result_resume_series_button, result_season_button) + else + setFocusUpAndDown(result_bookmark_button, result_season_button) + } + + observe(viewModel.selectedDubStatus) { status -> + result_dub_select?.setText(status) + + if (result_dub_select?.isVisible == true) + if (result_season_button?.isVisible != true && result_episode_select?.isVisible != true) { + if (result_resume_parent?.isVisible == true) + setFocusUpAndDown(result_resume_series_button, result_dub_select) + else + setFocusUpAndDown(result_bookmark_button, result_dub_select) + } + } + + observe(viewModel.selectPopup) { popup -> + when (popup) { + is Some.Success -> { + popupDialog?.dismissSafe(activity) + + popupDialog = activity?.let { act -> + val pop = popup.value + val options = pop.getOptions(act) + val title = pop.getTitle(act) + + act.showBottomDialogInstant( + options, title, { + popupDialog = null + pop.callback(null) + }, { + popupDialog = null + pop.callback(it) + } + ) + } + } + is Some.None -> { + popupDialog?.dismissSafe(activity) + popupDialog = null + } + } + + //showBottomDialogInstant + } + + observe(viewModel.loadedLinks) { load -> + when (load) { + is Some.Success -> { + if (loadingDialog?.isShowing != true) { + loadingDialog?.dismissSafe(activity) + loadingDialog = null + } + loadingDialog = loadingDialog ?: context?.let { ctx -> + val builder = + BottomSheetDialog(ctx) + builder.setContentView(R.layout.bottom_loading) + builder.setOnDismissListener { + loadingDialog = null + viewModel.cancelLinks() + } + //builder.setOnCancelListener { + // it?.dismiss() + //} + builder.setCanceledOnTouchOutside(true) + + builder.show() + + builder + } + } + is Some.None -> { + loadingDialog?.dismissSafe(activity) + loadingDialog = null + } + } + } + + observe(viewModel.selectedRange) { range -> + result_episode_select.setText(range) + + // If Season button is invisible then the bookmark button next focus is episode select + if (result_episode_select?.isVisible == true) + if (result_season_button?.isVisible != true) { + if (result_resume_parent?.isVisible == true) + setFocusUpAndDown(result_resume_series_button, result_episode_select) + else + setFocusUpAndDown(result_bookmark_button, result_episode_select) + } } // val preferDub = context?.getApiDubstatusSettings()?.all { it == DubStatus.Dubbed } == true observe(viewModel.dubSubSelections) { range -> - dubRange = range + result_dub_select.setOnClickListener { view -> + view?.context?.let { ctx -> + view.popupMenuNoIconsAndNoStringRes(range + .mapNotNull { (text, status) -> + Pair( + status.ordinal, + text?.asStringNull(ctx) ?: return@mapNotNull null + ) + }) { + viewModel.changeDubStatus(DubStatus.values()[itemId]) + } + } + } + } -// if (preferDub && dubRange?.contains(DubStatus.Dubbed) == true) { -// viewModel.changeDubStatus(DubStatus.Dubbed) -// } + observe(viewModel.rangeSelections) { range -> + result_episode_select?.setOnClickListener { view -> + view?.context?.let { ctx -> + val names = range + .mapNotNull { (text, r) -> + r to (text?.asStringNull(ctx) ?: return@mapNotNull null) + } - result_dub_select?.visibility = if (range.size <= 1) GONE else VISIBLE + view.popupMenuNoIconsAndNoStringRes(names.mapIndexed { index, (_, name) -> + index to name + }) { + viewModel.changeRange(names[itemId].first) + } + } + } + } - if (result_season_button?.isVisible != true && result_episode_select?.isVisible != true) { - if (result_series_parent?.isVisible == true) - setFocusUpAndDown(result_resume_series_button, result_dub_select) - else - setFocusUpAndDown(result_bookmark_button, result_dub_select) + observe(viewModel.seasonSelections) { seasonList -> + result_season_button?.setOnClickListener { view -> + view?.context?.let { ctx -> + val names = seasonList + .mapNotNull { (text, r) -> + r to (text?.asStringNull(ctx) ?: return@mapNotNull null) + } + + view.popupMenuNoIconsAndNoStringRes(names.mapIndexed { index, (_, name) -> + index to name + }) { + viewModel.changeSeason(names[itemId].first) + } + } } } @@ -1780,261 +958,52 @@ class ResultFragment : ResultTrailerPlayer() { if (hasFocus) result_bookmark_button?.requestFocus() } - result_dub_select.setOnClickListener { - val ranges = dubRange - if (ranges != null) { - it.popupMenuNoIconsAndNoStringRes(ranges - .map { status -> - Pair( - status.ordinal, - status.toString() - ) - } - .toList()) { - viewModel.changeDubStatus(DubStatus.values()[itemId]) - } - } - } - - result_episode_select?.setOnClickListener { - val ranges = episodeRanges - if (ranges != null) { - it.popupMenuNoIconsAndNoStringRes(ranges.mapIndexed { index, s -> Pair(index, s) } - .toList()) { - viewModel.changeRange(itemId) - } - } - } - result_sync_set_score?.setOnClickListener { syncModel.publishUserData() } - observe(viewModel.publicEpisodesCount) { count -> - if (count < 0) { - result_episodes_text?.isVisible = false - } else { - // result_episodes_text?.isVisible = true - result_episodes_text?.text = - "$count ${if (count == 1) getString(R.string.episode) else getString(R.string.episodes)}" - } + observe(viewModel.episodesCountText) { count -> + result_episodes_text.setText(count) } - observe(viewModel.id) { - currentId = it + observe(viewModel.trailers) { trailers -> + setTrailers(trailers.flatMap { it.mirros }) // I dont care about subtitles yet! } - observe(viewModel.result) { data -> + observe(viewModel.recommendations) { recommendations -> + setRecommendations(recommendations, null) + } + + observe(viewModel.movie) { data -> when (data) { - is Resource.Success -> { - val d = data.value - if (d !is AnimeLoadResponse && result_episode_loading.isVisible) { // no episode loading when not anime - result_episode_loading.isVisible = false - } - - updateVisStatus(2) - - result_vpn?.text = when (api.vpnStatus) { - VPNStatus.MightBeNeeded -> getString(R.string.vpn_might_be_needed) - VPNStatus.Torrent -> getString(R.string.vpn_torrent) - else -> "" - } - result_vpn?.isGone = api.vpnStatus == VPNStatus.None - - result_info?.text = when (api.providerType) { - ProviderType.MetaProvider -> getString(R.string.provider_info_meta) - else -> "" - } - result_info?.isVisible = api.providerType == ProviderType.MetaProvider - - if (d.type.isEpisodeBased()) { - val ep = d as? TvSeriesLoadResponse - val epCount = ep?.episodes?.size ?: 1 - if (epCount < 1) { - result_info?.text = getString(R.string.no_episodes_found) - result_info?.isVisible = true - } - } - - currentHeaderName = d.name - currentType = d.type - - currentPoster = d.posterUrl - currentIsMovie = !d.isEpisodeBased() - - result_open_in_browser?.setOnClickListener { - val i = Intent(ACTION_VIEW) - i.data = Uri.parse(d.url) - try { - startActivity(i) - } catch (e: Exception) { - logError(e) - } - } - - result_search?.setOnClickListener { - QuickSearchFragment.pushSearch(activity, d.name) - } - - result_share?.setOnClickListener { - try { - val i = Intent(ACTION_SEND) - i.type = "text/plain" - i.putExtra(EXTRA_SUBJECT, d.name) - i.putExtra(EXTRA_TEXT, d.url) - startActivity(createChooser(i, d.name)) - } catch (e: Exception) { - logError(e) - } - } - - val showStatus = when (d) { - is TvSeriesLoadResponse -> d.showStatus - is AnimeLoadResponse -> d.showStatus - else -> null - } - - setShow(showStatus) - setDuration(d.duration) - setYear(d.year) - setRating(d.rating) - setRecommendations(d.recommendations, null) - setActors(d.actors) - setNextEpisode(if (d is EpisodeResponse) d.nextAiring else null) - setTrailers(d.trailers.flatMap { it.mirros }) // I dont care about subtitles yet! - - if (syncModel.addSyncs(d.syncData)) { - syncModel.updateMetaAndUser() - syncModel.updateSynced() - } else { - syncModel.addFromUrl(d.url) - } - - result_meta_site?.text = d.apiName - - val posterImageLink = d.posterUrl - if (!posterImageLink.isNullOrEmpty()) { - result_poster?.setImage(posterImageLink, d.posterHeaders) - //result_poster_blur?.setImageBlur(posterImageLink, 10, 3, d.posterHeaders) - //Full screen view of Poster image - if (context?.isTrueTvSettings() == false) // Poster not clickable on tv - result_poster_holder?.setOnClickListener { - try { - context?.let { ctx -> - runBlocking { - val sourceBuilder = AlertDialog.Builder(ctx) - sourceBuilder.setView(R.layout.result_poster) - - val sourceDialog = sourceBuilder.create() - sourceDialog.show() - - sourceDialog.findViewById(R.id.imgPoster) - ?.apply { - setImage(posterImageLink) - setOnClickListener { - sourceDialog.dismissSafe() - } - } - } - } - } catch (e: Exception) { - logError(e) - } - } - - } else { - result_poster?.setImageResource(R.drawable.default_cover) - //result_poster_blur?.setImageResource(R.drawable.default_cover) - } - - result_poster_holder?.visibility = VISIBLE - - result_play_movie?.text = - if (d.type == TvType.Live) getString(R.string.play_livestream_button) else getString( - R.string.play_movie_button - ) - //result_plot_header?.text = - // if (d.type == TvType.Torrent) getString(R.string.torrent_plot) else getString(R.string.result_plot) - val syno = d.plot - if (!syno.isNullOrEmpty()) { - result_description?.setOnClickListener { - val builder: AlertDialog.Builder = - AlertDialog.Builder(requireContext(), R.style.AlertDialogCustom) - builder.setMessage(syno.html()) - .setTitle(if (d.type == TvType.Torrent) R.string.torrent_plot else R.string.result_plot) - .show() - } - result_description?.text = syno.html() - } else { - result_description?.text = - if (d.type == TvType.Torrent) getString(R.string.torrent_no_plot) else getString( - R.string.normal_no_plot - ) - } - - result_tag?.removeAllViews() - //result_tag_holder?.visibility = GONE - // result_status.visibility = GONE - - d.comingSoon.let { soon -> - result_coming_soon?.isVisible = soon - result_data_holder?.isGone = soon - } - - val tags = d.tags - if (tags.isNullOrEmpty()) { - //result_tag_holder?.visibility = GONE - } else { - //result_tag_holder?.visibility = VISIBLE - val isOnTv = context?.isTrueTvSettings() == true - for ((index, tag) in tags.withIndex()) { - val viewBtt = layoutInflater.inflate(R.layout.result_tag, null) - val btt = viewBtt.findViewById(R.id.result_tag_card) - btt.text = tag - btt.isFocusable = !isOnTv - btt.isClickable = !isOnTv - result_tag?.addView(viewBtt, index) - } - } - - if (d.type.isMovieType()) { - val hasDownloadSupport = api.hasDownloadSupport - lateFixDownloadButton(true) - + is ResourceSome.Success -> { + data.value.let { (text, ep) -> + result_play_movie.setText(text) result_play_movie?.setOnClickListener { - val card = - currentEpisodes?.firstOrNull() ?: return@setOnClickListener - handleAction(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) + viewModel.handleAction( + activity, + EpisodeClickEvent(ACTION_CLICK_DEFAULT, ep) + ) } - result_play_movie?.setOnLongClickListener { - val card = currentEpisodes?.firstOrNull() - ?: return@setOnLongClickListener true - handleAction(EpisodeClickEvent(ACTION_SHOW_OPTIONS, card)) + viewModel.handleAction( + activity, + EpisodeClickEvent(ACTION_SHOW_OPTIONS, ep) + ) return@setOnLongClickListener true } - result_download_movie?.setOnLongClickListener { - val card = currentEpisodes?.firstOrNull() - ?: return@setOnLongClickListener true - handleAction(EpisodeClickEvent(ACTION_SHOW_OPTIONS, card)) - return@setOnLongClickListener true - } - -// result_options.setOnClickListener { -// val card = currentEpisodes?.first() ?: return@setOnClickListener -// handleAction(EpisodeClickEvent(ACTION_SHOW_OPTIONS, card)) -// } - - result_movie_progress_downloaded_holder?.isVisible = hasDownloadSupport - if (hasDownloadSupport) { - val localId = d.getId() - + main { val file = - VideoDownloadManager.getDownloadFileInfoAndUpdateSettings( - requireContext(), - localId - ) + ioWork { + context?.let { + VideoDownloadManager.getDownloadFileInfoAndUpdateSettings( + it, + ep.id + ) + } + } + downloadButton?.dispose() downloadButton = EasyDownloadButton() downloadButton?.setUpMoreButton( @@ -2047,111 +1016,156 @@ class ResultFragment : ResultTrailerPlayer() { result_download_movie, true, VideoDownloadHelper.DownloadEpisodeCached( - d.name, - d.posterUrl, + ep.name, + ep.poster, 0, null, - localId, - localId, - d.rating, - d.plot, - System.currentTimeMillis(), - ), - ::handleDownloadButton - ) - - result_download_movie?.setOnLongClickListener { - val card = - currentEpisodes?.firstOrNull() - ?: return@setOnLongClickListener false - handleAction(EpisodeClickEvent(ACTION_DOWNLOAD_MIRROR, card)) - return@setOnLongClickListener true - } - - /*downloadButton?.setUpMaterialButton( - file?.fileLength, - file?.totalBytes, - result_movie_progress_downloaded, - result_download_movie, - null, //result_movie_text_progress - VideoDownloadHelper.DownloadEpisodeCached( - d.name, - d.posterUrl, - 0, + ep.id, + ep.id, + null, null, - localId, - localId, - d.rating, - d.plot, System.currentTimeMillis(), ) - ) { downloadClickEvent -> - if (downloadClickEvent.action == DOWNLOAD_ACTION_DOWNLOAD) { - currentEpisodes?.firstOrNull()?.let { episode -> - handleAction( - EpisodeClickEvent( - ACTION_DOWNLOAD_EPISODE, - ResultEpisode( - d.name, - d.name, - null, - 0, - null, - episode.data, - d.apiName, - localId, - 0, - 0L, - 0L, - null, - null, - null, - d.type, - localId, - ) - ) + ) { click -> + when(click.action) { + DOWNLOAD_ACTION_DOWNLOAD -> { + viewModel.handleAction( + activity, + EpisodeClickEvent(ACTION_DOWNLOAD_EPISODE, ep) ) } - } else { - handleDownloadClick( - activity, - currentHeaderName, - downloadClickEvent - ) + else -> handleDownloadClick(activity, click) } - }*/ + } + result_movie_progress_downloaded_holder?.isVisible = true } + } + } + else -> { + result_movie_progress_downloaded_holder?.isVisible = false + result_play_movie?.isVisible = false + } + } + } + + observe(viewModel.page) { data -> + when (data) { + is Resource.Success -> { + val d = data.value + + updateVisStatus(2) + + result_vpn.setText(d.vpnText) + result_info.setText(d.metaText) + result_no_episodes.setText(d.noEpisodesFoundText) + result_title.setText(d.titleText) + result_meta_site.setText(d.apiName) + result_meta_type.setText(d.typeText) + result_meta_year.setText(d.yearText) + result_meta_duration.setText(d.durationText) + result_meta_rating.setText(d.ratingText) + result_cast_text.setText(d.actorsText) + result_next_airing.setText(d.nextAiringEpisode) + result_next_airing_time.setText(d.nextAiringDate) + result_poster.setImage(d.posterImage) + + if (d.posterImage != null && context?.isTrueTvSettings() == false) + result_poster_holder?.setOnClickListener { + try { + context?.let { ctx -> + runBlocking { + val sourceBuilder = AlertDialog.Builder(ctx) + sourceBuilder.setView(R.layout.result_poster) + + val sourceDialog = sourceBuilder.create() + sourceDialog.show() + + sourceDialog.findViewById(R.id.imgPoster) + ?.apply { + setImage(d.posterImage) + setOnClickListener { + sourceDialog.dismissSafe() + } + } + } + } + } catch (e: Exception) { + logError(e) + } + } + + + result_cast_items?.isVisible = d.actors != null + (result_cast_items?.adapter as ActorAdaptor?)?.apply { + updateList(d.actors ?: emptyList()) + } + + result_open_in_browser?.isGone = d.url.isBlank() + result_open_in_browser?.setOnClickListener { + val i = Intent(ACTION_VIEW) + i.data = Uri.parse(d.url) + try { + startActivity(i) + } catch (e: Exception) { + logError(e) + } + } + + result_search?.setOnClickListener { + QuickSearchFragment.pushSearch(activity, d.title) + } + + result_share?.setOnClickListener { + try { + val i = Intent(ACTION_SEND) + i.type = "text/plain" + i.putExtra(EXTRA_SUBJECT, d.title) + i.putExtra(EXTRA_TEXT, d.url) + startActivity(createChooser(i, d.title)) + } catch (e: Exception) { + logError(e) + } + } + + if (syncModel.addSyncs(d.syncData)) { + syncModel.updateMetaAndUser() + syncModel.updateSynced() } else { - lateFixDownloadButton(false) + syncModel.addFromUrl(d.url) } - context?.getString( - when (d.type) { - TvType.TvSeries -> R.string.tv_series_singular - TvType.Anime -> R.string.anime_singular - TvType.OVA -> R.string.ova_singular - TvType.AnimeMovie -> R.string.movies_singular - TvType.Cartoon -> R.string.cartoons_singular - TvType.Documentary -> R.string.documentaries_singular - TvType.Movie -> R.string.movies_singular - TvType.Torrent -> R.string.torrent_singular - TvType.AsianDrama -> R.string.asian_drama_singular - TvType.Live -> R.string.live_singular + result_description.setTextHtml(d.plotText) + result_description?.setOnClickListener { view -> + view.context?.let { ctx -> + val builder: AlertDialog.Builder = + AlertDialog.Builder(ctx, R.style.AlertDialogCustom) + builder.setMessage(d.plotText.asString(ctx).html()) + .setTitle(d.plotHeaderText.asString(ctx)) + .show() } - )?.let { - result_meta_type?.text = it } - when (d) { - is AnimeLoadResponse -> { - // val preferEnglish = true - //val titleName = (if (preferEnglish) d.engName else d.japName) ?: d.name - val titleName = d.name - result_title.text = titleName - //result_toolbar.title = titleName + result_tag?.removeAllViews() + + d.comingSoon.let { soon -> + result_coming_soon?.isVisible = soon + result_data_holder?.isGone = soon + } + + val tags = d.tags + result_tag_holder?.isVisible = tags.isNotEmpty() + if (tags.isNotEmpty()) { + //result_tag_holder?.visibility = VISIBLE + val isOnTv = context?.isTrueTvSettings() == true + for ((index, tag) in tags.withIndex()) { + val viewBtt = layoutInflater.inflate(R.layout.result_tag, null) + val btt = viewBtt.findViewById(R.id.result_tag_card) + btt.text = tag + btt.isFocusable = !isOnTv + btt.isClickable = !isOnTv + result_tag?.addView(viewBtt, index) } - else -> result_title.text = d.name } } is Resource.Failure -> { @@ -2172,8 +1186,9 @@ class ResultFragment : ResultTrailerPlayer() { SearchHelper.handleSearchClickCallback(activity, callback) } - context?.let { ctx -> + val dubStatus = if(ctx.getApiDubstatusSettings().contains(DubStatus.Dubbed)) DubStatus.Dubbed else DubStatus.Subbed + result_bookmark_button?.isVisible = ctx.isTvSettings() val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx) @@ -2183,15 +1198,14 @@ class ResultFragment : ResultTrailerPlayer() { Kitsu.isEnabled = settingsManager.getBoolean(ctx.getString(R.string.show_kitsu_posters_key), true) - val tempUrl = url - if (tempUrl != null) { + if (url != null) { result_reload_connectionerror.setOnClickListener { - viewModel.load(tempUrl, apiName, showFillers) + viewModel.load(activity, url, apiName, showFillers, dubStatus, start) } result_reload_connection_open_in_browser?.setOnClickListener { val i = Intent(ACTION_VIEW) - i.data = Uri.parse(tempUrl) + i.data = Uri.parse(url) try { startActivity(i) } catch (e: Exception) { @@ -2201,7 +1215,7 @@ class ResultFragment : ResultTrailerPlayer() { result_open_in_browser?.setOnClickListener { val i = Intent(ACTION_VIEW) - i.data = Uri.parse(tempUrl) + i.data = Uri.parse(url) try { startActivity(i) } catch (e: Exception) { @@ -2212,16 +1226,16 @@ class ResultFragment : ResultTrailerPlayer() { // bloats the navigation on tv if (context?.isTrueTvSettings() == false) { result_meta_site?.setOnClickListener { - it.context?.openBrowser(tempUrl) + it.context?.openBrowser(url) } result_meta_site?.isFocusable = true } else { result_meta_site?.isFocusable = false } - if (restart || viewModel.result.value == null) { + if (restart || !viewModel.hasLoaded()) { //viewModel.clear() - viewModel.load(tempUrl, apiName, showFillers) + viewModel.load(activity, url, apiName, showFillers, dubStatus, start) } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt deleted file mode 100644 index 1cdd5e24..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt +++ /dev/null @@ -1,625 +0,0 @@ -package com.lagradost.cloudstream3.ui.result - -import android.util.Log -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.lagradost.cloudstream3.* -import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings -import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull -import com.lagradost.cloudstream3.APIHolder.getApiFromUrlNull -import com.lagradost.cloudstream3.APIHolder.getId -import com.lagradost.cloudstream3.AcraApplication.Companion.context -import com.lagradost.cloudstream3.AcraApplication.Companion.setKey -import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer -import com.lagradost.cloudstream3.LoadResponse.Companion.getAniListId -import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId -import com.lagradost.cloudstream3.animeproviders.GogoanimeProvider -import com.lagradost.cloudstream3.animeproviders.NineAnimeProvider -import com.lagradost.cloudstream3.metaproviders.SyncRedirector -import com.lagradost.cloudstream3.mvvm.Resource -import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.mvvm.safeApiCall -import com.lagradost.cloudstream3.syncproviders.SyncAPI -import com.lagradost.cloudstream3.syncproviders.providers.Kitsu.getEpisodesDetails -import com.lagradost.cloudstream3.ui.APIRepository -import com.lagradost.cloudstream3.ui.WatchType -import com.lagradost.cloudstream3.ui.player.IGenerator -import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator -import com.lagradost.cloudstream3.ui.player.SubtitleData -import com.lagradost.cloudstream3.utils.Coroutines.ioWork -import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE -import com.lagradost.cloudstream3.utils.DataStoreHelper -import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData -import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultSeason -import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultWatchState -import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos -import com.lagradost.cloudstream3.utils.DataStoreHelper.setBookmarkedData -import com.lagradost.cloudstream3.utils.DataStoreHelper.setDub -import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultSeason -import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultWatchState -import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.FillerEpisodeCheck.getFillerEpisodes -import com.lagradost.cloudstream3.utils.VideoDownloadHelper -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlin.collections.set - -const val EPISODE_RANGE_SIZE = 50 -const val EPISODE_RANGE_OVERLOAD = 60 - -class ResultViewModel : ViewModel() { - private var repo: APIRepository? = null - private var generator: IGenerator? = null - - private val _resultResponse: MutableLiveData> = MutableLiveData() - private val _episodes: MutableLiveData> = MutableLiveData() - private val episodeById: MutableLiveData> = - MutableLiveData() // lookup by ID to get Index - - private val _publicEpisodes: MutableLiveData>> = MutableLiveData() - private val _publicEpisodesCount: MutableLiveData = MutableLiveData() // before the sorting - private val _rangeOptions: MutableLiveData> = MutableLiveData() - val selectedRange: MutableLiveData = MutableLiveData() - private val selectedRangeInt: MutableLiveData = MutableLiveData() - val rangeOptions: LiveData> = _rangeOptions - - val result: LiveData> get() = _resultResponse - - val episodes: LiveData> get() = _episodes - val publicEpisodes: LiveData>> get() = _publicEpisodes - val publicEpisodesCount: LiveData get() = _publicEpisodesCount - - val dubStatus: LiveData get() = _dubStatus - private val _dubStatus: MutableLiveData = MutableLiveData() - - val id: MutableLiveData = MutableLiveData() - val selectedSeason: MutableLiveData = MutableLiveData(-2) - val seasonSelections: MutableLiveData> = MutableLiveData() - - val dubSubSelections: LiveData> get() = _dubSubSelections - private val _dubSubSelections: MutableLiveData> = MutableLiveData() - - val dubSubEpisodes: LiveData>?> get() = _dubSubEpisodes - private val _dubSubEpisodes: MutableLiveData>?> = - MutableLiveData() - - private val _watchStatus: MutableLiveData = MutableLiveData() - val watchStatus: LiveData get() = _watchStatus - - fun updateWatchStatus(status: WatchType) = viewModelScope.launch { - val currentId = id.value ?: return@launch - _watchStatus.postValue(status) - val resultPage = _resultResponse.value - - withContext(Dispatchers.IO) { - setResultWatchState(currentId, status.internalId) - if (resultPage != null && resultPage is Resource.Success) { - val resultPageData = resultPage.value - val current = getBookmarkedData(currentId) - val currentTime = System.currentTimeMillis() - setBookmarkedData( - currentId, - DataStoreHelper.BookmarkedData( - currentId, - current?.bookmarkedTime ?: currentTime, - currentTime, - resultPageData.name, - resultPageData.url, - resultPageData.apiName, - resultPageData.type, - resultPageData.posterUrl, - resultPageData.year - ) - ) - } - } - } - - companion object { - const val TAG = "RVM" - } - - var lastMeta: SyncAPI.SyncResult? = null - var lastSync: Map? = null - - private suspend fun applyMeta( - resp: LoadResponse, - meta: SyncAPI.SyncResult?, - syncs: Map? = null - ): Pair { - if (meta == null) return resp to false - var updateEpisodes = false - val out = resp.apply { - Log.i(TAG, "applyMeta") - - duration = duration ?: meta.duration - rating = rating ?: meta.publicScore - tags = tags ?: meta.genres - plot = if (plot.isNullOrBlank()) meta.synopsis else plot - posterUrl = posterUrl ?: meta.posterUrl ?: meta.backgroundPosterUrl - actors = actors ?: meta.actors - - if (this is EpisodeResponse) { - nextAiring = nextAiring ?: meta.nextAiring - } - - for ((k, v) in syncs ?: emptyMap()) { - syncData[k] = v - } - - val realRecommendations = ArrayList() - val apiNames = listOf(GogoanimeProvider().name, NineAnimeProvider().name) - meta.recommendations?.forEach { rec -> - apiNames.forEach { name -> - realRecommendations.add(rec.copy(apiName = name)) - } - } - - recommendations = recommendations?.union(realRecommendations)?.toList() - ?: realRecommendations - - argamap({ - addTrailer(meta.trailers) - }, { - if (this !is AnimeLoadResponse) return@argamap - val map = getEpisodesDetails(getMalId(), getAniListId(), isResponseRequired = false) - if (map.isNullOrEmpty()) return@argamap - updateEpisodes = DubStatus.values().map { dubStatus -> - val current = - this.episodes[dubStatus]?.mapIndexed { index, episode -> - episode.apply { - this.episode = this.episode ?: (index + 1) - } - }?.sortedBy { it.episode ?: 0 }?.toMutableList() - if (current.isNullOrEmpty()) return@map false - val episodeNumbers = current.map { ep -> ep.episode!! } - var updateCount = 0 - map.forEach { (episode, node) -> - episodeNumbers.binarySearch(episode).let { index -> - current.getOrNull(index)?.let { currentEp -> - current[index] = currentEp.apply { - updateCount++ - val currentBack = this - this.description = this.description ?: node.description?.en - this.name = this.name ?: node.titles?.canonical - this.episode = this.episode ?: node.num ?: episodeNumbers[index] - this.posterUrl = this.posterUrl ?: node.thumbnail?.original?.url - } - } - } - } - this.episodes[dubStatus] = current - updateCount > 0 - }.any { it } - }) - } - return out to updateEpisodes - } - - fun setMeta(meta: SyncAPI.SyncResult, syncs: Map?) = - viewModelScope.launch { - Log.i(TAG, "setMeta") - lastMeta = meta - lastSync = syncs - val (value, updateEpisodes) = ioWork { - (result.value as? Resource.Success?)?.value?.let { resp -> - return@ioWork applyMeta(resp, meta, syncs) - } - return@ioWork null to null - } - _resultResponse.postValue(Resource.Success(value ?: return@launch)) - if (updateEpisodes ?: return@launch) updateEpisodes(value, lastShowFillers) - } - - private fun loadWatchStatus(localId: Int? = null) { - val currentId = localId ?: id.value ?: return - val currentWatch = getResultWatchState(currentId) - _watchStatus.postValue(currentWatch) - } - - private fun filterEpisodes(list: List?, selection: Int?, range: Int?) { - if (list == null) return - val seasonTypes = HashMap() - for (i in list) { - if (!seasonTypes.containsKey(i.season)) { - seasonTypes[i.season] = true - } - } - val seasons = seasonTypes.toList().map { it.first }.sortedBy { it } - seasonSelections.postValue(seasons) - if (seasons.isEmpty()) { // WHAT THE FUCK DID YOU DO????? HOW DID YOU DO THIS - _publicEpisodes.postValue(Resource.Success(emptyList())) - return - } - - val realSelection = if (!seasonTypes.containsKey(selection)) seasons.first() else selection - val internalId = id.value - - if (internalId != null) setResultSeason(internalId, realSelection) - - selectedSeason.postValue(realSelection ?: -2) - - var currentList = list.filter { it.season == realSelection } - _publicEpisodesCount.postValue(currentList.size) - - val rangeList = ArrayList() - for (i in currentList.indices step EPISODE_RANGE_SIZE) { - if (i + EPISODE_RANGE_SIZE < currentList.size) { - rangeList.add("${i + 1}-${i + EPISODE_RANGE_SIZE}") - } else { - rangeList.add("${i + 1}-${currentList.size}") - } - } - - val cRange = range ?: if (selection != null) { - 0 - } else { - selectedRangeInt.value ?: 0 - } - - val realRange = if (cRange * EPISODE_RANGE_SIZE > currentList.size) { - currentList.size / EPISODE_RANGE_SIZE - } else { - cRange - } - - if (currentList.size > EPISODE_RANGE_OVERLOAD) { - currentList = currentList.subList( - realRange * EPISODE_RANGE_SIZE, - minOf(currentList.size, (realRange + 1) * EPISODE_RANGE_SIZE) - ) - _rangeOptions.postValue(rangeList) - selectedRangeInt.postValue(realRange) - selectedRange.postValue(rangeList[realRange]) - } else { - val allRange = "1-${currentList.size}" - _rangeOptions.postValue(listOf(allRange)) - selectedRangeInt.postValue(0) - selectedRange.postValue(allRange) - } - - _publicEpisodes.postValue(Resource.Success(currentList)) - } - - fun changeSeason(selection: Int?) { - filterEpisodes(_episodes.value, selection, null) - } - - fun changeRange(range: Int?) { - filterEpisodes(_episodes.value, null, range) - } - - fun changeDubStatus(status: DubStatus?) { - if (status == null) return - dubSubEpisodes.value?.get(status)?.let { episodes -> - id.value?.let { - setDub(it, status) - } - _dubStatus.postValue(status!!) - updateEpisodes(null, episodes, null) - } - } - - suspend fun loadEpisode( - episode: ResultEpisode, - isCasting: Boolean, - clearCache: Boolean = false - ): Resource, Set>> { - return safeApiCall { - val index = _episodes.value?.indexOf(episode) ?: episode.index - - val currentLinks = mutableSetOf() - val currentSubs = mutableSetOf() - - generator?.goto(index) - generator?.generateLinks(clearCache, isCasting, { - it.first?.let { link -> - currentLinks.add(link) - } - }, { sub -> - currentSubs.add(sub) - }) - - return@safeApiCall Pair( - currentLinks.toSet(), - currentSubs.toSet() - ) - } - } - - fun getGenerator(episode: ResultEpisode): IGenerator? { - val index = _episodes.value?.indexOf(episode) ?: episode.index - - generator?.goto(index) - return generator - } - - private fun updateEpisodes(localId: Int?, list: List, selection: Int?) { - _episodes.postValue(list) - generator = RepoLinkGenerator(list) - - val set = HashMap() - val range = selectedRangeInt.value - - list.withIndex().forEach { set[it.value.id] = it.index } - episodeById.postValue(set) - - filterEpisodes( - list, - if (selection == -1) getResultSeason(localId ?: id.value ?: return) else selection, - range - ) - } - - fun reloadEpisodes() { - val current = _episodes.value ?: return - val copy = current.map { - val posDur = getViewPos(it.id) - it.copy(position = posDur?.position ?: 0, duration = posDur?.duration ?: 0) - } - updateEpisodes(null, copy, selectedSeason.value) - } - - private fun filterName(name: String?): String? { - if (name == null) return null - Regex("[eE]pisode [0-9]*(.*)").find(name)?.groupValues?.get(1)?.let { - if (it.isEmpty()) - return null - } - return name - } - - var lastShowFillers = false - private suspend fun updateEpisodes(loadResponse: LoadResponse, showFillers: Boolean) { - Log.i(TAG, "updateEpisodes") - try { - lastShowFillers = showFillers - val mainId = loadResponse.getId() - - when (loadResponse) { - is AnimeLoadResponse -> { - if (loadResponse.episodes.isEmpty()) { - _dubSubEpisodes.postValue(emptyMap()) - return - } - -// val status = getDub(mainId) - val statuses = loadResponse.episodes.map { it.key } - - // Extremely bruh to have to take in context here, but I'm not sure how to do this in a better way :( - val preferDub = context?.getApiDubstatusSettings() - ?.contains(DubStatus.Dubbed) == true - - // 3 statements because there can be only dub even if you do not prefer it. - val dubStatus = - if (preferDub && statuses.contains(DubStatus.Dubbed)) DubStatus.Dubbed - else if (!preferDub && statuses.contains(DubStatus.Subbed)) DubStatus.Subbed - else statuses.first() - - val fillerEpisodes = - if (showFillers) safeApiCall { getFillerEpisodes(loadResponse.name) } else null - - val existingEpisodes = HashSet() - val res = loadResponse.episodes.map { ep -> - val episodes = ArrayList() - val idIndex = ep.key.id - for ((index, i) in ep.value.withIndex()) { - val episode = i.episode ?: (index + 1) - val id = mainId + episode + idIndex * 1000000 - if (!existingEpisodes.contains(episode)) { - existingEpisodes.add(id) - episodes.add(buildResultEpisode( - loadResponse.name, - filterName(i.name), - i.posterUrl, - episode, - i.season, - i.data, - loadResponse.apiName, - id, - index, - i.rating, - i.description, - if (fillerEpisodes is Resource.Success) fillerEpisodes.value?.let { - it.contains(episode) && it[episode] == true - } ?: false else false, - loadResponse.type, - mainId - )) - } - } - - Pair(ep.key, episodes) - }.toMap() - - // These posts needs to be in this order as to make the preferDub in ResultFragment work - _dubSubEpisodes.postValue(res) - res[dubStatus]?.let { episodes -> - updateEpisodes(mainId, episodes, -1) - } - - _dubStatus.postValue(dubStatus) - _dubSubSelections.postValue(loadResponse.episodes.keys) - } - - is TvSeriesLoadResponse -> { - val episodes = ArrayList() - val existingEpisodes = HashSet() - for ((index, episode) in loadResponse.episodes.sortedBy { - (it.season?.times(10000) ?: 0) + (it.episode ?: 0) - }.withIndex()) { - val episodeIndex = episode.episode ?: (index + 1) - val id = - mainId + (episode.season?.times(100000) ?: 0) + episodeIndex + 1 - if (!existingEpisodes.contains(id)) { - existingEpisodes.add(id) - episodes.add( - buildResultEpisode( - loadResponse.name, - filterName(episode.name), - episode.posterUrl, - episodeIndex, - episode.season, - episode.data, - loadResponse.apiName, - id, - index, - episode.rating, - episode.description, - null, - loadResponse.type, - mainId - ) - ) - } - } - updateEpisodes(mainId, episodes, -1) - } - is MovieLoadResponse -> { - buildResultEpisode( - loadResponse.name, - loadResponse.name, - null, - 0, - null, - loadResponse.dataUrl, - loadResponse.apiName, - (mainId), // HAS SAME ID - 0, - null, - null, - null, - loadResponse.type, - mainId - ).let { - updateEpisodes(mainId, listOf(it), -1) - } - } - is LiveStreamLoadResponse -> { - buildResultEpisode( - loadResponse.name, - loadResponse.name, - null, - 0, - null, - loadResponse.dataUrl, - loadResponse.apiName, - (mainId), // HAS SAME ID - 0, - null, - null, - null, - loadResponse.type, - mainId - ).let { - updateEpisodes(mainId, listOf(it), -1) - } - } - is TorrentLoadResponse -> { - updateEpisodes( - mainId, listOf( - buildResultEpisode( - loadResponse.name, - loadResponse.name, - null, - 0, - null, - loadResponse.torrent ?: loadResponse.magnet ?: "", - loadResponse.apiName, - (mainId), // HAS SAME ID - 0, - null, - null, - null, - loadResponse.type, - mainId - ) - ), -1 - ) - } - } - } catch (e: Exception) { - logError(e) - } - } - - fun load(url: String, apiName: String, showFillers: Boolean) = viewModelScope.launch { - _publicEpisodes.postValue(Resource.Loading()) - _resultResponse.postValue(Resource.Loading(url)) - - val api = getApiFromNameNull(apiName) ?: getApiFromUrlNull(url) - if (api == null) { - _resultResponse.postValue( - Resource.Failure( - false, - null, - null, - "This provider does not exist" - ) - ) - return@launch - } - - val validUrlResource = safeApiCall { - SyncRedirector.redirect( - url, - api.mainUrl.replace(NineAnimeProvider().mainUrl, "9anime") - .replace(GogoanimeProvider().mainUrl, "gogoanime") - ) - } - - if (validUrlResource !is Resource.Success) { - if (validUrlResource is Resource.Failure) { - _resultResponse.postValue(validUrlResource) - } - - return@launch - } - val validUrl = validUrlResource.value - - _resultResponse.postValue(Resource.Loading(validUrl)) - - _apiName.postValue(apiName) - - repo = APIRepository(api) - - val data = repo?.load(validUrl) ?: return@launch - - _resultResponse.postValue(data) - - when (data) { - is Resource.Success -> { - val loadResponse = if (lastMeta != null || lastSync != null) ioWork { - applyMeta(data.value, lastMeta, lastSync).first - } else data.value - _resultResponse.postValue(Resource.Success(loadResponse)) - val mainId = loadResponse.getId() - id.postValue(mainId) - loadWatchStatus(mainId) - - setKey( - DOWNLOAD_HEADER_CACHE, - mainId.toString(), - VideoDownloadHelper.DownloadHeaderCached( - apiName, - validUrl, - loadResponse.type, - loadResponse.name, - loadResponse.posterUrl, - mainId, - System.currentTimeMillis(), - ) - ) - updateEpisodes(loadResponse, showFillers) - } - else -> Unit - } - } - - private var _apiName: MutableLiveData = MutableLiveData() - val apiName: LiveData get() = _apiName - -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt new file mode 100644 index 00000000..a06655af --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt @@ -0,0 +1,1878 @@ +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 +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.CommonActivity.showToast +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 +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.getNameFull +import com.lagradost.cloudstream3.utils.AppUtils.isAppInstalled +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.Coroutines.ioWork +import com.lagradost.cloudstream3.utils.DataStore.setKey +import com.lagradost.cloudstream3.utils.DataStoreHelper.getDub +import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultEpisode +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.setDub +import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultEpisode +import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultSeason +import com.lagradost.cloudstream3.utils.UIHelper.checkWrite +import com.lagradost.cloudstream3.utils.UIHelper.navigate +import com.lagradost.cloudstream3.utils.UIHelper.requestRW +import kotlinx.coroutines.* +import java.io.File +import java.util.concurrent.TimeUnit + + +/** This starts at 1 */ +data class EpisodeRange( + // used to index data + val startIndex: Int, + val length: Int, + // used to display data + val startEpisode: Int, + val endEpisode: Int, +) + +data class AutoResume( + val season: Int?, + val episode: Int?, + val id: Int?, + val startAction: Int, +) + +data class ResultData( + val url: String, + val tags: List, + val actors: List?, + val actorsText: UiText?, + + val comingSoon: Boolean, + val backgroundPosterUrl: String?, + val title: String, + var syncData: Map, + + val posterImage: UiImage?, + val plotText: UiText, + val apiName: UiText, + val ratingText: UiText?, + val vpnText: UiText?, + val metaText: UiText?, + val durationText: UiText?, + val onGoingText: UiText?, + val noEpisodesFoundText: UiText?, + val titleText: UiText, + val typeText: UiText, + val yearText: UiText?, + val nextAiringDate: UiText?, + val nextAiringEpisode: UiText?, + val plotHeaderText: UiText, +) + +fun txt(status: DubStatus?): UiText? { + return txt( + when (status) { + DubStatus.Dubbed -> R.string.app_dubbed_text + DubStatus.Subbed -> R.string.app_subbed_text + else -> null + } + ) +} + +fun LoadResponse.toResultData(repo: APIRepository): ResultData { + debugAssert({ repo.name != apiName }) { + "Api returned wrong apiName" + } + + val hasActorImages = actors?.firstOrNull()?.actor?.image?.isNotBlank() == true + + var nextAiringEpisode: UiText? = null + var nextAiringDate: UiText? = null + + if (this is EpisodeResponse) { + val airing = this.nextAiring + if (airing != null && airing.unixTime > unixTime) { + val seconds = airing.unixTime - unixTime + val days = TimeUnit.SECONDS.toDays(seconds) + val hours: Long = TimeUnit.SECONDS.toHours(seconds) - days * 24 + val minute = + TimeUnit.SECONDS.toMinutes(seconds) - TimeUnit.SECONDS.toHours(seconds) * 60 + nextAiringDate = when { + days > 0 -> { + txt( + R.string.next_episode_time_day_format, + days, + hours, + minute + ) + } + hours > 0 -> txt( + R.string.next_episode_time_hour_format, + hours, + minute + ) + minute > 0 -> txt( + R.string.next_episode_time_min_format, + minute + ) + else -> null + }?.also { + nextAiringEpisode = txt(R.string.next_episode_format, airing.episode) + } + } + } + val dur = duration + return ResultData( + syncData = syncData, + plotHeaderText = txt( + when (this.type) { + TvType.Torrent -> R.string.torrent_plot + else -> R.string.result_plot + } + ), + nextAiringDate = nextAiringDate, + nextAiringEpisode = nextAiringEpisode, + posterImage = img( + posterUrl, posterHeaders + ) ?: img(R.drawable.default_cover), + titleText = txt(name), + url = url, + tags = tags ?: emptyList(), + comingSoon = comingSoon, + actors = if (hasActorImages) actors else null, + actorsText = if (hasActorImages || actors.isNullOrEmpty()) null else txt( + R.string.cast_format, + actors?.joinToString { it.actor.name }), + plotText = + if (plot.isNullOrBlank()) txt(if (this is TorrentLoadResponse) R.string.torrent_no_plot else R.string.normal_no_plot) else txt( + plot!! + ), + backgroundPosterUrl = backgroundPosterUrl, + title = name, + typeText = txt( + when (type) { + TvType.TvSeries -> R.string.tv_series_singular + TvType.Anime -> R.string.anime_singular + TvType.OVA -> R.string.ova_singular + TvType.AnimeMovie -> R.string.movies_singular + TvType.Cartoon -> R.string.cartoons_singular + TvType.Documentary -> R.string.documentaries_singular + TvType.Movie -> R.string.movies_singular + TvType.Torrent -> R.string.torrent_singular + TvType.AsianDrama -> R.string.asian_drama_singular + TvType.Live -> R.string.live_singular + } + ), + yearText = txt(year?.toString()), + apiName = txt(apiName), + ratingText = rating?.div(1000f)?.let { txt(R.string.rating_format, it) }, + vpnText = txt( + when (repo.vpnStatus) { + VPNStatus.None -> null + VPNStatus.Torrent -> R.string.vpn_torrent + VPNStatus.MightBeNeeded -> R.string.vpn_might_be_needed + } + ), + metaText = + if (repo.providerType == ProviderType.MetaProvider) txt(R.string.provider_info_meta) else null, + durationText = if (dur == null || dur <= 0) null else txt( + R.string.duration_format, + dur + ), + onGoingText = if (this is EpisodeResponse) { + txt( + when (showStatus) { + ShowStatus.Ongoing -> R.string.status_ongoing + ShowStatus.Completed -> R.string.status_completed + else -> null + } + ) + } else null, + noEpisodesFoundText = + if ((this is TvSeriesLoadResponse && this.episodes.isEmpty()) || (this is AnimeLoadResponse && !this.episodes.any { it.value.isNotEmpty() })) txt( + R.string.no_episodes_found + ) else null + ) +} + + +data class LinkProgress( + val linksLoaded: Int, + val subsLoaded: Int, +) + +data class ResumeProgress( + val progress: Int, + val maxProgress: Int, + val progressLeft: UiText, +) + +data class ResumeWatchingStatus( + val progress: ResumeProgress?, + val isMovie: Boolean, + val result: ResultEpisode, +) + +data class LinkLoadingResult( + val links: List, + val subs: List, +) + +sealed class SelectPopup { + data class SelectText( + val text: UiText, + val options: List, + val callback: (Int?) -> Unit + ) : SelectPopup() + + data class SelectArray( + val text: UiText, + val options: List>, + val callback: (Int?) -> Unit + ) : SelectPopup() +} + +fun SelectPopup.callback(index: Int?) { + val ret = transformResult(index) + return when (this) { + is SelectPopup.SelectArray -> callback(ret) + is SelectPopup.SelectText -> callback(ret) + } +} + +fun SelectPopup.transformResult(input: Int?): Int? { + if (input == null) return null + return when (this) { + is SelectPopup.SelectArray -> options.getOrNull(input)?.second + is SelectPopup.SelectText -> input + } +} + +fun SelectPopup.getTitle(context: Context): String { + return when (this) { + is SelectPopup.SelectArray -> text.asString(context) + is SelectPopup.SelectText -> text.asString(context) + } +} + +fun SelectPopup.getOptions(context: Context): List { + return when (this) { + is SelectPopup.SelectArray -> { + this.options.map { it.first.asString(context) } + } + is SelectPopup.SelectText -> options.map { it.asString(context) } + } +} + +class ResultViewModel2 : ViewModel() { + private var currentResponse: LoadResponse? = null + + data class EpisodeIndexer( + val dubStatus: DubStatus, + val season: Int, + ) + + /** map>> */ + private var currentEpisodes: Map> = mapOf() + private var currentRanges: Map> = mapOf() + private var currentSeasons: Set = setOf() + private var currentDubStatus: Set = setOf() + private var currentMeta: SyncAPI.SyncResult? = null + private var currentSync: Map? = null + private var currentIndex: EpisodeIndexer? = null + private var currentRange: EpisodeRange? = null + private var currentShowFillers: Boolean = false + private var currentRepo: APIRepository? = null + private var currentId: Int? = null + private var fillers: Map = emptyMap() + private var generator: IGenerator? = null + private var preferDubStatus: DubStatus? = null + private var preferStartEpisode: Int? = null + private var preferStartSeason: Int? = null + //private val currentIsMovie get() = currentResponse?.isEpisodeBased() == false + //private val currentHeaderName get() = currentResponse?.name + + + private val _page: MutableLiveData> = + MutableLiveData(Resource.Loading()) + val page: LiveData> = _page + + private val _episodes: MutableLiveData>> = + MutableLiveData(ResourceSome.Loading()) + val episodes: LiveData>> = _episodes + + private val _movie: MutableLiveData>> = + MutableLiveData(ResourceSome.None) + val movie: LiveData>> = _movie + + private val _episodesCountText: MutableLiveData> = + MutableLiveData(Some.None) + val episodesCountText: LiveData> = _episodesCountText + + private val _trailers: MutableLiveData> = MutableLiveData(mutableListOf()) + val trailers: LiveData> = _trailers + + + private val _dubSubSelections: MutableLiveData>> = + MutableLiveData(emptyList()) + val dubSubSelections: LiveData>> = _dubSubSelections + + private val _rangeSelections: MutableLiveData>> = + MutableLiveData(emptyList()) + val rangeSelections: LiveData>> = _rangeSelections + + private val _seasonSelections: MutableLiveData>> = + MutableLiveData(emptyList()) + val seasonSelections: LiveData>> = _seasonSelections + + private val _recommendations: MutableLiveData> = + MutableLiveData(emptyList()) + val recommendations: LiveData> = _recommendations + + private val _selectedRange: MutableLiveData> = + MutableLiveData(Some.None) + val selectedRange: LiveData> = _selectedRange + + private val _selectedSeason: MutableLiveData> = + MutableLiveData(Some.None) + val selectedSeason: LiveData> = _selectedSeason + + private val _selectedDubStatus: MutableLiveData> = MutableLiveData(Some.None) + val selectedDubStatus: LiveData> = _selectedDubStatus + + private val _loadedLinks: MutableLiveData> = MutableLiveData(Some.None) + val loadedLinks: LiveData> = _loadedLinks + + private val _resumeWatching: MutableLiveData> = + MutableLiveData(Some.None) + val resumeWatching: LiveData> = _resumeWatching + + companion object { + const val TAG = "RVM2" + private const val EPISODE_RANGE_SIZE = 20 + private const val EPISODE_RANGE_OVERLOAD = 30 + + private fun filterName(name: String?): String? { + if (name == null) return null + Regex("[eE]pisode [0-9]*(.*)").find(name)?.groupValues?.get(1)?.let { + if (it.isEmpty()) + return null + } + return name + } + + fun singleMap(ep: ResultEpisode): Map> = + mapOf( + EpisodeIndexer(DubStatus.None, 0) to listOf( + ep + ) + ) + + private fun getRanges(allEpisodes: Map>): Map> { + return allEpisodes.keys.mapNotNull { index -> + val episodes = + allEpisodes[index] ?: return@mapNotNull null // this should never happened + + // fast case + if (episodes.size <= EPISODE_RANGE_OVERLOAD) { + return@mapNotNull index to listOf( + EpisodeRange( + 0, + episodes.size, + episodes.minOf { it.episode }, + episodes.maxOf { it.episode }) + ) + } + + if (episodes.isEmpty()) { + return@mapNotNull null + } + + val list = mutableListOf() + + val currentEpisode = episodes.first() + var currentIndex = 0 + val maxIndex = episodes.size + var targetEpisode = 0 + var currentMin = currentEpisode.episode + var currentMax = currentEpisode.episode + + while (currentIndex < maxIndex) { + val startIndex = currentIndex + targetEpisode += EPISODE_RANGE_SIZE + while (currentIndex < maxIndex && episodes[currentIndex].episode <= targetEpisode) { + val episodeNumber = episodes[currentIndex].episode + if (episodeNumber < currentMin) { + currentMin = episodeNumber + } else if (episodeNumber > currentMax) { + currentMax = episodeNumber + } + ++currentIndex + } + + val length = currentIndex - startIndex + if (length <= 0) continue + list.add( + EpisodeRange( + startIndex, + length, + currentMin, + currentMax + ) + ) + currentMin = Int.MAX_VALUE + currentMax = Int.MIN_VALUE + } + + /*var currentMin = Int.MAX_VALUE + var currentMax = Int.MIN_VALUE + var currentStartIndex = 0 + var currentLength = 0 + for (ep in episodes) { + val episodeNumber = ep.episode + if (episodeNumber < currentMin) { + currentMin = episodeNumber + } else if (episodeNumber > currentMax) { + currentMax = episodeNumber + } + + if (++currentLength >= EPISODE_RANGE_SIZE) { + list.add( + EpisodeRange( + currentStartIndex, + currentLength, + currentMin, + currentMax + ) + ) + currentMin = Int.MAX_VALUE + currentMax = Int.MIN_VALUE + currentStartIndex += currentLength + currentLength = 0 + } + } + if (currentLength > 0) { + list.add( + EpisodeRange( + currentStartIndex, + currentLength, + currentMin, + currentMax + ) + ) + }*/ + + index to list + }.toMap() + } + + private fun downloadSubtitle( + context: Context?, + link: ExtractorSubtitleLink, + fileName: String, + folder: String + ) { + ioSafe { + VideoDownloadManager.downloadThing( + context ?: return@ioSafe, + link, + "$fileName ${link.name}", + folder, + if (link.url.contains(".srt")) ".srt" else "vtt", + false, + null + ) { + // no notification + } + } + } + + private fun getFolder(currentType: TvType, titleName: String): String { + val sanitizedFileName = VideoDownloadManager.sanitizeFilename(titleName) + return when (currentType) { + TvType.Anime -> "Anime/$sanitizedFileName" + TvType.Movie -> "Movies" + TvType.AnimeMovie -> "Movies" + TvType.TvSeries -> "TVSeries/$sanitizedFileName" + TvType.OVA -> "OVA" + TvType.Cartoon -> "Cartoons/$sanitizedFileName" + TvType.Torrent -> "Torrent" + TvType.Documentary -> "Documentaries" + TvType.AsianDrama -> "AsianDrama" + TvType.Live -> "LiveStreams" + } + } + + private fun downloadSubtitle( + context: Context?, + link: SubtitleData, + meta: VideoDownloadManager.DownloadEpisodeMetadata, + ) { + context?.let { ctx -> + val fileName = VideoDownloadManager.getFileName(ctx, meta) + val folder = getFolder(meta.type ?: return, meta.mainName) + downloadSubtitle( + ctx, + ExtractorSubtitleLink(link.name, link.url, ""), + fileName, + folder + ) + } + } + + fun startDownload( + context: Context?, + episode: ResultEpisode, + currentIsMovie: Boolean, + currentHeaderName: String, + currentType: TvType, + currentPoster: String?, + apiName: String, + parentId: Int, + url: String, + links: List, + subs: List? + ) { + try { + if (context == null) return + + val meta = + getMeta( + episode, + currentHeaderName, + apiName, + currentPoster, + currentIsMovie, + currentType + ) + + val folder = getFolder(currentType, currentHeaderName) + + val src = "$DOWNLOAD_NAVIGATE_TO/$parentId" // url ?: return@let + + // SET VISUAL KEYS + AcraApplication.setKey( + DOWNLOAD_HEADER_CACHE, + parentId.toString(), + VideoDownloadHelper.DownloadHeaderCached( + apiName, + url, + currentType, + currentHeaderName, + currentPoster, + parentId, + System.currentTimeMillis(), + ) + ) + + AcraApplication.setKey( + DataStore.getFolderName( + DOWNLOAD_EPISODE_CACHE, + parentId.toString() + ), // 3 deep folder for faster acess + episode.id.toString(), + VideoDownloadHelper.DownloadEpisodeCached( + episode.name, + episode.poster, + episode.episode, + episode.season, + episode.id, + parentId, + episode.rating, + episode.description, + System.currentTimeMillis(), + ) + ) + + // DOWNLOAD VIDEO + VideoDownloadManager.downloadEpisodeUsingWorker( + context, + src,//url ?: return, + folder, + meta, + links + ) + + // 1. Checks if the lang should be downloaded + // 2. Makes it into the download format + // 3. Downloads it as a .vtt file + val downloadList = SubtitlesFragment.getDownloadSubsLanguageISO639_1() + subs?.let { subsList -> + subsList.filter { + downloadList.contains( + SubtitleHelper.fromLanguageToTwoLetters( + it.name, + true + ) + ) + } + .map { ExtractorSubtitleLink(it.name, it.url, "") } + .forEach { link -> + val fileName = VideoDownloadManager.getFileName(context, meta) + downloadSubtitle(context, link, fileName, folder) + } + } + } catch (e: Exception) { + logError(e) + } + } + + suspend fun downloadEpisode( + activity: Activity?, + episode: ResultEpisode, + currentIsMovie: Boolean, + currentHeaderName: String, + currentType: TvType, + currentPoster: String?, + apiName: String, + parentId: Int, + url: String, + ) { + ioSafe { + val generator = RepoLinkGenerator(listOf(episode)) + val currentLinks = mutableSetOf() + val currentSubs = mutableSetOf() + generator.generateLinks(clearCache = false, isCasting = false, callback = { + it.first?.let { link -> + currentLinks.add(link) + } + }, subtitleCallback = { sub -> + currentSubs.add(sub) + }) + + if (currentLinks.isEmpty()) { + Coroutines.main { + showToast( + activity, + R.string.no_links_found_toast, + Toast.LENGTH_SHORT + ) + } + return@ioSafe + } else { + Coroutines.main { + showToast( + activity, + R.string.download_started, + Toast.LENGTH_SHORT + ) + } + } + + startDownload( + activity, + episode, + currentIsMovie, + currentHeaderName, + currentType, + currentPoster, + apiName, + parentId, + url, + sortUrls(currentLinks), + sortSubs(currentSubs), + ) + } + } + + private fun getMeta( + episode: ResultEpisode, + titleName: String, + apiName: String, + currentPoster: String?, + currentIsMovie: Boolean, + tvType: TvType, + ): VideoDownloadManager.DownloadEpisodeMetadata { + return VideoDownloadManager.DownloadEpisodeMetadata( + episode.id, + VideoDownloadManager.sanitizeFilename(titleName), + apiName, + episode.poster ?: currentPoster, + episode.name, + if (currentIsMovie) null else episode.season, + if (currentIsMovie) null else episode.episode, + tvType, + ) + } + } + + private val _watchStatus: MutableLiveData = MutableLiveData(WatchType.NONE) + val watchStatus: LiveData get() = _watchStatus + + private val _selectPopup: MutableLiveData> = MutableLiveData(Some.None) + val selectPopup: LiveData> get() = _selectPopup + + fun updateWatchStatus(status: WatchType) { + val currentId = currentId ?: return + val resultPage = currentResponse ?: return + _watchStatus.postValue(status) + + DataStoreHelper.setResultWatchState(currentId, status.internalId) + val current = DataStoreHelper.getBookmarkedData(currentId) + val currentTime = System.currentTimeMillis() + DataStoreHelper.setBookmarkedData( + currentId, + DataStoreHelper.BookmarkedData( + currentId, + current?.bookmarkedTime ?: currentTime, + currentTime, + resultPage.name, + resultPage.url, + resultPage.apiName, + resultPage.type, + resultPage.posterUrl, + resultPage.year + ) + ) + } + + private fun startChromecast( + activity: Activity?, + result: ResultEpisode, + isVisible: Boolean = true + ) { + if (activity == null) return + loadLinks(result, isVisible = isVisible, isCasting = true) { data -> + startChromecast(activity, result, data.links, data.subs, 0) + } + } + + private fun startChromecast( + activity: Activity?, + result: ResultEpisode, + links: List, + subs: List, + startIndex: Int, + ) { + if (activity == null) return + val response = currentResponse ?: return + val eps = currentEpisodes[currentIndex ?: return] ?: return + + activity.getCastSession()?.startCast( + response.apiName, + response.isMovie(), + response.name, + response.posterUrl, + result.index, + eps, + links, + subs, + startTime = result.getRealPosition(), + startIndex = startIndex + ) + } + + fun cancelLinks() { + println("called::cancelLinks") + currentLoadLinkJob?.cancel() + currentLoadLinkJob = null + _loadedLinks.postValue(Some.None) + } + + private fun postPopup(text: UiText, options: List, callback: suspend (Int?) -> Unit) { + _selectPopup.postValue( + some(SelectPopup.SelectText( + text, + options + ) { value -> + viewModelScope.launch { + _selectPopup.postValue(Some.None) + callback.invoke(value) + } + }) + ) + } + + @JvmName("postPopupArray") + private fun postPopup( + text: UiText, + options: List>, + callback: suspend (Int?) -> Unit + ) { + _selectPopup.postValue( + some(SelectPopup.SelectArray( + text, + options, + ) { value -> + viewModelScope.launch { + _selectPopup.value = Some.None + callback.invoke(value) + } + }) + ) + } + + fun loadLinks( + result: ResultEpisode, + isVisible: Boolean, + isCasting: Boolean, + clearCache: Boolean = false, + work: suspend (CoroutineScope.(LinkLoadingResult) -> Unit) + ) { + currentLoadLinkJob?.cancel() + currentLoadLinkJob = ioSafe { + val links = loadLinks( + result, + isVisible = isVisible, + isCasting = isCasting, + clearCache = clearCache + ) + if (!this.isActive) return@ioSafe + work(links) + } + } + + private var currentLoadLinkJob: Job? = null + private fun acquireSingleLink( + result: ResultEpisode, + isCasting: Boolean, + text: UiText, + callback: (Pair) -> Unit, + ) { + loadLinks(result, isVisible = true, isCasting = isCasting) { links -> + postPopup( + text, + links.links.map { txt("${it.name} ${Qualities.getStringByInt(it.quality)}") }) { + callback.invoke(links to (it ?: return@postPopup)) + } + } + } + + private fun acquireSingleSubtitle( + result: ResultEpisode, + isCasting: Boolean, + text: UiText, + callback: (Pair) -> Unit, + ) { + loadLinks(result, isVisible = true, isCasting = isCasting) { links -> + postPopup( + text, + links.subs.map { txt(it.name) }) + { + callback.invoke(links to (it ?: return@postPopup)) + } + } + } + + suspend fun CoroutineScope.loadLinks( + result: ResultEpisode, + isVisible: Boolean, + isCasting: Boolean, + clearCache: Boolean = false, + ): LinkLoadingResult { + val tempGenerator = RepoLinkGenerator(listOf(result)) + + val links: MutableSet = mutableSetOf() + val subs: MutableSet = mutableSetOf() + fun updatePage() { + if (isVisible && isActive) { + _loadedLinks.postValue(some(LinkProgress(links.size, subs.size))) + } + } + try { + updatePage() + tempGenerator.generateLinks(clearCache, isCasting, { (link, _) -> + if (link != null) { + links += link + updatePage() + } + }, { sub -> + subs += sub + updatePage() + }) + } catch (e: Exception) { + logError(e) + } finally { + _loadedLinks.postValue(Some.None) + } + + return LinkLoadingResult(sortUrls(links), sortSubs(subs)) + } + + private fun playWithVlc(act: Activity?, data: LinkLoadingResult, id: Int) = ioSafe { + if (act == null) return@ioSafe + if (data.links.isEmpty()) { + showToast(act, R.string.no_links_found_toast, Toast.LENGTH_SHORT) + 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) + 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 -> { + val options = mutableListOf>() + if (activity?.isConnectedToChromecast() == true) { + options.addAll( + listOf( + txt(R.string.episode_action_chromecast_episode) to ACTION_CHROME_CAST_EPISODE, + txt(R.string.episode_action_chromecast_mirror) to ACTION_CHROME_CAST_MIRROR, + ) + ) + } + options.add(txt(R.string.episode_action_play_in_app) to ACTION_PLAY_EPISODE_IN_PLAYER) + + if (activity?.isAppInstalled(VLC_PACKAGE) == true) { + options.add(txt(R.string.episode_action_play_in_vlc) to ACTION_PLAY_EPISODE_IN_VLC_PLAYER) + } + options.addAll( + listOf( + txt(R.string.episode_action_play_in_browser) to ACTION_PLAY_EPISODE_IN_BROWSER, + txt(R.string.episode_action_copy_link) to ACTION_COPY_LINK, + txt(R.string.episode_action_auto_download) to ACTION_DOWNLOAD_EPISODE, + txt(R.string.episode_action_download_mirror) to ACTION_DOWNLOAD_MIRROR, + txt(R.string.episode_action_download_subtitle) to ACTION_DOWNLOAD_EPISODE_SUBTITLE_MIRROR, + txt(R.string.episode_action_reload_links) to ACTION_RELOAD_EPISODE, + ) + ) + + postPopup( + txt( + activity?.getNameFull( + click.data.name, + click.data.episode, + click.data.season + ) ?: "" + ), // TODO FIX + options + ) { result -> + handleEpisodeClickEvent( + activity, + click.copy(action = result ?: return@postPopup) + ) + } + } + 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) + ) + } + } + } + /* not implemented, not used + ACTION_DOWNLOAD_EPISODE_SUBTITLE -> { + loadLinks(click.data, isVisible = false, isCasting = false) { links -> + downloadSubtitle(activity,links.subs,) + } + }*/ + ACTION_DOWNLOAD_EPISODE_SUBTITLE_MIRROR -> { + 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 + ) + ) + showToast( + activity, + R.string.download_started, + Toast.LENGTH_SHORT + ) + } + } + ACTION_SHOW_TOAST -> { + 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) -> + ioSafe { + startDownload( + activity, + click.data, + response.isMovie(), + response.name, + response.type, + response.posterUrl, + response.apiName, + response.getId(), + response.url, + listOf(result.links[index]), + result.subs, + ) + } + showToast( + activity, + R.string.download_started, + Toast.LENGTH_SHORT + ) + } + } + ACTION_RELOAD_EPISODE -> { + ioSafe { + loadLinks( + click.data, + isVisible = false, + isCasting = false, + clearCache = true + ) + } + } + ACTION_CHROME_CAST_MIRROR -> { + acquireSingleLink( + click.data, + isCasting = true, + 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, + isCasting = true, + 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, + isCasting = true, + 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) + showToast(act, R.string.copy_link_toast, Toast.LENGTH_SHORT) + } + } + ACTION_CHROME_CAST_EPISODE -> { + startChromecast(activity, click.data) + } + ACTION_PLAY_EPISODE_IN_VLC_PLAYER -> { + loadLinks(click.data, isVisible = true, isCasting = true) { links -> + playWithVlc( + activity, + links, + click.data.id + ) + } + } + ACTION_PLAY_EPISODE_IN_PLAYER -> { + val data = currentResponse?.syncData?.toList() ?: emptyList() + val list = + HashMap().apply { putAll(data) } + + activity?.navigate( + R.id.global_to_navigation_player, + GeneratorPlayer.newInstance( + generator?.also { + it.getAll() // I know kinda shit to itterate all, but it is 100% sure to work + ?.indexOfFirst { value -> value is ResultEpisode && value.id == click.data.id } + ?.let { index -> + if (index > 0) + it.goto(index) + } + + } ?: return, list + ) + ) + } + } + } + + private suspend fun applyMeta( + resp: LoadResponse, + meta: SyncAPI.SyncResult?, + syncs: Map? = null + ): Pair { + if (meta == null) return resp to false + var updateEpisodes = false + val out = resp.apply { + Log.i(TAG, "applyMeta") + + duration = duration ?: meta.duration + rating = rating ?: meta.publicScore + tags = tags ?: meta.genres + plot = if (plot.isNullOrBlank()) meta.synopsis else plot + posterUrl = posterUrl ?: meta.posterUrl ?: meta.backgroundPosterUrl + actors = actors ?: meta.actors + + if (this is EpisodeResponse) { + nextAiring = nextAiring ?: meta.nextAiring + } + + for ((k, v) in syncs ?: emptyMap()) { + syncData[k] = v + } + + val realRecommendations = ArrayList() + val apiNames = listOf(GogoanimeProvider().name, NineAnimeProvider().name) + meta.recommendations?.forEach { rec -> + apiNames.forEach { name -> + realRecommendations.add(rec.copy(apiName = name)) + } + } + + recommendations = recommendations?.union(realRecommendations)?.toList() + ?: realRecommendations + + argamap({ + addTrailer(meta.trailers) + }, { + if (this !is AnimeLoadResponse) return@argamap + val map = + Kitsu.getEpisodesDetails(getMalId(), getAniListId(), isResponseRequired = false) + if (map.isNullOrEmpty()) return@argamap + updateEpisodes = DubStatus.values().map { dubStatus -> + val current = + this.episodes[dubStatus]?.mapIndexed { index, episode -> + episode.apply { + this.episode = this.episode ?: (index + 1) + } + }?.sortedBy { it.episode ?: 0 }?.toMutableList() + if (current.isNullOrEmpty()) return@map false + val episodeNumbers = current.map { ep -> ep.episode!! } + var updateCount = 0 + map.forEach { (episode, node) -> + episodeNumbers.binarySearch(episode).let { index -> + current.getOrNull(index)?.let { currentEp -> + current[index] = currentEp.apply { + updateCount++ + val currentBack = this + this.description = this.description ?: node.description?.en + this.name = this.name ?: node.titles?.canonical + this.episode = this.episode ?: node.num ?: episodeNumbers[index] + this.posterUrl = this.posterUrl ?: node.thumbnail?.original?.url + } + } + } + } + this.episodes[dubStatus] = current + updateCount > 0 + }.any { it } + }) + } + return out to updateEpisodes + } + + fun setMeta(meta: SyncAPI.SyncResult, syncs: Map?) { + // I dont want to update everything if the metadata is not relevant + if (currentMeta == meta && currentSync == syncs) { + Log.i(TAG, "setMeta same") + return + } + Log.i(TAG, "setMeta") + viewModelScope.launch { + currentMeta = meta + currentSync = syncs + val (value, updateEpisodes) = ioWork { + currentResponse?.let { resp -> + return@ioWork applyMeta(resp, meta, syncs) + } + return@ioWork null to null + } + + postSuccessful( + value ?: return@launch, + currentRepo ?: return@launch, + updateEpisodes ?: return@launch, + false + ) + } + } + + + private suspend fun updateFillers(name: String) { + fillers = + ioWork { + try { + FillerEpisodeCheck.getFillerEpisodes(name) + } catch (e: Exception) { + logError(e) + null + } + } ?: emptyMap() + } + + fun changeDubStatus(status: DubStatus) { + postEpisodeRange(currentIndex?.copy(dubStatus = status), currentRange) + } + + fun changeRange(range: EpisodeRange) { + postEpisodeRange(currentIndex, range) + } + + fun changeSeason(season: Int) { + postEpisodeRange(currentIndex?.copy(season = season), currentRange) + } + + private fun getMovie(): ResultEpisode? { + return currentEpisodes.entries.firstOrNull()?.value?.firstOrNull()?.let { ep -> + val posDur = getViewPos(ep.id) + ep.copy(position = posDur?.position ?: 0, duration = posDur?.duration ?: 0) + } + } + + private fun getEpisodes(indexer: EpisodeIndexer, range: EpisodeRange): List { + val startIndex = range.startIndex + val length = range.length + + return currentEpisodes[indexer] + ?.let { list -> + val start = minOf(list.size, startIndex) + val end = minOf(list.size, start + length) + list.subList(start, end).map { + val posDur = getViewPos(it.id) + it.copy(position = posDur?.position ?: 0, duration = posDur?.duration ?: 0) + } + } + ?: emptyList() + } + + private fun postMovie() { + val response = currentResponse + _episodes.postValue(ResourceSome.None) + + if (response == null) { + _movie.postValue(ResourceSome.None) + return + } + + val text = txt( + when (response.type) { + TvType.Torrent -> R.string.play_torrent_button + else -> { + if (response.type.isLiveStream()) + R.string.play_livestream_button + else if (response.type.isMovieType()) // this wont break compatibility as you only need to override isMovieType + R.string.play_movie_button + else null + } + } + ) + val data = getMovie() + _episodes.postValue(ResourceSome.None) + if (text == null || data == null) { + _movie.postValue(ResourceSome.None) + } else { + _movie.postValue(ResourceSome.Success(text to data)) + } + } + + fun reloadEpisodes() { + if (currentResponse?.isMovie() == true) { + postMovie() + } else { + _episodes.postValue( + ResourceSome.Success( + getEpisodes( + currentIndex ?: return, + currentRange ?: return + ) + ) + ) + _movie.postValue(ResourceSome.None) + } + postResume() + } + + private fun postEpisodeRange(indexer: EpisodeIndexer?, range: EpisodeRange?) { + if (range == null || indexer == null) { + return + } + + val episodes = currentEpisodes[indexer] + val ranges = currentRanges[indexer] + val size = episodes?.size + val isMovie = currentResponse?.isMovie() == true + currentIndex = indexer + currentRange = range + + _rangeSelections.postValue(ranges?.map { r -> + val text = txt(R.string.episodes_range, r.startEpisode, r.endEpisode) + text to r + } ?: emptyList()) + + _episodesCountText.postValue( + some( + if (isMovie) null else + txt( + R.string.episode_format, + size, + txt(if (size == 1) R.string.episode else R.string.episodes), + ) + ) + ) + + _selectedSeason.postValue( + some( + if (isMovie || currentSeasons.size <= 1) null else + when (indexer.season) { + 0 -> txt(R.string.no_season) + else -> txt( + R.string.season_format, + txt(R.string.season), + indexer.season + ) //TODO FIX DISPLAYNAME + } + ) + ) + + _selectedRange.postValue( + some( + if (isMovie) null else if ((currentRanges[indexer]?.size ?: 0) > 1) { + txt(R.string.episodes_range, range.startEpisode, range.endEpisode) + } else { + null + } + ) + ) + _selectedDubStatus.postValue( + some( + if (isMovie || currentDubStatus.size <= 1) null else + txt(indexer.dubStatus) + ) + ) + + currentId?.let { id -> + setDub(id, indexer.dubStatus) + setResultSeason(id, indexer.season) + setResultEpisode(id, range.startEpisode) + } + + preferStartEpisode = range.startEpisode + preferStartSeason = indexer.season + preferDubStatus = indexer.dubStatus + + generator = if (isMovie) { + getMovie()?.let { RepoLinkGenerator(listOf(it)) } + } else { + episodes?.let { list -> + RepoLinkGenerator(list) + } + } + + if (isMovie) { + postMovie() + } else { + val ret = getEpisodes(indexer, range) + _episodes.postValue(ResourceSome.Success(ret)) + } + } + + private suspend fun postSuccessful( + loadResponse: LoadResponse, + apiRepository: APIRepository, + updateEpisodes: Boolean, + updateFillers: Boolean, + ) { + currentResponse = loadResponse + postPage(loadResponse, apiRepository) + if (updateEpisodes) + postEpisodes(loadResponse, updateFillers) + } + + private suspend fun postEpisodes(loadResponse: LoadResponse, updateFillers: Boolean) { + _episodes.postValue(ResourceSome.Loading()) + + val mainId = loadResponse.getId() + currentId = mainId + + _watchStatus.postValue(getResultWatchState(mainId)) + + if (updateFillers && loadResponse is AnimeLoadResponse) { + updateFillers(loadResponse.name) + } + + val allEpisodes = when (loadResponse) { + is AnimeLoadResponse -> { + val existingEpisodes = HashSet() + val episodes: MutableMap> = + mutableMapOf() + loadResponse.episodes.map { ep -> + val idIndex = ep.key.id + for ((index, i) in ep.value.withIndex()) { + val episode = i.episode ?: (index + 1) + val id = mainId + episode + idIndex * 1000000 + if (!existingEpisodes.contains(episode)) { + existingEpisodes.add(id) + val eps = + buildResultEpisode( + loadResponse.name, + filterName(i.name), + i.posterUrl, + episode, + null, + i.season, + i.data, + loadResponse.apiName, + id, + index, + i.rating, + i.description, + fillers.getOrDefault(episode, false), + loadResponse.type, + mainId + ) + + val season = eps.season ?: 0 + val indexer = EpisodeIndexer(ep.key, season) + episodes[indexer]?.add(eps) ?: run { + episodes[indexer] = mutableListOf(eps) + } + } + } + } + episodes + } + is TvSeriesLoadResponse -> { + val episodes: MutableMap> = + mutableMapOf() + val existingEpisodes = HashSet() + for ((index, episode) in loadResponse.episodes.sortedBy { + (it.season?.times(10000) ?: 0) + (it.episode ?: 0) + }.withIndex()) { + val episodeIndex = episode.episode ?: (index + 1) + val id = + mainId + (episode.season?.times(100000) ?: 0) + episodeIndex + 1 + if (!existingEpisodes.contains(id)) { + existingEpisodes.add(id) + val seasonIndex = episode.season?.minus(1) + val currentSeason = + loadResponse.seasonNames?.getOrNull(seasonIndex ?: -1) + + val ep = + buildResultEpisode( + loadResponse.name, + filterName(episode.name), + episode.posterUrl, + episodeIndex, + seasonIndex, + currentSeason?.season ?: episode.season, + episode.data, + loadResponse.apiName, + id, + index, + episode.rating, + episode.description, + null, + loadResponse.type, + mainId + ) + + val season = episode.season ?: 0 + val indexer = EpisodeIndexer(DubStatus.None, season) + + episodes[indexer]?.add(ep) ?: kotlin.run { + episodes[indexer] = mutableListOf(ep) + } + } + } + episodes + } + is MovieLoadResponse -> { + singleMap( + buildResultEpisode( + loadResponse.name, + loadResponse.name, + null, + 0, + null, + null, + loadResponse.dataUrl, + loadResponse.apiName, + (mainId), // HAS SAME ID + 0, + null, + null, + null, + loadResponse.type, + mainId + ) + ) + } + is LiveStreamLoadResponse -> { + singleMap( + buildResultEpisode( + loadResponse.name, + loadResponse.name, + null, + 0, + null, + null, + loadResponse.dataUrl, + loadResponse.apiName, + (mainId), // HAS SAME ID + 0, + null, + null, + null, + loadResponse.type, + mainId + ) + ) + } + is TorrentLoadResponse -> { + singleMap( + buildResultEpisode( + loadResponse.name, + loadResponse.name, + null, + 0, + null, + null, + loadResponse.torrent ?: loadResponse.magnet ?: "", + loadResponse.apiName, + (mainId), // HAS SAME ID + 0, + null, + null, + null, + loadResponse.type, + mainId + ) + ) + } + else -> { + mapOf() + } + } + + val seasonsSelection = mutableSetOf() + val dubSelection = mutableSetOf() + allEpisodes.keys.forEach { key -> + seasonsSelection += key.season + dubSelection += key.dubStatus + } + currentDubStatus = dubSelection + currentSeasons = seasonsSelection + _dubSubSelections.postValue(dubSelection.map { txt(it) to it }) + if (loadResponse is EpisodeResponse) { + _seasonSelections.postValue(seasonsSelection.map { seasonNumber -> + val name = + /*loadResponse.seasonNames?.firstOrNull { it.season == seasonNumber }?.name?.let { seasonData -> + txt(seasonData) + } ?:*/txt(R.string.season_format, txt(R.string.season), seasonNumber) //TODO FIX + name to seasonNumber + }) + } + + currentEpisodes = allEpisodes + val ranges = getRanges(allEpisodes) + currentRanges = ranges + + + // this takes the indexer most preferable by the user given the current sorting + val min = ranges.keys.minByOrNull { index -> + kotlin.math.abs( + index.season - (preferStartSeason ?: 0) + ) + if (index.dubStatus == preferDubStatus) 0 else 100000 + } + + // this takes the range most preferable by the user given the current sorting + val ranger = ranges[min] + val range = ranger?.firstOrNull { + it.startEpisode >= (preferStartEpisode ?: 0) + } ?: ranger?.lastOrNull() + + postEpisodeRange(min, range) + postResume() + } + + fun postResume() { + _resumeWatching.postValue(some(resume())) + } + + private fun resume(): ResumeWatchingStatus? { + val correctId = currentId ?: return null + val resume = DataStoreHelper.getLastWatched(correctId) + val resumeParentId = resume?.parentId + if (resumeParentId != correctId) return null // is null or smth went wrong with getLastWatched + val resumeId = resume.episodeId ?: return null// invalid episode id + val response = currentResponse ?: return null + // kinda ugly ik + val episode = + currentEpisodes.values.flatten().firstOrNull { it.id == resumeId } ?: return null + + val isMovie = response.isMovie() + + val progress = getViewPos(resume.episodeId)?.let { viewPos -> + ResumeProgress( + progress = (viewPos.position / 1000).toInt(), + maxProgress = (viewPos.duration / 1000).toInt(), + txt(R.string.resume_time_left, (viewPos.duration - viewPos.position) / (60_000)) + ) + } + + return ResumeWatchingStatus(progress = progress, isMovie = isMovie, result = episode) + } + + + // this instantly updates the metadata on the page + private fun postPage(loadResponse: LoadResponse, apiRepository: APIRepository) { + _recommendations.postValue(loadResponse.recommendations ?: emptyList()) + _page.postValue(Resource.Success(loadResponse.toResultData(apiRepository))) + _trailers.postValue(loadResponse.trailers) + } + + fun hasLoaded() = currentResponse != null + + private fun handleAutoStart(activity: Activity?, autostart: AutoResume?) = + viewModelScope.launch { + if (autostart == null || activity == null) return@launch + + when (autostart.startAction) { + START_ACTION_RESUME_LATEST -> { + currentEpisodes[currentIndex]?.let { currentRange -> + for (ep in currentRange) { + if (ep.getWatchProgress() > 0.9) continue + handleAction( + activity, + EpisodeClickEvent(ACTION_PLAY_EPISODE_IN_PLAYER, ep) + ) + break + } + } + } + START_ACTION_LOAD_EP -> { + val all = currentEpisodes.values.flatten() + val episode = + autostart.id?.let { id -> all.firstOrNull { it.id == id } } + ?: autostart.episode?.let { ep -> + currentEpisodes[currentIndex]?.firstOrNull { it.episode == ep && it.season == autostart.episode } + ?: all.firstOrNull { it.episode == ep && it.season == autostart.episode } + } + ?: return@launch + handleAction( + activity, + EpisodeClickEvent(ACTION_PLAY_EPISODE_IN_PLAYER, episode) + ) + } + } + } + + fun load( + activity: Activity?, + url: String, + apiName: String, + showFillers: Boolean, + dubStatus: DubStatus, + autostart: AutoResume?, + ) = + viewModelScope.launch { + _page.postValue(Resource.Loading(url)) + _episodes.postValue(ResourceSome.Loading()) + + preferDubStatus = dubStatus + currentShowFillers = showFillers + + // set api + val api = APIHolder.getApiFromNameNull(apiName) ?: APIHolder.getApiFromUrlNull(url) + if (api == null) { + _page.postValue( + Resource.Failure( + false, + null, + null, + "This provider does not exist" + ) + ) + return@launch + } + + + // validate url + val validUrlResource = safeApiCall { + SyncRedirector.redirect( + url, + api.mainUrl.replace(NineAnimeProvider().mainUrl, "9anime") + .replace(GogoanimeProvider().mainUrl, "gogoanime") + ) + } + if (validUrlResource !is Resource.Success) { + if (validUrlResource is Resource.Failure) { + _page.postValue(validUrlResource) + } + + return@launch + } + val validUrl = validUrlResource.value + val repo = APIRepository(api) + currentRepo = repo + + when (val data = repo.load(validUrl)) { + is Resource.Failure -> { + _page.postValue(data) + } + is Resource.Success -> { + if (!isActive) return@launch + val loadResponse = ioWork { + applyMeta(data.value, currentMeta, currentSync).first + } + if (!isActive) return@launch + val mainId = loadResponse.getId() + + preferDubStatus = getDub(mainId) ?: preferDubStatus + preferStartEpisode = getResultEpisode(mainId) + preferStartSeason = getResultSeason(mainId) + + AcraApplication.setKey( + DOWNLOAD_HEADER_CACHE, + mainId.toString(), + VideoDownloadHelper.DownloadHeaderCached( + apiName, + validUrl, + loadResponse.type, + loadResponse.name, + loadResponse.posterUrl, + mainId, + System.currentTimeMillis(), + ) + ) + + postSuccessful( + data.value, + updateEpisodes = true, + updateFillers = showFillers, + apiRepository = repo + ) + if (!isActive) return@launch + handleAutoStart(activity, autostart) + } + is Resource.Loading -> { + debugException { "Invalid load result" } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt index 097da927..b99d92f7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt @@ -4,7 +4,6 @@ import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import com.lagradost.cloudstream3.apmap import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.logError @@ -12,8 +11,8 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi import com.lagradost.cloudstream3.syncproviders.SyncAPI +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.SyncUtil -import kotlinx.coroutines.launch import java.util.* @@ -44,9 +43,13 @@ class SyncViewModel : ViewModel() { // prefix, id private var syncs = mutableMapOf() - private val _syncIds: MutableLiveData> = - MutableLiveData(mutableMapOf()) - val syncIds: LiveData> get() = _syncIds + //private val _syncIds: MutableLiveData> = + // MutableLiveData(mutableMapOf()) + //val syncIds: LiveData> get() = _syncIds + + fun getSyncs() : Map { + return syncs + } private val _currentSynced: MutableLiveData> = MutableLiveData(getMissing()) @@ -76,7 +79,7 @@ class SyncViewModel : ViewModel() { Log.i(TAG, "addSync $idPrefix = $id") syncs[idPrefix] = id - _syncIds.postValue(syncs) + //_syncIds.postValue(syncs) return true } @@ -99,10 +102,10 @@ class SyncViewModel : ViewModel() { var hasAddedFromUrl: HashSet = hashSetOf() - fun addFromUrl(url: String?) = viewModelScope.launch { + fun addFromUrl(url: String?) = ioSafe { Log.i(TAG, "addFromUrl = $url") - if (url == null || hasAddedFromUrl.contains(url)) return@launch + if (url == null || hasAddedFromUrl.contains(url)) return@ioSafe SyncUtil.getIdsFromUrl(url)?.let { (malId, aniListId) -> hasAddedFromUrl.add(url) @@ -166,7 +169,7 @@ class SyncViewModel : ViewModel() { } } - fun publishUserData() = viewModelScope.launch { + fun publishUserData() = ioSafe { Log.i(TAG, "publishUserData") val user = userData.value if (user is Resource.Success) { @@ -191,7 +194,7 @@ class SyncViewModel : ViewModel() { /// modifies the current sync data, return null if you don't want to change it private fun modifyData(update: ((SyncAPI.SyncStatus) -> (SyncAPI.SyncStatus?))) = - viewModelScope.launch { + ioSafe { syncs.apmap { (prefix, id) -> repos.firstOrNull { it.idPrefix == prefix }?.let { repo -> if (repo.hasAccount()) { @@ -209,7 +212,7 @@ class SyncViewModel : ViewModel() { } } - fun updateUserData() = viewModelScope.launch { + fun updateUserData() = ioSafe { Log.i(TAG, "updateUserData") _userDataResponse.postValue(Resource.Loading()) var lastError: Resource = Resource.Failure(false, null, null, "No data") @@ -219,7 +222,7 @@ class SyncViewModel : ViewModel() { val result = repo.getStatus(id) if (result is Resource.Success) { _userDataResponse.postValue(result) - return@launch + return@ioSafe } else if (result is Resource.Failure) { Log.e(TAG, "updateUserData error ${result.errorString}") lastError = result @@ -230,7 +233,7 @@ class SyncViewModel : ViewModel() { _userDataResponse.postValue(lastError) } - private fun updateMetadata() = viewModelScope.launch { + private fun updateMetadata() = ioSafe { Log.i(TAG, "updateMetadata") _metaResponse.postValue(Resource.Loading()) @@ -253,7 +256,7 @@ class SyncViewModel : ViewModel() { val result = repo.getResult(id) if (result is Resource.Success) { _metaResponse.postValue(result) - return@launch + return@ioSafe } else if (result is Resource.Failure) { Log.e( TAG, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt new file mode 100644 index 00000000..6bd7ee62 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt @@ -0,0 +1,163 @@ +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 +import androidx.annotation.StringRes +import androidx.core.view.isGone +import androidx.core.view.isVisible +import com.lagradost.cloudstream3.mvvm.Some +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.utils.AppUtils.html +import com.lagradost.cloudstream3.utils.UIHelper.setImage + +sealed class 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, + val args: List + ) : UiText() { + override fun toString(): String = "resId = $resId\nargs = ${args.toList().map { "(${it::class} = $it)" }}" + } + + fun asStringNull(context: Context?): String? { + try { + return asString(context ?: return null) + } catch (e: Exception) { + Log.e(TAG, "Got invalid data from $this") + logError(e) + return null + } + } + + fun asString(context: Context): String { + return when (this) { + is DynamicString -> value + 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()) + } + } + } + } +} + +sealed class UiImage { + data class Image( + val url: String, + val headers: Map? = null, + @DrawableRes val errorDrawable: Int? = null + ) : UiImage() + + data class Drawable(@DrawableRes val resId: Int) : UiImage() +} + +fun ImageView?.setImage(value: UiImage?) { + when (value) { + is UiImage.Image -> setImageImage(value) + is UiImage.Drawable -> setImageDrawable(value) + null -> { + this?.isVisible = false + } + } +} + +fun ImageView?.setImageImage(value: UiImage.Image) { + if (this == null) return + this.isVisible = setImage(value.url, value.headers, value.errorDrawable) +} + +fun ImageView?.setImageDrawable(value: UiImage.Drawable) { + if (this == null) return + this.isVisible = true + setImageResource(value.resId) +} + +@JvmName("imgNull") +fun img( + url: String?, + headers: Map? = null, + @DrawableRes errorDrawable: Int? = null +): UiImage? { + if (url.isNullOrBlank()) return null + return UiImage.Image(url, headers, errorDrawable) +} + +fun img( + url: String, + headers: Map? = null, + @DrawableRes errorDrawable: Int? = null +): UiImage { + return UiImage.Image(url, headers, errorDrawable) +} + +fun img(@DrawableRes drawable: Int): UiImage { + return UiImage.Drawable(drawable) +} + +fun txt(value: String): UiText { + return UiText.DynamicString(value) +} + +@JvmName("txtNull") +fun txt(value: String?): UiText? { + return UiText.DynamicString(value ?: return null) +} + +fun txt(@StringRes resId: Int, vararg args: Any): UiText { + return UiText.StringResource(resId, args.toList()) +} + +@JvmName("txtNull") +fun txt(@StringRes resId: Int?, vararg args: Any?): UiText? { + if (resId == null || args.any { it == null }) { + return null + } + return UiText.StringResource(resId, args.filterNotNull().toList()) +} + +fun TextView?.setText(text: UiText?) { + if (this == null) return + if (text == null) { + this.isVisible = false + } else { + val str = text.asStringNull(context) + this.isGone = str.isNullOrBlank() + this.text = str + } +} + +fun TextView?.setTextHtml(text: UiText?) { + if (this == null) return + if (text == null) { + this.isVisible = false + } else { + val str = text.asStringNull(context) + this.isGone = str.isNullOrBlank() + this.text = str.html() + } +} + +fun TextView?.setTextHtml(text: Some?) { + setTextHtml(if(text is Some.Success) text.value else null) +} + +fun TextView?.setText(text: Some?) { + setText(if(text is Some.Success) text.value else null) +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt index adfe151e..1de89809 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt @@ -27,7 +27,7 @@ object SearchHelper { } else { if (card.isFromDownload) { handleDownloadClick( - activity, card.name, DownloadClickEvent( + activity, DownloadClickEvent( DOWNLOAD_ACTION_PLAY_FILE, VideoDownloadHelper.DownloadEpisodeCached( card.name, diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt index 460bb919..79061e24 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt @@ -187,21 +187,21 @@ object AppUtils { @WorkerThread fun Context.addProgramsToContinueWatching(data: List) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return - + val context = this ioSafe { data.forEach { episodeInfo -> try { - val (program, id) = getWatchNextProgramByVideoId(episodeInfo.url, this) - val nextProgram = buildWatchNextProgramUri(this, episodeInfo) + val (program, id) = getWatchNextProgramByVideoId(episodeInfo.url, context) + val nextProgram = buildWatchNextProgramUri(context, episodeInfo) // If the program is already in the Watch Next row, update it if (program != null && id != null) { - PreviewChannelHelper(this).updateWatchNextProgram( + PreviewChannelHelper(context).updateWatchNextProgram( nextProgram, id, ) } else { - PreviewChannelHelper(this) + PreviewChannelHelper(context) .publishWatchNextProgram(nextProgram) } } catch (e: Exception) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/Coroutines.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/Coroutines.kt index d5cb06f7..978b2720 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/Coroutines.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/Coroutines.kt @@ -12,7 +12,7 @@ object Coroutines { } } - fun ioSafe(work: suspend (() -> Unit)): Job { + fun ioSafe(work: suspend (CoroutineScope.() -> Unit)): Job { return CoroutineScope(Dispatchers.IO).launch { try { work() @@ -22,7 +22,7 @@ object Coroutines { } } - suspend fun ioWork(work: suspend (() -> T)): T { + suspend fun ioWork(work: suspend (CoroutineScope.() -> T)): T { return withContext(Dispatchers.IO) { work() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt index 9367189b..46c29e3f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -18,6 +18,7 @@ const val RESULT_WATCH_STATE_DATA = "result_watch_state_data" const val RESULT_RESUME_WATCHING = "result_resume_watching_2" // changed due to id changes const val RESULT_RESUME_WATCHING_OLD = "result_resume_watching" const val RESULT_RESUME_WATCHING_HAS_MIGRATED = "result_resume_watching_migrated" +const val RESULT_EPISODE = "result_episode" const val RESULT_SEASON = "result_season" const val RESULT_DUB = "result_dub" @@ -163,7 +164,7 @@ object DataStoreHelper { ) } - fun getLastWatchedOld(id: Int?): VideoDownloadHelper.ResumeWatching? { + private fun getLastWatchedOld(id: Int?): VideoDownloadHelper.ResumeWatching? { if (id == null) return null return getKey( "$currentAccount/$RESULT_RESUME_WATCHING_OLD", @@ -192,8 +193,9 @@ object DataStoreHelper { return getKey("$currentAccount/$VIDEO_POS_DUR", id.toString(), null) } - fun getDub(id: Int): DubStatus { - return DubStatus.values()[getKey("$currentAccount/$RESULT_DUB", id.toString()) ?: 0] + fun getDub(id: Int): DubStatus? { + return DubStatus.values() + .getOrNull(getKey("$currentAccount/$RESULT_DUB", id.toString(), -1) ?: -1) } fun setDub(id: Int, status: DubStatus) { @@ -221,14 +223,22 @@ object DataStoreHelper { ) } - fun getResultSeason(id: Int): Int { - return getKey("$currentAccount/$RESULT_SEASON", id.toString()) ?: -1 + fun getResultSeason(id: Int): Int? { + return getKey("$currentAccount/$RESULT_SEASON", id.toString(), null) } fun setResultSeason(id: Int, value: Int?) { setKey("$currentAccount/$RESULT_SEASON", id.toString(), value) } + fun getResultEpisode(id: Int): Int? { + return getKey("$currentAccount/$RESULT_EPISODE", id.toString(), null) + } + + fun setResultEpisode(id: Int, value: Int?) { + setKey("$currentAccount/$RESULT_EPISODE", id.toString(), value) + } + fun addSync(id: Int, idPrefix: String, url: String) { setKey("${idPrefix}_sync", id.toString(), url) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt index 814cf95b..b228fe77 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt @@ -12,6 +12,9 @@ import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSet import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes import com.lagradost.cloudstream3.utils.UIHelper.setImage +import kotlinx.android.synthetic.main.add_account_input.* +import kotlinx.android.synthetic.main.add_account_input.text1 +import kotlinx.android.synthetic.main.bottom_selection_dialog_direct.* object SingleSelectionHelper { fun Activity?.showOptionSelectStringRes( @@ -21,7 +24,7 @@ object SingleSelectionHelper { tvOptions: List = listOf(), callback: (Pair) -> Unit ) { - if(this == null) return + if (this == null) return this.showOptionSelect( view, @@ -39,7 +42,7 @@ object SingleSelectionHelper { tvOptions: List, callback: (Pair) -> Unit ) { - if(this == null) return + if (this == null) return if (this.isTvSettings()) { val builder = @@ -86,42 +89,44 @@ object SingleSelectionHelper { showApply: Boolean, isMultiSelect: Boolean, callback: (List) -> Unit, - dismissCallback: () -> Unit + dismissCallback: () -> Unit, + itemLayout: Int = R.layout.sort_bottom_single_choice ) { - if(this == null) return + if (this == null) return val realShowApply = showApply || isMultiSelect - val listView = dialog.findViewById(R.id.listview1)!! - val textView = dialog.findViewById(R.id.text1)!! - val applyButton = dialog.findViewById(R.id.apply_btt)!! - val cancelButton = dialog.findViewById(R.id.cancel_btt)!! - val applyHolder = dialog.findViewById(R.id.apply_btt_holder)!! + val listView = dialog.listview1//.findViewById(R.id.listview1)!! + val textView = dialog.text1//.findViewById(R.id.text1)!! + val applyButton = dialog.apply_btt//.findViewById(R.id.apply_btt) + val cancelButton = dialog.cancel_btt//findViewById(R.id.cancel_btt) + val applyHolder = dialog.apply_btt_holder//.findViewById(R.id.apply_btt_holder) - applyHolder.isVisible = realShowApply + applyHolder?.isVisible = realShowApply if (!realShowApply) { val params = listView.layoutParams as LinearLayout.LayoutParams params.setMargins(listView.marginLeft, listView.marginTop, listView.marginRight, 0) listView.layoutParams = params } - textView.text = name + textView?.text = name + textView?.isGone = name.isBlank() - val arrayAdapter = ArrayAdapter(this, R.layout.sort_bottom_single_choice) + val arrayAdapter = ArrayAdapter(this, itemLayout) arrayAdapter.addAll(items) - listView.adapter = arrayAdapter + listView?.adapter = arrayAdapter if (isMultiSelect) { - listView.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE + listView?.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE } else { - listView.choiceMode = AbsListView.CHOICE_MODE_SINGLE + listView?.choiceMode = AbsListView.CHOICE_MODE_SINGLE } for (select in selectedIndex) { - listView.setItemChecked(select, true) + listView?.setItemChecked(select, true) } selectedIndex.minOrNull()?.let { - listView.setSelection(it) + listView?.setSelection(it) } // var lastSelectedIndex = if(selectedIndex.isNotEmpty()) selectedIndex.first() else -1 @@ -130,7 +135,7 @@ object SingleSelectionHelper { dismissCallback.invoke() } - listView.setOnItemClickListener { _, _, which, _ -> + listView?.setOnItemClickListener { _, _, which, _ -> // lastSelectedIndex = which if (realShowApply) { if (!isMultiSelect) { @@ -142,7 +147,7 @@ object SingleSelectionHelper { } } if (realShowApply) { - applyButton.setOnClickListener { + applyButton?.setOnClickListener { val list = ArrayList() for (index in 0 until listView.count) { if (listView.checkedItemPositions[index]) @@ -151,7 +156,7 @@ object SingleSelectionHelper { callback.invoke(list) dialog.dismissSafe(this) } - cancelButton.setOnClickListener { + cancelButton?.setOnClickListener { dialog.dismissSafe(this) } } @@ -166,7 +171,7 @@ object SingleSelectionHelper { callback: (String) -> Unit, dismissCallback: () -> Unit ) { - if(this == null) return + if (this == null) return val inputView = dialog.findViewById(R.id.nginx_text_input)!! val textView = dialog.findViewById(R.id.text1)!! @@ -205,7 +210,7 @@ object SingleSelectionHelper { dismissCallback: () -> Unit, callback: (List) -> Unit, ) { - if(this == null) return + if (this == null) return val builder = AlertDialog.Builder(this, R.style.AlertDialogCustom) @@ -224,7 +229,7 @@ object SingleSelectionHelper { dismissCallback: () -> Unit, callback: (Int) -> Unit, ) { - if(this == null) return + if (this == null) return val builder = AlertDialog.Builder(this, R.style.AlertDialogCustom) @@ -271,6 +276,31 @@ object SingleSelectionHelper { ) } + fun Activity.showBottomDialogInstant( + items: List, + name: String, + dismissCallback: () -> Unit, + callback: (Int) -> Unit, + ): BottomSheetDialog { + val builder = + BottomSheetDialog(this) + builder.setContentView(R.layout.bottom_selection_dialog_direct) + + builder.show() + showDialog( + builder, + items, + listOf(), + name, + showApply = false, + isMultiSelect = false, + callback = { if (it.isNotEmpty()) callback.invoke(it.first()) }, + dismissCallback = dismissCallback, + itemLayout = R.layout.sort_bottom_single_choice_no_checkmark + ) + return builder + } + fun Activity.showNginxTextInputDialog( name: String, value: String, diff --git a/app/src/main/res/layout/bottom_loading.xml b/app/src/main/res/layout/bottom_loading.xml new file mode 100644 index 00000000..ab05889d --- /dev/null +++ b/app/src/main/res/layout/bottom_loading.xml @@ -0,0 +1,33 @@ + + + + + + + + diff --git a/app/src/main/res/layout/bottom_selection_dialog_direct.xml b/app/src/main/res/layout/bottom_selection_dialog_direct.xml new file mode 100644 index 00000000..0d179ebb --- /dev/null +++ b/app/src/main/res/layout/bottom_selection_dialog_direct.xml @@ -0,0 +1,34 @@ + + + + + + + diff --git a/app/src/main/res/layout/dialog_loading.xml b/app/src/main/res/layout/dialog_loading.xml index 5640b880..02669665 100644 --- a/app/src/main/res/layout/dialog_loading.xml +++ b/app/src/main/res/layout/dialog_loading.xml @@ -12,7 +12,7 @@ @@ -290,15 +291,15 @@ + diff --git a/app/src/main/res/layout/fragment_result_swipe.xml b/app/src/main/res/layout/fragment_result_swipe.xml index 668e413b..53b8fbd9 100644 --- a/app/src/main/res/layout/fragment_result_swipe.xml +++ b/app/src/main/res/layout/fragment_result_swipe.xml @@ -2,6 +2,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:orientation="vertical" + style="@style/AlertDialogCustom" android:layout_width="match_parent" android:layout_height="match_parent"> diff --git a/app/src/main/res/layout/quick_search.xml b/app/src/main/res/layout/quick_search.xml index 3e19f05a..eeec4a5c 100644 --- a/app/src/main/res/layout/quick_search.xml +++ b/app/src/main/res/layout/quick_search.xml @@ -1,6 +1,7 @@ \ No newline at end of file diff --git a/app/src/main/res/layout/sort_bottom_single_choice_no_checkmark.xml b/app/src/main/res/layout/sort_bottom_single_choice_no_checkmark.xml new file mode 100644 index 00000000..0938ad1f --- /dev/null +++ b/app/src/main/res/layout/sort_bottom_single_choice_no_checkmark.xml @@ -0,0 +1,22 @@ + + + diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 50981dd3..2eaa9af5 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -29,7 +29,7 @@ شارك فتح في الويب تخطي التحميل - …تحميل + …تحميل مشاهدة في الانتظار diff --git a/app/src/main/res/values-bp/strings.xml b/app/src/main/res/values-bp/strings.xml index 39fd2212..b64ea0e4 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -108,7 +108,7 @@ Compartilhar Abrir no Navegador Pular Carregamento - Carregando… + Carregando… Assistindo Em espera diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 1cd6460d..2fefb15f 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -101,7 +101,7 @@ Sdílet Otevřít v prohlížeči Přeskočit načítání - Načítání… + Načítání… Sledování Pozastaveno diff --git a/app/src/main/res/values-de/strings-de.xml b/app/src/main/res/values-de/strings-de.xml index 0850cdda..c1a919c6 100644 --- a/app/src/main/res/values-de/strings-de.xml +++ b/app/src/main/res/values-de/strings-de.xml @@ -24,7 +24,7 @@ Teilen Im Browser öffnen Buffern überspringen - Lädt… + Lädt… Am schauen Pausiert Abgeschlossen diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 9982ca16..de9ac4f4 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -17,7 +17,7 @@ Μοίρασε Άνοιγμα στον περιηγητή Προσπέραση φορτώματος - Φόρτωση… + Φόρτωση… Watching On-Hold diff --git a/app/src/main/res/values-es/strings-es.xml b/app/src/main/res/values-es/strings-es.xml index 170b6f00..3eb6e57d 100644 --- a/app/src/main/res/values-es/strings-es.xml +++ b/app/src/main/res/values-es/strings-es.xml @@ -56,7 +56,7 @@ Compartir Abrir en el navegador Omitir carga - Cargando… + Cargando… Viendo En espera diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index bcce9a8a..6ca5d68a 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -16,7 +16,7 @@ Partager Ouvrir dans le naviguateur Passer le chargement - Chargement… + Chargement… En visionnage En pose Terminé diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index b0ef6f5b..adad5323 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -52,7 +52,7 @@ Bagikan Buka Di Browser Skip Loading - Loading… + Loading… Sedang Menonton Tertahan diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 89159a12..d5869c2f 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -52,7 +52,7 @@ Condividi Apri nel browser Salta caricamento - Caricamento… + Caricamento… Guardando In attesa diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 229a1a82..96044dc7 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -20,7 +20,7 @@ Сподели Отвори во прелистувач Прескокни вчитување - Вчитување… + Вчитување… Моментални гледања Ставено на чекање diff --git a/app/src/main/res/values-mo/string.xml b/app/src/main/res/values-mo/string.xml index bf0f0dca..340428b9 100644 --- a/app/src/main/res/values-mo/string.xml +++ b/app/src/main/res/values-mo/string.xml @@ -17,7 +17,7 @@ aauuh oooohh oooohhhaaaoouuh oooohhooooo - ooh aaahhu + ooh aaahhu aaaghh ooo-ahah aaahhu ahhahooo diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 6a3bac29..60eb1ad2 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -17,7 +17,7 @@ Deel Openen in Browser Laden overslaan - Laden… + Laden… Aan het kijken In de wacht diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml index 1f3ebcc2..8a25fe08 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -28,7 +28,7 @@ Dele Åpne i nettleseren Hopp over - Laster inn… + Laster inn… Ser på På vent diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 8fbd69d4..f6879c5b 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -31,7 +31,7 @@ Udostępnij Otwórz w przeglądarce Pomiń ładowanie - Ładowanie… + Ładowanie… W trakcie Zawieszone diff --git a/app/src/main/res/values-pt/strings-pt.xml b/app/src/main/res/values-pt/strings-pt.xml index 038bf25b..e686c988 100644 --- a/app/src/main/res/values-pt/strings-pt.xml +++ b/app/src/main/res/values-pt/strings-pt.xml @@ -31,7 +31,7 @@ Compartir Abrir no Navegador Saltar Carga - Cargando… + Cargando… Assistindo Em espera diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index c0ecd4a8..36237180 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -52,7 +52,7 @@ Distribuie Deschide în browser Săriți încărcarea - Se încarcă... + Se încarcă... În curs de vizualizare În așteptare diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 605a91ce..3f3cead1 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -20,7 +20,7 @@ Dela Öppna i webbläsaren Hoppa över - Laddar… + Laddar… Tittar på Pausad diff --git a/app/src/main/res/values-tl/strings.xml b/app/src/main/res/values-tl/strings.xml index 49b892ec..9d0e0e02 100644 --- a/app/src/main/res/values-tl/strings.xml +++ b/app/src/main/res/values-tl/strings.xml @@ -35,7 +35,7 @@ I-share Buksan sa browser Skip Loading… - Loading… + Loading… Pinapanood Inihinto diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 0fd76192..dd5e233b 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -56,7 +56,7 @@ Paylaş Tarayıcıda aç Yüklemeyi atla - Yükleniyor… + Yükleniyor… İzleniyor Beklemede diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index f7d7aedf..db8164f2 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -108,7 +108,7 @@ Chia sẻ Mở bằng trình duyệt Bỏ qua - Đang tải… + Đang tải… Đang xem Đang chờ diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 64c0bbe0..ffda185c 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -38,7 +38,7 @@ 分享 在浏览器中打开 跳过加载 - 正在加载… + 正在加载… 正在观看 暂时搁置 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7172b7b4..d4c19c6c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -110,7 +110,7 @@ Share Open In Browser Skip Loading - Loading… + Loading… Watching On-Hold @@ -286,9 +286,12 @@ Season + %s %d No Season Episode Episodes + %d-%d + %d %s S E No Episodes found diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 3f2cc6c0..bf5e190a 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -266,6 +266,7 @@ @@ -334,6 +335,9 @@ scrollable--> + - + + + +