diff --git a/app/build.gradle b/app/build.gradle index aee896b8..875361ed 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -36,7 +36,7 @@ android { targetSdkVersion 30 versionCode 50 - versionName "3.0.2" + versionName "3.1.2" resValue "string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}" diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index 5c628582..0ed802ba 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -260,10 +260,10 @@ object CommonActivity { KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_MEDIA_SKIP_BACKWARD, KeyEvent.KEYCODE_MEDIA_REWIND -> { PlayerEventType.SeekBack } - KeyEvent.KEYCODE_MEDIA_NEXT, KeyEvent.KEYCODE_BUTTON_R1 -> { + KeyEvent.KEYCODE_MEDIA_NEXT, KeyEvent.KEYCODE_BUTTON_R1, KeyEvent.KEYCODE_N -> { PlayerEventType.NextEpisode } - KeyEvent.KEYCODE_MEDIA_PREVIOUS, KeyEvent.KEYCODE_BUTTON_L1 -> { + KeyEvent.KEYCODE_MEDIA_PREVIOUS, KeyEvent.KEYCODE_BUTTON_L1, KeyEvent.KEYCODE_B -> { PlayerEventType.PrevEpisode } KeyEvent.KEYCODE_MEDIA_PAUSE -> { @@ -294,6 +294,9 @@ object CommonActivity { KeyEvent.KEYCODE_R, KeyEvent.KEYCODE_NUMPAD_0 -> { PlayerEventType.Resize } + KeyEvent.KEYCODE_C, KeyEvent.KEYCODE_NUMPAD_4 -> { + PlayerEventType.SkipOp + } KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_NUMPAD_ENTER, KeyEvent.KEYCODE_ENTER -> { // space is not captured due to navigation PlayerEventType.PlayPauseToggle } diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 45ed3e82..ca1eb3b6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -17,6 +17,7 @@ import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.loadExtractor import okhttp3.Interceptor import java.text.SimpleDateFormat @@ -80,7 +81,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() } @@ -527,6 +528,7 @@ enum class ShowStatus { } enum class DubStatus(val id: Int) { + None(-1), Dubbed(1), Subbed(0), } @@ -861,6 +863,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)) } @@ -902,25 +908,71 @@ interface LoadResponse { } /**better to call addTrailer with mutible trailers directly instead of calling this multiple times*/ - suspend fun LoadResponse.addTrailer(trailerUrl: String?, referer: String? = null) { - if (!isTrailersEnabled || trailerUrl == null) return + suspend fun LoadResponse.addTrailer( + trailerUrl: String?, + referer: String? = null, + addRaw: Boolean = false + ) { + if (!isTrailersEnabled || trailerUrl.isNullOrBlank()) return val links = arrayListOf() val subs = arrayListOf() - loadExtractor(trailerUrl, referer, { subs.add(it) }, { links.add(it) }) - this.trailers.add(TrailerData(links, subs)) + if (!loadExtractor( + trailerUrl, + referer, + { subs.add(it) }, + { links.add(it) }) && addRaw + ) { + this.trailers.add( + TrailerData( + listOf( + ExtractorLink( + "", + "Trailer", + trailerUrl, + referer ?: "", + Qualities.Unknown.value, + trailerUrl.contains(".m3u8") + ) + ), listOf() + ) + ) + } else { + this.trailers.add(TrailerData(links, subs)) + } } fun LoadResponse.addTrailer(newTrailers: List) { trailers.addAll(newTrailers.map { TrailerData(listOf(it)) }) } - suspend fun LoadResponse.addTrailer(trailerUrls: List?, referer: String? = null) { + suspend fun LoadResponse.addTrailer( + trailerUrls: List?, + referer: String? = null, + addRaw: Boolean = false + ) { if (!isTrailersEnabled || trailerUrls == null) return - val trailers = trailerUrls.apmap { trailerUrl -> + val trailers = trailerUrls.filter { it.isNotBlank() }.apmap { trailerUrl -> val links = arrayListOf() val subs = arrayListOf() - loadExtractor(trailerUrl, referer, { subs.add(it) }, { links.add(it) }) - links to subs + if (!loadExtractor( + trailerUrl, + referer, + { subs.add(it) }, + { links.add(it) }) && addRaw + ) { + arrayListOf( + ExtractorLink( + "", + "Trailer", + trailerUrl, + referer ?: "", + Qualities.Unknown.value, + trailerUrl.contains(".m3u8") + ) + ) to arrayListOf() + } else { + links to subs + } }.map { (links, subs) -> TrailerData(links, subs) } this.trailers.addAll(trailers) } @@ -1001,6 +1053,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 49504efd..0bbdd358 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -151,7 +151,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { // Fucks up anime info layout since that has its own layout cast_mini_controller_holder?.isVisible = - !listOf(R.id.navigation_results, R.id.navigation_player).contains(destination.id) + !listOf( + R.id.navigation_results_phone, + R.id.navigation_results_tv, + R.id.navigation_player + ).contains(destination.id) val isNavVisible = listOf( R.id.navigation_home, @@ -327,6 +331,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { if (str.startsWith("https://cs.repo")) { val realUrl = "https://" + str.substringAfter("?") println("Repository url: $realUrl") + val activity = this ioSafe { val repo = RepositoryManager.parseRepository(realUrl) ?: return@ioSafe RepositoryManager.addRepository( @@ -337,8 +342,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { ) main { showToast( - this, - this.getString(R.string.player_loaded_subtitles, repo.name), + activity, + getString(R.string.player_loaded_subtitles, repo.name), Toast.LENGTH_LONG ) } @@ -346,6 +351,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } else 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) @@ -356,10 +362,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/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/home/HomeParentItemAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt index 0b3764f0..4f113eba 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt @@ -98,7 +98,7 @@ class ParentItemAdapter( recyclerView?.apply { // this loops every viewHolder in the recycle view and checks the position to see if it is within the update range val missingUpdates = (position until (position + count)).toMutableSet() - for (i in 0 until mAdapter.itemCount) { + for (i in 0 until itemCount) { val viewHolder = getChildViewHolder(getChildAt(i)) val absolutePosition = viewHolder.absoluteAdapterPosition if (absolutePosition >= position && absolutePosition < position + count) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt index 6c6393cb..2ddef9a4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt @@ -1164,6 +1164,9 @@ open class FullScreenPlayer : AbstractPlayerFragment() { openOnlineSubPicker(view.context, null) {} } } + PlayerEventType.SkipOp -> { + skipOp() + } } } 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/player/IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt index 0d5c1e26..8561a0f4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt @@ -23,6 +23,7 @@ enum class PlayerEventType(val value: Int) { ShowMirrors(12), Resize(13), SearchSubtitlesOnline(14), + SkipOp(15), } enum class CSPlayerEvent(val value: Int) { 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..9acae56b 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,11 +57,11 @@ const val ACTION_DOWNLOAD_EPISODE_SUBTITLE_MIRROR = 14 data class EpisodeClickEvent(val action: Int, val data: ResultEpisode) class EpisodeAdapter( - var cardList: List, private val hasDownloadSupport: Boolean, private val clickCallback: (EpisodeClickEvent) -> Unit, private val downloadClickCallback: (DownloadClickEvent) -> Unit, ) : RecyclerView.Adapter() { + private var cardList: MutableList = mutableListOf() private val mBoundViewHolders: HashSet = HashSet() private fun getAllBoundViewHolders(): Set? { @@ -74,6 +75,9 @@ class EpisodeAdapter( } override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) { + if(holder.itemView.hasFocus()) { + holder.itemView.clearFocus() + } if (holder is DownloadButtonViewHolder) { holder.downloadButton.dispose() } @@ -92,15 +96,19 @@ 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) } + var layout = R.layout.result_episode_both + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { /*val layout = if (cardList.filter { it.poster != null }.size >= cardList.size / 2) R.layout.result_episode_large @@ -108,7 +116,7 @@ class EpisodeAdapter( return EpisodeCardViewHolder( LayoutInflater.from(parent.context) - .inflate(R.layout.result_episode_both, parent, false), + .inflate(layout, parent, false), hasDownloadSupport, clickCallback, downloadClickCallback @@ -146,6 +154,8 @@ class EpisodeAdapter( fun bind(card: ResultEpisode) { localCard = card + val isTrueTv = itemView.context?.isTrueTvSettings() == true + val (parentView, otherView) = if (card.poster == null) { itemView.episode_holder to itemView.episode_holder_large } else { @@ -196,20 +206,22 @@ class EpisodeAdapter( } } - episodePoster?.setOnClickListener { - clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) - } + if (!isTrueTv) { + episodePoster?.setOnClickListener { + clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) + } - episodePoster?.setOnLongClickListener { - clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_TOAST, card)) - return@setOnLongClickListener true + episodePoster?.setOnLongClickListener { + clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_TOAST, card)) + return@setOnLongClickListener true + } } parentView.setOnClickListener { clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) } - if (parentView.context.isTrueTvSettings()) { + if (isTrueTv) { parentView.isFocusable = true parentView.isFocusableInTouchMode = true parentView.touchscreenBlocksFocus = false @@ -263,3 +275,19 @@ class EpisodeAdapter( } } } + +class ResultDiffCallback( + private val oldList: List, + private val newList: List +) : + DiffUtil.Callback() { + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = + oldList[oldItemPosition].id == newList[newItemPosition].id + + override fun getOldListSize() = oldList.size + + override fun getNewListSize() = newList.size + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = + oldList[oldItemPosition] == newList[newItemPosition] +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt index e5b13abf..eb23d3ea 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,117 +1,121 @@ package com.lagradost.cloudstream3.ui.result import android.annotation.SuppressLint -import android.app.Activity -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.content.Context.CLIPBOARD_SERVICE import android.content.Intent import android.content.Intent.* import android.content.res.ColorStateList -import android.content.res.Configuration -import android.graphics.Rect import android.net.Uri import android.os.Build 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 androidx.appcompat.app.AlertDialog -import androidx.core.content.FileProvider import androidx.core.view.isGone import androidx.core.view.isVisible -import androidx.core.widget.NestedScrollView import androidx.core.widget.doOnTextChanged import androidx.lifecycle.ViewModelProvider import androidx.preference.PreferenceManager import com.discord.panels.OverlappingPanelsLayout -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.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.DubStatus +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.SearchResponse +import com.lagradost.cloudstream3.TvType 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.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.download.EasyDownloadButton 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.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.result_cast_items +import kotlinx.android.synthetic.main.fragment_result.result_cast_text +import kotlinx.android.synthetic.main.fragment_result.result_coming_soon +import kotlinx.android.synthetic.main.fragment_result.result_data_holder +import kotlinx.android.synthetic.main.fragment_result.result_description +import kotlinx.android.synthetic.main.fragment_result.result_download_movie +import kotlinx.android.synthetic.main.fragment_result.result_episode_loading +import kotlinx.android.synthetic.main.fragment_result.result_episodes +import kotlinx.android.synthetic.main.fragment_result.result_error_text +import kotlinx.android.synthetic.main.fragment_result.result_finish_loading +import kotlinx.android.synthetic.main.fragment_result.result_info +import kotlinx.android.synthetic.main.fragment_result.result_loading +import kotlinx.android.synthetic.main.fragment_result.result_loading_error +import kotlinx.android.synthetic.main.fragment_result.result_meta_duration +import kotlinx.android.synthetic.main.fragment_result.result_meta_rating +import kotlinx.android.synthetic.main.fragment_result.result_meta_site +import kotlinx.android.synthetic.main.fragment_result.result_meta_type +import kotlinx.android.synthetic.main.fragment_result.result_meta_year +import kotlinx.android.synthetic.main.fragment_result.result_movie_download_icon +import kotlinx.android.synthetic.main.fragment_result.result_movie_download_text +import kotlinx.android.synthetic.main.fragment_result.result_movie_download_text_precentage +import kotlinx.android.synthetic.main.fragment_result.result_movie_progress_downloaded +import kotlinx.android.synthetic.main.fragment_result.result_movie_progress_downloaded_holder +import kotlinx.android.synthetic.main.fragment_result.result_next_airing +import kotlinx.android.synthetic.main.fragment_result.result_next_airing_time +import kotlinx.android.synthetic.main.fragment_result.result_no_episodes +import kotlinx.android.synthetic.main.fragment_result.result_play_movie +import kotlinx.android.synthetic.main.fragment_result.result_reload_connection_open_in_browser +import kotlinx.android.synthetic.main.fragment_result.result_reload_connectionerror +import kotlinx.android.synthetic.main.fragment_result.result_resume_parent +import kotlinx.android.synthetic.main.fragment_result.result_resume_progress_holder +import kotlinx.android.synthetic.main.fragment_result.result_resume_series_progress +import kotlinx.android.synthetic.main.fragment_result.result_resume_series_progress_text +import kotlinx.android.synthetic.main.fragment_result.result_resume_series_title +import kotlinx.android.synthetic.main.fragment_result.result_tag +import kotlinx.android.synthetic.main.fragment_result.result_tag_holder +import kotlinx.android.synthetic.main.fragment_result.result_title +import kotlinx.android.synthetic.main.fragment_result.result_vpn import kotlinx.android.synthetic.main.fragment_result_swipe.* +import kotlinx.android.synthetic.main.fragment_result_tv.* 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 +import android.widget.EditText + +import android.widget.AbsListView + + + + -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 +150,7 @@ fun buildResultEpisode( name: String? = null, poster: String? = null, episode: Int, + seasonIndex: Int? = null, season: Int? = null, data: String, apiName: String, @@ -163,6 +168,7 @@ fun buildResultEpisode( name, poster, episode, + seasonIndex, season, data, apiName, @@ -183,7 +189,7 @@ fun ResultEpisode.getWatchProgress(): Float { return (getDisplayPosition() / 1000).toFloat() / (duration / 1000).toFloat() } -class ResultFragment : ResultTrailerPlayer() { +open class ResultFragment : ResultTrailerPlayer() { companion object { const val URL_BUNDLE = "url" const val API_NAME_BUNDLE = "apiName" @@ -192,6 +198,7 @@ class ResultFragment : ResultTrailerPlayer() { const val START_ACTION_BUNDLE = "startAction" const val START_VALUE_BUNDLE = "startValue" const val RESTART_BUNDLE = "restart" + fun newInstance( card: SearchResponse, startAction: Int = 0, startValue: Int? = null ): Bundle { @@ -199,7 +206,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 +214,8 @@ class ResultFragment : ResultTrailerPlayer() { putInt(START_ACTION_BUNDLE, startAction) if (startValue != null) putInt(START_VALUE_BUNDLE, startValue) + + putBoolean(RESTART_BUNDLE, true) } } @@ -232,234 +240,13 @@ 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 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 + open fun setTrailers(trailers: List?) {} + + protected lateinit var viewModel: ResultViewModel2 //by activityViewModels() + protected lateinit var syncModel: SyncViewModel + protected open val resultLayout = R.layout.fragment_result_swipe override fun onCreateView( inflater: LayoutInflater, @@ -467,33 +254,22 @@ 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) + return inflater.inflate(resultLayout, container, false) } + private var downloadButton: EasyDownloadButton? = null override fun onDestroyView() { updateUIListener = null (result_episodes?.adapter as EpisodeAdapter?)?.killAdapter() downloadButton?.dispose() - //somehow this still leaks and I dont know why???? - // todo look at https://github.com/discord/OverlappingPanels/blob/70b4a7cf43c6771873b1e091029d332896d41a1a/sample_app/src/main/java/com/discord/sampleapp/MainActivity.kt - PanelsChildGestureRegionObserver.Provider.get().removeGestureRegionsUpdateListener(this) - result_cast_items?.let { - PanelsChildGestureRegionObserver.Provider.get().unregister(it) - } + super.onDestroyView() } - override fun onDestroy() { - //requireActivity().viewModelStore.clear() // REMEMBER THE CLEAR - - - super.onDestroy() - } - override fun onResume() { super.onResume() activity?.let { @@ -516,7 +292,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,304 +318,8 @@ 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 + open fun setRecommendations(rec: List?, validApiName: String?) { - 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 - - override fun nextMirror() { - currentTrailerIndex++ - loadTrailer() - } - - override fun hasNextMirror(): Boolean { - return currentTrailerIndex + 1 < currentTrailers.size - } - - override fun playerError(exception: Exception) { - if (player.getIsPlaying()) { // because we dont want random toasts in player - super.playerError(exception) - } else { - nextMirror() - } - } - - 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 -> - context?.let { ctx -> - player.onPause() - player.loadPlayer( - ctx, - false, - trailer, - null, - startPosition = 0L, - subtitles = emptySet(), - subtitle = null, - autoPlay = false - ) - true - } ?: run { - false - } - } ?: run { - false - } - result_trailer_loading?.isVisible = isSuccess - result_smallscreen_holder?.isVisible = !isSuccess && !isFullScreenPlayer - - // We don't want the trailer to be focusable if it's not visible - result_smallscreen_holder?.descendantFocusability = if (isSuccess) { - ViewGroup.FOCUS_AFTER_DESCENDANTS - } else { - ViewGroup.FOCUS_BLOCK_DESCENDANTS - } - result_fullscreen_holder?.isVisible = !isSuccess && isFullScreenPlayer - } - - private fun setTrailers(trailers: List?) { - context?.updateHasTrailers() - if (!LoadResponse.isTrailersEnabled) return - currentTrailers = trailers?.sortedBy { -it.quality } ?: emptyList() - 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 - result_recommendations_btt?.isGone = isInvalid - result_recommendations_btt?.setOnClickListener { - val nextFocusDown = if (result_overlapping_panels?.getSelectedPanel()?.ordinal == 1) { - result_overlapping_panels?.openEndPanel() - R.id.result_recommendations - } else { - result_overlapping_panels?.closePanels() - R.id.result_description - } - - result_recommendations_btt?.nextFocusDownId = nextFocusDown - result_search?.nextFocusDownId = nextFocusDown - result_open_in_browser?.nextFocusDownId = nextFocusDown - result_share?.nextFocusDownId = nextFocusDown - } - result_overlapping_panels?.setEndPanelLockState(if (isInvalid) OverlappingPanelsLayout.LockState.CLOSE else OverlappingPanelsLayout.LockState.UNLOCKED) - - val matchAgainst = validApiName ?: rec?.firstOrNull()?.apiName - rec?.map { it.apiName }?.distinct()?.let { apiNames -> - // very dirty selection - result_recommendations_filter_button?.isVisible = apiNames.size > 1 - result_recommendations_filter_button?.text = matchAgainst - result_recommendations_filter_button?.setOnClickListener { _ -> - activity?.showBottomDialog( - apiNames, - apiNames.indexOf(matchAgainst), - getString(R.string.home_change_provider_img_des), false, {} - ) { - setRecommendations(rec, apiNames[it]) - } - } - } ?: run { - result_recommendations_filter_button?.isVisible = false - } - - result_recommendations?.post { - rec?.let { list -> - (result_recommendations?.adapter as SearchAdapter?)?.updateList(list.filter { it.apiName == matchAgainst }) - } - } - } - - private fun fixGrid() { - activity?.getSpanCount()?.let { _ -> - //result_recommendations?.spanCount = count // this is due to discord not changing size with rotation - } - } - - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - 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() { @@ -847,400 +327,94 @@ class ResultFragment : ResultTrailerPlayer() { 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 - } + open fun updateMovie(data: ResourceSome>) { + when (data) { + is ResourceSome.Success -> { + data.value.let { (text, ep) -> + result_play_movie.setText(text) + result_play_movie?.setOnClickListener { + viewModel.handleAction( + activity, + EpisodeClickEvent(ACTION_CLICK_DEFAULT, ep) + ) + } + result_play_movie?.setOnLongClickListener { + viewModel.handleAction( + activity, + EpisodeClickEvent(ACTION_SHOW_OPTIONS, ep) + ) + return@setOnLongClickListener true + } - var currentLinks: Set? = null - var currentSubs: Set? = null + main { + val file = + ioWork { + context?.let { + VideoDownloadManager.getDownloadFileInfoAndUpdateSettings( + it, + ep.id + ) + } + } - //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 + downloadButton?.dispose() + downloadButton = EasyDownloadButton() + downloadButton?.setUpMoreButton( + file?.fileLength, + file?.totalBytes, + result_movie_progress_downloaded, + result_movie_download_icon, + result_movie_download_text, + result_movie_download_text_precentage, + result_download_movie, + true, + VideoDownloadHelper.DownloadEpisodeCached( + ep.name, + ep.poster, + 0, + null, + ep.id, + ep.id, + null, + null, + System.currentTimeMillis(), ) - ) - } else { - handleAction( - EpisodeClickEvent( - ACTION_PLAY_EPISODE_IN_PLAYER, - episodeClick.data - ) - ) + ) { click -> + when (click.action) { + DOWNLOAD_ACTION_DOWNLOAD -> { + viewModel.handleAction( + activity, + EpisodeClickEvent(ACTION_DOWNLOAD_EPISODE, ep) + ) + } + else -> handleDownloadClick(activity, click) + } + } + result_movie_progress_downloaded_holder?.isVisible = true } } } - - 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) - } + else -> { + result_movie_progress_downloaded_holder?.isVisible = false + result_play_movie?.isVisible = false } + } + } - 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() - } + open fun updateEpisodes(episodes: ResourceSome>) { + when (episodes) { + is ResourceSome.None -> { + result_episode_loading?.isVisible = false + result_episodes?.isVisible = false } - 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) - } - } + is ResourceSome.Loading -> { + result_episode_loading?.isVisible = true + result_episodes?.isVisible = false } - - 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) - } + is ResourceSome.Success -> { + result_episodes?.isVisible = true + result_episode_loading?.isVisible = false + (result_episodes?.adapter as? EpisodeAdapter?)?.updateList(episodes.value) } } } @@ -1249,20 +423,7 @@ class ResultFragment : ResultTrailerPlayer() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - result_cast_items?.let { - PanelsChildGestureRegionObserver.Provider.get().register(it) - } result_cast_items?.adapter = ActorAdaptor() - fixGrid() - result_recommendations?.spanCount = 3 - result_overlapping_panels?.setStartPanelLockState(OverlappingPanelsLayout.LockState.CLOSE) - result_overlapping_panels?.setEndPanelLockState(OverlappingPanelsLayout.LockState.CLOSE) - - player_open_source?.setOnClickListener { - currentTrailers.getOrNull(currentTrailerIndex)?.let { - context?.openBrowser(it.url) - } - } updateUIListener = ::updateUI @@ -1290,12 +451,23 @@ 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) @@ -1325,46 +497,17 @@ class ResultFragment : ResultTrailerPlayer() { } } - result_scroll.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY -> - val dy = scrollY - oldScrollY - if (dy > 0) { //check for scroll down - result_bookmark_fab?.shrink() - } else if (dy < -5) { - result_bookmark_fab?.extend() - } - if (!isFullScreenPlayer && player.getIsPlaying()) { - if (scrollY > (player_background?.height ?: scrollY)) { - player.handleEvent(CSPlayerEvent.Pause) - } - } - //result_poster_blur_holder?.translationY = -scrollY.toFloat() - }) - - result_back.setOnClickListener { - activity?.popCurrentPage() - } - - result_episodes.adapter = + result_episodes?.adapter = EpisodeAdapter( - ArrayList(), api.hasDownloadSupport, { episodeClick -> - handleAction(episodeClick) + viewModel.handleAction(activity, episodeClick) }, { downloadClickEvent -> - handleDownloadClick(activity, currentHeaderName, downloadClickEvent) + handleDownloadClick(activity, downloadClickEvent) } ) - result_bookmark_button.setOnClickListener { - it.popupMenuNoIcons( - items = WatchType.values() - .map { watchType -> Pair(watchType.internalId, watchType.stringRes) }, - //.map { watchType -> Triple(watchType.internalId, watchType.iconRes, watchType.stringRes) }, - ) { - viewModel.updateWatchStatus(WatchType.fromInternalId(this.itemId)) - } - } observe(viewModel.watchStatus) { watchType -> result_bookmark_button?.text = getString(watchType.stringRes) @@ -1392,64 +535,11 @@ class ResultFragment : ResultTrailerPlayer() { } } - /** - * Sets next focus to allow navigation up and down between 2 views - * if either of them is null nothing happens. - **/ - fun setFocusUpAndDown(upper: View?, down: View?) { - if (upper == null || down == null) return - upper.nextFocusDownId = down.id - down.nextFocusUpId = upper.id - } - // This is to band-aid FireTV navigation - result_season_button?.isFocusableInTouchMode = context?.isTvSettings() == true - 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) - } - } - } + val isTv = context?.isTvSettings() == true + result_season_button?.isFocusableInTouchMode = isTv + result_episode_select?.isFocusableInTouchMode = isTv + result_dub_select?.isFocusableInTouchMode = isTv context?.let { ctx -> val arrayAdapter = ArrayAdapter(ctx, R.layout.sort_bottom_single_choice) @@ -1500,19 +590,6 @@ class ResultFragment : ResultTrailerPlayer() { } } - result_mini_sync?.adapter = ImageAdapter( - R.layout.result_mini_image, - nextFocusDown = R.id.result_sync_set_score, - clickCallback = { action -> - if (action == IMAGE_CLICK || action == IMAGE_LONG_CLICK) { - if (result_overlapping_panels?.getSelectedPanel()?.ordinal == 1) { - result_overlapping_panels?.openStartPanel() - } else { - result_overlapping_panels?.closePanels() - } - } - }) - observe(syncModel.synced) { list -> result_sync_names?.text = list.filter { it.isSynced && it.hasAccount }.joinToString { it.name } @@ -1523,10 +600,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 +622,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,170 +683,58 @@ 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 - - result_resume_series_button?.setOnClickListener { - episodeList.firstOrNull { it.id == resume.episodeId }?.let { - handleAction(EpisodeClickEvent(ACTION_PLAY_EPISODE_IN_PLAYER, it)) + 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_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_text.setText(progress.progressLeft) result_resume_series_progress?.apply { - max = (viewPos.duration / 1000).toInt() - progress = (viewPos.position / 1000).toInt() + isVisible = true + this.max = progress.maxProgress + this.progress = progress.progress } - 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 + 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 } - } ?: 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() - } + result_resume_series_button?.isVisible = !value.isMovie + result_resume_series_button_play?.isVisible = !value.isMovie - 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( + val click = View.OnClickListener { + 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 - } - } } + result_resume_series_button?.setOnClickListener(click) + result_resume_series_button_play?.setOnClickListener(click) } - else -> Unit - } - arguments?.remove("startValue") - arguments?.remove("startAction") - startAction = null - startValue = null - } - - observe(viewModel.publicEpisodes) { episodes -> - when (episodes) { - is Resource.Failure -> { - result_episode_loading?.isVisible = false - //result_episodes?.isVisible = false - } - is Resource.Loading -> { - result_episode_loading?.isVisible = true - // result_episodes?.isVisible = false - } - is Resource.Success -> { - //result_episodes?.isVisible = true - result_episode_loading?.isVisible = false - if (result_episodes == null || result_episodes.adapter == null) return@observe - currentEpisodes = episodes.value - (result_episodes?.adapter as? EpisodeAdapter?)?.cardList = episodes.value - (result_episodes?.adapter as? EpisodeAdapter?)?.updateLayout() - (result_episodes?.adapter as? EpisodeAdapter?)?.notifyDataSetChanged() + is Some.None -> { + result_resume_parent?.isVisible = false } } } - observe(viewModel.dubStatus) { status -> - result_dub_select?.text = status.toString() - } - -// val preferDub = context?.getApiDubstatusSettings()?.all { it == DubStatus.Dubbed } == true - - observe(viewModel.dubSubSelections) { range -> - dubRange = range - -// if (preferDub && dubRange?.contains(DubStatus.Dubbed) == true) { -// viewModel.changeDubStatus(DubStatus.Dubbed) -// } - - result_dub_select?.visibility = if (range.size <= 1) GONE else VISIBLE - - if (result_season_button?.isVisible != true && result_episode_select?.isVisible != true) { - if (result_series_parent?.isVisible == true) - setFocusUpAndDown(result_resume_series_button, result_dub_select) - else - setFocusUpAndDown(result_bookmark_button, result_dub_select) - } + observe(viewModel.episodes) { episodes -> + updateEpisodes(episodes) } result_cast_items?.setOnFocusChangeListener { _, hasFocus -> @@ -1780,88 +742,75 @@ 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.trailers) { trailers -> + setTrailers(trailers.flatMap { it.mirros }) // I dont care about subtitles yet! } - observe(viewModel.id) { - currentId = it + observe(viewModel.recommendations) { recommendations -> + setRecommendations(recommendations, null) } - observe(viewModel.result) { data -> + observe(viewModel.movie) { data -> + updateMovie(data) + } + + observe(viewModel.page) { data -> when (data) { is Resource.Success -> { val d = data.value - if (d !is AnimeLoadResponse && result_episode_loading.isVisible) { // no episode loading when not anime - result_episode_loading.isVisible = false - } updateVisStatus(2) - result_vpn?.text = when (api.vpnStatus) { - VPNStatus.MightBeNeeded -> getString(R.string.vpn_might_be_needed) - VPNStatus.Torrent -> getString(R.string.vpn_torrent) - else -> "" - } - result_vpn?.isGone = api.vpnStatus == VPNStatus.None + result_vpn.setText(d.vpnText) + result_info.setText(d.metaText) + result_no_episodes.setText(d.noEpisodesFoundText) + result_title.setText(d.titleText) + result_meta_site.setText(d.apiName) + result_meta_type.setText(d.typeText) + result_meta_year.setText(d.yearText) + result_meta_duration.setText(d.durationText) + result_meta_rating.setText(d.ratingText) + result_cast_text.setText(d.actorsText) + result_next_airing.setText(d.nextAiringEpisode) + result_next_airing_time.setText(d.nextAiringDate) + result_poster.setImage(d.posterImage) - result_info?.text = when (api.providerType) { - ProviderType.MetaProvider -> getString(R.string.provider_info_meta) - else -> "" - } - result_info?.isVisible = api.providerType == ProviderType.MetaProvider + 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) - 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 + 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()) } - currentHeaderName = d.name - currentType = d.type - - currentPoster = d.posterUrl - currentIsMovie = !d.isEpisodeBased() - + result_open_in_browser?.isVisible = d.url.startsWith("http") result_open_in_browser?.setOnClickListener { val i = Intent(ACTION_VIEW) i.data = Uri.parse(d.url) @@ -1873,36 +822,21 @@ class ResultFragment : ResultTrailerPlayer() { } result_search?.setOnClickListener { - QuickSearchFragment.pushSearch(activity, d.name) + QuickSearchFragment.pushSearch(activity, d.title) } result_share?.setOnClickListener { try { val i = Intent(ACTION_SEND) i.type = "text/plain" - i.putExtra(EXTRA_SUBJECT, d.name) + i.putExtra(EXTRA_SUBJECT, d.title) i.putExtra(EXTRA_TEXT, d.url) - startActivity(createChooser(i, d.name)) + startActivity(createChooser(i, d.title)) } catch (e: Exception) { logError(e) } } - val showStatus = when (d) { - is TvSeriesLoadResponse -> d.showStatus - is AnimeLoadResponse -> d.showStatus - else -> null - } - - setShow(showStatus) - setDuration(d.duration) - setYear(d.year) - setRating(d.rating) - setRecommendations(d.recommendations, null) - setActors(d.actors) - setNextEpisode(if (d is EpisodeResponse) d.nextAiring else null) - setTrailers(d.trailers.flatMap { it.mirros }) // I dont care about subtitles yet! - if (syncModel.addSyncs(d.syncData)) { syncModel.updateMetaAndUser() syncModel.updateSynced() @@ -1910,71 +844,20 @@ class ResultFragment : ResultTrailerPlayer() { 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) - } + result_description.setTextHtml(d.plotText) + if (this !is ResultFragmentTv) // dont want this clickable on tv layout + 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() } - - } 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 @@ -1982,9 +865,8 @@ class ResultFragment : ResultTrailerPlayer() { } val tags = d.tags - if (tags.isNullOrEmpty()) { - //result_tag_holder?.visibility = GONE - } else { + result_tag_holder?.isVisible = tags.isNotEmpty() + if (tags.isNotEmpty()) { //result_tag_holder?.visibility = VISIBLE val isOnTv = context?.isTrueTvSettings() == true for ((index, tag) in tags.withIndex()) { @@ -1996,163 +878,6 @@ class ResultFragment : ResultTrailerPlayer() { result_tag?.addView(viewBtt, index) } } - - if (d.type.isMovieType()) { - val hasDownloadSupport = api.hasDownloadSupport - lateFixDownloadButton(true) - - result_play_movie?.setOnClickListener { - val card = - currentEpisodes?.firstOrNull() ?: return@setOnClickListener - handleAction(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) - } - - result_play_movie?.setOnLongClickListener { - val card = currentEpisodes?.firstOrNull() - ?: return@setOnLongClickListener true - handleAction(EpisodeClickEvent(ACTION_SHOW_OPTIONS, card)) - 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() - - val file = - VideoDownloadManager.getDownloadFileInfoAndUpdateSettings( - requireContext(), - localId - ) - downloadButton?.dispose() - downloadButton = EasyDownloadButton() - downloadButton?.setUpMoreButton( - file?.fileLength, - file?.totalBytes, - result_movie_progress_downloaded, - result_movie_download_icon, - result_movie_download_text, - result_movie_download_text_precentage, - result_download_movie, - true, - VideoDownloadHelper.DownloadEpisodeCached( - d.name, - d.posterUrl, - 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, - 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, - ) - ) - ) - } - } else { - handleDownloadClick( - activity, - currentHeaderName, - downloadClickEvent - ) - } - }*/ - } - } else { - lateFixDownloadButton(false) - } - - context?.getString( - when (d.type) { - TvType.TvSeries -> R.string.tv_series_singular - TvType.Anime -> R.string.anime_singular - TvType.OVA -> R.string.ova_singular - TvType.AnimeMovie -> R.string.movies_singular - TvType.Cartoon -> R.string.cartoons_singular - TvType.Documentary -> R.string.documentaries_singular - TvType.Movie -> R.string.movies_singular - TvType.Torrent -> R.string.torrent_singular - TvType.AsianDrama -> R.string.asian_drama_singular - TvType.Live -> R.string.live_singular - } - )?.let { - result_meta_type?.text = it - } - - when (d) { - is AnimeLoadResponse -> { - - // val preferEnglish = true - //val titleName = (if (preferEnglish) d.engName else d.japName) ?: d.name - val titleName = d.name - result_title.text = titleName - //result_toolbar.title = titleName - } - else -> result_title.text = d.name - } } is Resource.Failure -> { result_error_text.text = url?.plus("\n") + data.errorString @@ -2164,16 +889,11 @@ class ResultFragment : ResultTrailerPlayer() { } } - result_recommendations?.adapter = - SearchAdapter( - ArrayList(), - result_recommendations, - ) { callback -> - 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 +903,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) { @@ -2199,9 +918,10 @@ class ResultFragment : ResultTrailerPlayer() { } } + result_open_in_browser?.isVisible = url.startsWith("http") 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,29 +932,18 @@ 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) } } } - - PanelsChildGestureRegionObserver.Provider.get().addGestureRegionsUpdateListener(this) - } - - override fun onPause() { - super.onPause() - PanelsChildGestureRegionObserver.Provider.get().addGestureRegionsUpdateListener(this) - } - - override fun onGestureRegionsUpdate(gestureRegions: List) { - result_overlapping_panels?.setChildGestureRegions(gestureRegions) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt new file mode 100644 index 00000000..750556ad --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt @@ -0,0 +1,401 @@ +package com.lagradost.cloudstream3.ui.result + +import android.app.Dialog +import android.graphics.Rect +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isGone +import androidx.core.view.isVisible +import androidx.core.widget.NestedScrollView +import com.discord.panels.OverlappingPanelsLayout +import com.discord.panels.PanelsChildGestureRegionObserver +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.lagradost.cloudstream3.APIHolder.updateHasTrailers +import com.lagradost.cloudstream3.DubStatus +import com.lagradost.cloudstream3.LoadResponse +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.SearchResponse +import com.lagradost.cloudstream3.mvvm.Some +import com.lagradost.cloudstream3.mvvm.observe +import com.lagradost.cloudstream3.ui.WatchType +import com.lagradost.cloudstream3.ui.player.CSPlayerEvent +import com.lagradost.cloudstream3.ui.search.SearchAdapter +import com.lagradost.cloudstream3.ui.search.SearchHelper +import com.lagradost.cloudstream3.utils.AppUtils.openBrowser +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog +import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant +import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe +import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage +import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIcons +import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes +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.trailer_custom_layout.* + +class ResultFragmentPhone : ResultFragment() { + var currentTrailers: List = emptyList() + var currentTrailerIndex = 0 + + override fun nextMirror() { + currentTrailerIndex++ + loadTrailer() + } + + override fun hasNextMirror(): Boolean { + return currentTrailerIndex + 1 < currentTrailers.size + } + + override fun playerError(exception: Exception) { + if (player.getIsPlaying()) { // because we dont want random toasts in player + super.playerError(exception) + } else { + nextMirror() + } + } + + private fun loadTrailer(index: Int? = null) { + val isSuccess = + currentTrailers.getOrNull(index ?: currentTrailerIndex)?.let { trailer -> + context?.let { ctx -> + player.onPause() + player.loadPlayer( + ctx, + false, + trailer, + null, + startPosition = 0L, + subtitles = emptySet(), + subtitle = null, + autoPlay = false + ) + true + } ?: run { + false + } + } ?: run { + false + } + result_trailer_loading?.isVisible = isSuccess + result_smallscreen_holder?.isVisible = !isSuccess && !isFullScreenPlayer + + // We don't want the trailer to be focusable if it's not visible + result_smallscreen_holder?.descendantFocusability = if (isSuccess) { + ViewGroup.FOCUS_AFTER_DESCENDANTS + } else { + ViewGroup.FOCUS_BLOCK_DESCENDANTS + } + result_fullscreen_holder?.isVisible = !isSuccess && isFullScreenPlayer + } + + override fun setTrailers(trailers: List?) { + context?.updateHasTrailers() + if (!LoadResponse.isTrailersEnabled) return + currentTrailers = trailers?.sortedBy { -it.quality } ?: emptyList() + loadTrailer() + } + + override fun onDestroyView() { + //somehow this still leaks and I dont know why???? + // todo look at https://github.com/discord/OverlappingPanels/blob/70b4a7cf43c6771873b1e091029d332896d41a1a/sample_app/src/main/java/com/discord/sampleapp/MainActivity.kt + PanelsChildGestureRegionObserver.Provider.get().let { obs -> + result_cast_items?.let { + obs.unregister(it) + } + obs.removeGestureRegionsUpdateListener(this) + } + + super.onDestroyView() + } + + var loadingDialog: Dialog? = null + var popupDialog: Dialog? = null + + /** + * Sets next focus to allow navigation up and down between 2 views + * if either of them is null nothing happens. + **/ + private fun setFocusUpAndDown(upper: View?, down: View?) { + if (upper == null || down == null) return + upper.nextFocusDownId = down.id + down.nextFocusUpId = upper.id + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + player_open_source?.setOnClickListener { + currentTrailers.getOrNull(currentTrailerIndex)?.let { + context?.openBrowser(it.url) + } + } + result_overlapping_panels?.setStartPanelLockState(OverlappingPanelsLayout.LockState.CLOSE) + result_overlapping_panels?.setEndPanelLockState(OverlappingPanelsLayout.LockState.CLOSE) + + result_recommendations?.spanCount = 3 + result_recommendations?.adapter = + SearchAdapter( + ArrayList(), + result_recommendations, + ) { callback -> + SearchHelper.handleSearchClickCallback(activity, callback) + } + PanelsChildGestureRegionObserver.Provider.get().addGestureRegionsUpdateListener(this) + + result_cast_items?.let { + PanelsChildGestureRegionObserver.Provider.get().register(it) + } + + + result_back?.setOnClickListener { + activity?.popCurrentPage() + } + + result_bookmark_button?.setOnClickListener { + it.popupMenuNoIcons( + items = WatchType.values() + .map { watchType -> Pair(watchType.internalId, watchType.stringRes) }, + //.map { watchType -> Triple(watchType.internalId, watchType.iconRes, watchType.stringRes) }, + ) { + viewModel.updateWatchStatus(WatchType.fromInternalId(this.itemId)) + } + } + + result_mini_sync?.adapter = ImageAdapter( + R.layout.result_mini_image, + nextFocusDown = R.id.result_sync_set_score, + clickCallback = { action -> + if (action == IMAGE_CLICK || action == IMAGE_LONG_CLICK) { + if (result_overlapping_panels?.getSelectedPanel()?.ordinal == 1) { + result_overlapping_panels?.openStartPanel() + } else { + result_overlapping_panels?.closePanels() + } + } + }) + + + result_scroll?.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY -> + val dy = scrollY - oldScrollY + if (dy > 0) { //check for scroll down + result_bookmark_fab?.shrink() + } else if (dy < -5) { + result_bookmark_fab?.extend() + } + if (!isFullScreenPlayer && player.getIsPlaying()) { + if (scrollY > (player_background?.height ?: scrollY)) { + player.handleEvent(CSPlayerEvent.Pause) + } + } + //result_poster_blur_holder?.translationY = -scrollY.toFloat() + }) + + observe(viewModel.episodesCountText) { count -> + result_episodes_text.setText(count) + } + + 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 + } + } + } + + 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.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.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 -> + 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]) + } + } + } + } + + observe(viewModel.rangeSelections) { range -> + result_episode_select?.setOnClickListener { view -> + view?.context?.let { ctx -> + val names = range + .mapNotNull { (text, r) -> + r to (text?.asStringNull(ctx) ?: return@mapNotNull null) + } + + view.popupMenuNoIconsAndNoStringRes(names.mapIndexed { index, (_, name) -> + index to name + }) { + viewModel.changeRange(names[itemId].first) + } + } + } + } + + observe(viewModel.seasonSelections) { seasonList -> + result_season_button?.setOnClickListener { view -> + view?.context?.let { ctx -> + val names = seasonList + .mapNotNull { (text, r) -> + r to (text?.asStringNull(ctx) ?: return@mapNotNull null) + } + + view.popupMenuNoIconsAndNoStringRes(names.mapIndexed { index, (_, name) -> + index to name + }) { + viewModel.changeSeason(names[itemId].first) + } + } + } + } + } + + override fun onPause() { + super.onPause() + PanelsChildGestureRegionObserver.Provider.get().addGestureRegionsUpdateListener(this) + } + + override fun onGestureRegionsUpdate(gestureRegions: List) { + result_overlapping_panels?.setChildGestureRegions(gestureRegions) + } + + override fun setRecommendations(rec: List?, validApiName: String?) { + val isInvalid = rec.isNullOrEmpty() + result_recommendations?.isGone = isInvalid + result_recommendations_btt?.isGone = isInvalid + result_recommendations_btt?.setOnClickListener { + val nextFocusDown = if (result_overlapping_panels?.getSelectedPanel()?.ordinal == 1) { + result_overlapping_panels?.openEndPanel() + R.id.result_recommendations + } else { + result_overlapping_panels?.closePanels() + R.id.result_description + } + + result_recommendations_btt?.nextFocusDownId = nextFocusDown + result_search?.nextFocusDownId = nextFocusDown + result_open_in_browser?.nextFocusDownId = nextFocusDown + result_share?.nextFocusDownId = nextFocusDown + } + result_overlapping_panels?.setEndPanelLockState(if (isInvalid) OverlappingPanelsLayout.LockState.CLOSE else OverlappingPanelsLayout.LockState.UNLOCKED) + + val matchAgainst = validApiName ?: rec?.firstOrNull()?.apiName + rec?.map { it.apiName }?.distinct()?.let { apiNames -> + // very dirty selection + result_recommendations_filter_button?.isVisible = apiNames.size > 1 + result_recommendations_filter_button?.text = matchAgainst + result_recommendations_filter_button?.setOnClickListener { _ -> + activity?.showBottomDialog( + apiNames, + apiNames.indexOf(matchAgainst), + getString(R.string.home_change_provider_img_des), false, {} + ) { + setRecommendations(rec, apiNames[it]) + } + } + } ?: run { + result_recommendations_filter_button?.isVisible = false + } + + result_recommendations?.post { + rec?.let { list -> + (result_recommendations?.adapter as SearchAdapter?)?.updateList(list.filter { it.apiName == matchAgainst }) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt new file mode 100644 index 00000000..895f0724 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt @@ -0,0 +1,208 @@ +package com.lagradost.cloudstream3.ui.result + +import android.app.Dialog +import android.os.Bundle +import android.view.View +import androidx.core.view.isGone +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.lagradost.cloudstream3.DubStatus +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.SearchResponse +import com.lagradost.cloudstream3.mvvm.ResourceSome +import com.lagradost.cloudstream3.mvvm.Some +import com.lagradost.cloudstream3.mvvm.observe +import com.lagradost.cloudstream3.ui.search.SearchAdapter +import com.lagradost.cloudstream3.ui.search.SearchHelper +import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant +import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe +import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage +import kotlinx.android.synthetic.main.fragment_result_tv.* + +class ResultFragmentTv : ResultFragment() { + override val resultLayout = R.layout.fragment_result_tv + + private var currentRecommendations: List = emptyList() + + private fun handleSelection(data: Any) { + when (data) { + is EpisodeRange -> { + viewModel.changeRange(data) + } + is Int -> { + viewModel.changeSeason(data) + } + is DubStatus -> { + viewModel.changeDubStatus(data) + } + is String -> { + setRecommendations(currentRecommendations, data) + } + } + } + + private fun RecyclerView?.select(index: Int) { + (this?.adapter as? SelectAdaptor?)?.select(index, this) + } + + private fun RecyclerView?.update(data: List) { + (this?.adapter as? SelectAdaptor?)?.updateSelectionList(data) + this?.isVisible = data.size > 1 + } + + private fun RecyclerView?.setAdapter() { + this?.adapter = SelectAdaptor { data -> + handleSelection(data) + } + } + + private fun hasNoFocus(): Boolean { + val focus = activity?.currentFocus + if (focus == null || !focus.isVisible) return true + return focus == this.result_root + } + + override fun updateEpisodes(episodes: ResourceSome>) { + super.updateEpisodes(episodes) + if (episodes is ResourceSome.Success && hasNoFocus()) { + result_episodes?.requestFocus() + } + } + + override fun updateMovie(data: ResourceSome>) { + super.updateMovie(data) + if (data is ResourceSome.Success && hasNoFocus()) { + result_play_movie?.requestFocus() + } + } + + override fun setRecommendations(rec: List?, validApiName: String?) { + currentRecommendations = rec ?: emptyList() + val isInvalid = rec.isNullOrEmpty() + result_recommendations?.isGone = isInvalid + result_recommendations_holder?.isGone = isInvalid + val matchAgainst = validApiName ?: rec?.firstOrNull()?.apiName + (result_recommendations?.adapter as SearchAdapter?)?.updateList(rec?.filter { it.apiName == matchAgainst } + ?: emptyList()) + + rec?.map { it.apiName }?.distinct()?.let { apiNames -> + // very dirty selection + result_recommendations_filter_selection?.isVisible = apiNames.size > 1 + result_recommendations_filter_selection?.update(apiNames.map { txt(it) to it }) + result_recommendations_filter_selection?.select(apiNames.indexOf(matchAgainst)) + } ?: run { + result_recommendations_filter_selection?.isVisible = false + } + } + var loadingDialog: Dialog? = null + var popupDialog: Dialog? = null + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + (result_episodes?.adapter as EpisodeAdapter?)?.apply { + layout = R.layout.result_episode_both_tv + } + + result_season_selection.setAdapter() + result_range_selection.setAdapter() + result_dub_selection.setAdapter() + result_recommendations_filter_selection.setAdapter() + + 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 + } + } + } + + 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.episodesCountText) { count -> + result_episodes_text.setText(count) + } + + observe(viewModel.selectedRangeIndex) { selected -> + result_range_selection.select(selected) + } + observe(viewModel.selectedSeasonIndex) { selected -> + result_season_selection.select(selected) + } + observe(viewModel.selectedDubStatusIndex) { selected -> + result_dub_selection.select(selected) + } + observe(viewModel.rangeSelections) { + result_range_selection.update(it) + } + observe(viewModel.dubSubSelections) { + result_dub_selection.update(it) + } + observe(viewModel.seasonSelections) { + result_season_selection.update(it) + } + + result_back?.setOnClickListener { + activity?.popCurrentPage() + } + + result_recommendations?.spanCount = 8 + result_recommendations?.adapter = + SearchAdapter( + ArrayList(), + result_recommendations, + ) { callback -> + SearchHelper.handleSearchClickCallback(activity, callback) + } + } +} \ No newline at end of file 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 14e95734..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt +++ /dev/null @@ -1,631 +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.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() - // TODO: fix - //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 - } - - var validUrlResource = safeApiCall { - SyncRedirector.redirect( - url, - api.mainUrl - ) - } - // TODO: fix - // 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..10f45ecb --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt @@ -0,0 +1,1927 @@ +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.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.lang.Math.abs +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 { if (it <= 0.1f) null else 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: List = listOf() + private var currentDubStatus: List = listOf() + 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 _selectedRangeIndex: MutableLiveData = + MutableLiveData(-1) + val selectedRangeIndex: LiveData = _selectedRangeIndex + + private val _selectedSeasonIndex: MutableLiveData = + MutableLiveData(-1) + val selectedSeasonIndex: LiveData = _selectedSeasonIndex + + private val _selectedDubStatusIndex: MutableLiveData = MutableLiveData(-1) + val selectedDubStatusIndex: LiveData = _selectedDubStatusIndex + + + 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() + // TODO: fix + //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] + + if (ranges?.contains(range) != true) { + // if the current ranges does not include the range then select the range with the closest matching start episode + // this usually happends when dub has less episodes then sub -> the range does not exist + ranges?.minByOrNull { abs(it.startEpisode - range.startEpisode) }?.let { r -> + postEpisodeRange(indexer, r) + return + } + } + + 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), + ) + ) + ) + + _selectedSeasonIndex.postValue( + currentSeasons.indexOf(indexer.season) + ) + + _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 + } + ) + ) + + _selectedRangeIndex.postValue( + ranges?.indexOf(range) ?: -1 + ) + + _selectedRange.postValue( + some( + if (isMovie) null else if ((currentRanges[indexer]?.size ?: 0) > 1) { + txt(R.string.episodes_range, range.startEpisode, range.endEpisode) + } else { + null + } + ) + ) + + _selectedDubStatusIndex.postValue( + currentDubStatus.indexOf(indexer.dubStatus) + ) + + _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) + /*if (ret.isEmpty()) { + val index = ranges?.indexOf(range) + if(index != null && index > 0) { + + } + }*/ + _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.toList() + currentSeasons = seasonsSelection.toList() + _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 + var validUrlResource = safeApiCall { + SyncRedirector.redirect( + url, + api.mainUrl + ) + } + // TODO: fix + // 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/SelectAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt new file mode 100644 index 00000000..eb4aafa1 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt @@ -0,0 +1,128 @@ +package com.lagradost.cloudstream3.ui.result + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.core.view.isVisible +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.button.MaterialButton +import com.lagradost.cloudstream3.ActorData +import com.lagradost.cloudstream3.ActorRole +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.ui.download.DownloadButtonViewHolder +import com.lagradost.cloudstream3.ui.home.ParentItemAdapter +import com.lagradost.cloudstream3.ui.settings.AccountAdapter +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings +import com.lagradost.cloudstream3.utils.UIHelper.setImage +import kotlinx.android.synthetic.main.cast_item.view.* +import org.schabi.newpipe.extractor.timeago.patterns.it + +typealias SelectData = Pair + +class SelectAdaptor(val callback: (Any) -> Unit) : RecyclerView.Adapter() { + private val selection: MutableList = mutableListOf() + private var selectedIndex: Int = -1 + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return SelectViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.result_selection, parent, false), + ) + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (holder) { + is SelectViewHolder -> { + holder.bind(selection[position], position == selectedIndex, callback) + } + } + } + + override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) { + if(holder.itemView.hasFocus()) { + holder.itemView.clearFocus() + } + } + + override fun getItemCount(): Int { + return selection.size + } + + fun select(newIndex: Int, recyclerView: RecyclerView?) { + if(recyclerView == null) return + if(newIndex == selectedIndex) return + val oldIndex = selectedIndex + selectedIndex = newIndex + recyclerView.apply { + for (i in 0 until itemCount) { + val viewHolder = getChildViewHolder( getChildAt(i) ?: continue) ?: continue + val pos = viewHolder.absoluteAdapterPosition + if (viewHolder is SelectViewHolder) { + if (pos == oldIndex) { + viewHolder.update(false) + } else if (pos == newIndex) { + viewHolder.update(true) + } + } + } + } + } + + fun updateSelectionList(newList: List) { + val diffResult = DiffUtil.calculateDiff( + SelectDataCallback(this.selection, newList) + ) + + selection.clear() + selection.addAll(newList) + + diffResult.dispatchUpdatesTo(this) + } + + + private class SelectViewHolder + constructor( + itemView: View, + ) : + RecyclerView.ViewHolder(itemView) { + private val item: MaterialButton = itemView as MaterialButton + + fun update(isSelected: Boolean) { + item.isSelected = isSelected + } + + fun bind( + data: SelectData, isSelected: Boolean, callback: (Any) -> Unit + ) { + val isTrueTv = itemView.context?.isTrueTvSettings() == true + if (isTrueTv) { + item.isFocusable = true + item.isFocusableInTouchMode = true + } + + item.isSelected = isSelected + item.setText(data.first) + item.setOnClickListener { + callback.invoke(data.second) + } + } + } +} + +class SelectDataCallback( + private val oldList: List, + private val newList: List +) : + DiffUtil.Callback() { + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = + oldList[oldItemPosition].second == newList[newItemPosition].second + + override fun getOldListSize() = oldList.size + + override fun getNewListSize() = newList.size + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = + oldList[oldItemPosition] == newList[newItemPosition] +} \ 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..c5955ea0 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,12 @@ 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 + if(!url.startsWith("http")) return@ioSafe + SyncUtil.getIdsFromUrl(url)?.let { (malId, aniListId) -> hasAddedFromUrl.add(url) @@ -166,7 +171,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 +196,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 +214,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 +224,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 +235,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 +258,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..0ca232e1 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt @@ -0,0 +1,172 @@ +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)?.let { + if (this.maxLines == 1) { + it.replace("\n", " ") + } else { + it + } + } + + 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..125778fc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt @@ -44,6 +44,9 @@ import com.lagradost.cloudstream3.isMovieType import com.lagradost.cloudstream3.mapper import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.result.ResultFragment +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.FillerEpisodeCheck.toClassDir import com.lagradost.cloudstream3.utils.JsUnpacker.Companion.load @@ -187,21 +190,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) { @@ -313,6 +316,14 @@ object AppUtils { //private val viewModel: ResultViewModel by activityViewModels() + private fun getResultsId(context: Context) : Int { + return if(context.isTrueTvSettings()) { + R.id.global_to_navigation_results_tv + } else { + R.id.global_to_navigation_results_phone + } + } + fun AppCompatActivity.loadResult( url: String, apiName: String, @@ -322,7 +333,7 @@ object AppUtils { this.runOnUiThread { // viewModelStore.clear() this.navigate( - R.id.global_to_navigation_results, + getResultsId(this.applicationContext ?: return@runOnUiThread), ResultFragment.newInstance(url, apiName, startAction, startValue) ) } @@ -336,7 +347,7 @@ object AppUtils { this?.runOnUiThread { // viewModelStore.clear() this.navigate( - R.id.global_to_navigation_results, + getResultsId(this), ResultFragment.newInstance(card, startAction, startValue) ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt index 6dbde4af..ea16a84d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -89,7 +89,9 @@ object BackupUtils { val newFile = ContentValues().apply { put(MediaStore.MediaColumns.DISPLAY_NAME, displayName) put(MediaStore.MediaColumns.TITLE, displayName) - put(MediaStore.MediaColumns.MIME_TYPE, "application/json") + // While it a json file we store as txt because not + // all file managers support mimetype json + put(MediaStore.MediaColumns.MIME_TYPE, "text/plain") //put(MediaStore.MediaColumns.RELATIVE_PATH, folder) } 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/color/selectable_black.xml b/app/src/main/res/color/selectable_black.xml new file mode 100644 index 00000000..6761eb74 --- /dev/null +++ b/app/src/main/res/color/selectable_black.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/selectable_white.xml b/app/src/main/res/color/selectable_white.xml new file mode 100644 index 00000000..bc012def --- /dev/null +++ b/app/src/main/res/color/selectable_white.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/outline_drawable_less.xml b/app/src/main/res/drawable/outline_drawable_less.xml new file mode 100644 index 00000000..0b641074 --- /dev/null +++ b/app/src/main/res/drawable/outline_drawable_less.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/outline_less.xml b/app/src/main/res/drawable/outline_less.xml new file mode 100644 index 00000000..b8dba5b6 --- /dev/null +++ b/app/src/main/res/drawable/outline_less.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file 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/fragment_result_tv.xml b/app/src/main/res/layout/fragment_result_tv.xml new file mode 100644 index 00000000..0996a355 --- /dev/null +++ b/app/src/main/res/layout/fragment_result_tv.xml @@ -0,0 +1,788 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file 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/result_episode_both_tv.xml b/app/src/main/res/layout/result_episode_both_tv.xml new file mode 100644 index 00000000..239bec11 --- /dev/null +++ b/app/src/main/res/layout/result_episode_both_tv.xml @@ -0,0 +1,20 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/result_episode_large.xml b/app/src/main/res/layout/result_episode_large.xml index 8fc917ec..07e10b78 100644 --- a/app/src/main/res/layout/result_episode_large.xml +++ b/app/src/main/res/layout/result_episode_large.xml @@ -3,11 +3,10 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" - android:nextFocusLeft="@id/episode_poster" android:nextFocusRight="@id/result_episode_download" android:id="@+id/episode_holder_large" - android:layout_width="match_parent" + android:layout_width="wrap_content" android:layout_height="wrap_content" app:cardCornerRadius="@dimen/rounded_image_radius" app:cardBackgroundColor="?attr/boxItemBackground" @@ -19,7 +18,7 @@ android:foreground="?android:attr/selectableItemBackgroundBorderless" android:padding="10dp" android:orientation="vertical" - android:layout_width="match_parent" + android:layout_width="wrap_content" android:layout_height="wrap_content"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/result_episode_tv.xml b/app/src/main/res/layout/result_episode_tv.xml new file mode 100644 index 00000000..e74254bc --- /dev/null +++ b/app/src/main/res/layout/result_episode_tv.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/result_poster.xml b/app/src/main/res/layout/result_poster.xml index 1345d279..0675da7e 100644 --- a/app/src/main/res/layout/result_poster.xml +++ b/app/src/main/res/layout/result_poster.xml @@ -12,6 +12,6 @@ android:scaleType="fitCenter" android:adjustViewBounds="true" android:src="@drawable/default_cover" - android:background="#fffff0" + android:background="?attr/primaryGrayBackground" android:contentDescription="@string/poster_image" /> \ No newline at end of file diff --git a/app/src/main/res/layout/result_selection.xml b/app/src/main/res/layout/result_selection.xml new file mode 100644 index 00000000..fe880dc9 --- /dev/null +++ b/app/src/main/res/layout/result_selection.xml @@ -0,0 +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/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index 8ee04901..746d1096 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -4,10 +4,35 @@ xmlns:tools="http://schemas.android.com/tools" android:id="@+id/mobile_navigation" app:startDestination="@+id/navigation_home"> - + + + + + + + - - - + + + + + + + + + + شارك فتح في الويب تخطي التحميل - …تحميل + …تحميل مشاهدة في الانتظار diff --git a/app/src/main/res/values-bp/strings.xml b/app/src/main/res/values-bp/strings.xml index c1817041..c35fa2c4 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -107,7 +107,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 ebd3b8a8..8ff3e860 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -100,7 +100,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 075df15b..d665a405 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 f4f46a70..b9ed9ec1 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 d0eabbd3..4d0debae 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 9c703053..35cbe652 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 fc91990e..51177594 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 7d697647..6716c86b 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 988646b2..84ce4ae6 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 b687ed28..44952875 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 5b4bee09..83950a70 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -107,7 +107,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 24176d1e..b178d676 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/dimens.xml b/app/src/main/res/values/dimens.xml index 5ac7cbf9..fb9180f3 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -3,6 +3,8 @@ 16dp 16dp 10dp + 4dp + 0dp 2dp 15dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a406cad4..29c8b4de 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -109,7 +109,7 @@ Share Open In Browser Skip Loading - Loading… + Loading… Watching On-Hold @@ -283,9 +283,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..9c6a5902 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -266,6 +266,7 @@ @@ -334,6 +335,10 @@ scrollable--> + + - + + + + +