diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 58c8f1ba..b2061572 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -223,7 +223,7 @@ dependencies { //implementation("com.github.HaarigerHarald:android-youtubeExtractor:master-SNAPSHOT") // newpipe yt taken from https://github.com/TeamNewPipe/NewPipe/blob/dev/app/build.gradle#L204 - implementation("com.github.TeamNewPipe:NewPipeExtractor:master-SNAPSHOT") + implementation("com.github.TeamNewPipe:NewPipeExtractor:8495ad619e") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6") // Library/extensions searching with Levenshtein distance diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt index d439d650..eb12c411 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt @@ -361,14 +361,11 @@ class HomeFragment : Fragment() { ?.toMutableList() ?: mutableListOf(TvType.Movie, TvType.TvSeries) - val cancelBtt = dialog.findViewById(R.id.cancel_btt) - val applyBtt = dialog.findViewById(R.id.apply_btt) - - cancelBtt?.setOnClickListener { + binding.cancelBtt.setOnClickListener { dialog.dismissSafe() } - applyBtt?.setOnClickListener { + binding.applyBtt.setOnClickListener { if (currentApiName != selectedApiName) { currentApiName?.let(callback) } @@ -493,15 +490,54 @@ class HomeFragment : Fragment() { super.onViewCreated(view, savedInstanceState) fixGrid() - binding?.homeChangeApiLoading?.setOnClickListener(apiChangeClickListener) - binding?.homeChangeApiLoading?.setOnClickListener(apiChangeClickListener) - binding?.homeApiFab?.setOnClickListener(apiChangeClickListener) - binding?.homeRandom?.setOnClickListener { - if (listHomepageItems.isNotEmpty()) { - activity.loadSearchResult(listHomepageItems.random()) + binding?.apply { + homeChangeApiLoading.setOnClickListener(apiChangeClickListener) + //homeChangeApiLoading.setOnClickListener(apiChangeClickListener) + homeApiFab.setOnClickListener(apiChangeClickListener) + homeRandom.setOnClickListener { + if (listHomepageItems.isNotEmpty()) { + activity.loadSearchResult(listHomepageItems.random()) + } } + + homeMasterRecycler.adapter = + HomeParentItemAdapterPreview( + mutableListOf(), + homeViewModel + ) + fixPaddingStatusbar(homeLoadingStatusbar) + + if (isTvSettings()) { + homeApiFab.isVisible = false + if (isTrueTvSettings()) { + homeChangeApiLoading.isVisible = true + homeChangeApiLoading.isFocusable = true + homeChangeApiLoading.isFocusableInTouchMode = true + } + // home_bookmark_select?.isFocusable = true + // home_bookmark_select?.isFocusableInTouchMode = true + } else { + homeApiFab.isVisible = true + homeChangeApiLoading.isVisible = false + } + + homeMasterRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + if (dy > 0) { //check for scroll down + homeApiFab.shrink() // hide + homeRandom.shrink() + } else if (dy < -5) { + if (!isTvSettings()) { + homeApiFab.extend() // show + homeRandom.extend() + } + } + super.onScrolled(recyclerView, dx, dy) + } + }) } + //Load value for toggling Random button. Hide at startup context?.let { val settingsManager = PreferenceManager.getDefaultSharedPreferences(it) @@ -551,13 +587,9 @@ class HomeFragment : Fragment() { } is Resource.Failure -> { - homeLoadingShimmer.stopShimmer() - resultErrorText.text = data.errorString - homeReloadConnectionerror.setOnClickListener(apiChangeClickListener) - homeReloadConnectionOpenInBrowser.setOnClickListener { view -> val validAPIs = apis//.filter { api -> api.hasMainPage } @@ -598,7 +630,6 @@ class HomeFragment : Fragment() { //context?.fixPaddingStatusbarView(home_statusbar) //context?.fixPaddingStatusbar(home_padding) - fixPaddingStatusbar(binding?.homeLoadingStatusbar) observeNullable(homeViewModel.popup) { item -> if (item == null) { @@ -620,52 +651,13 @@ class HomeFragment : Fragment() { }) } - binding?.homeMasterRecycler?.adapter = - HomeParentItemAdapterPreview( - mutableListOf(), - homeViewModel - ) - homeViewModel.reloadStored() //loadHomePage(false) - binding?.homeMasterRecycler?.addOnScrollListener(object : RecyclerView.OnScrollListener() { - override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { - - binding?.apply { - if (dy > 0) { //check for scroll down - homeApiFab.shrink() // hide - homeRandom.shrink() - } else if (dy < -5) { - if (!isTvSettings()) { - homeApiFab.extend() // show - homeRandom.extend() - } - } - } - - - super.onScrolled(recyclerView, dx, dy) - } - }) // nice profile pic on homepage //home_profile_picture_holder?.isVisible = false // just in case - binding?.apply { - if (isTvSettings()) { - homeApiFab.isVisible = false - if (isTrueTvSettings()) { - homeChangeApiLoading.isVisible = true - homeChangeApiLoading.isFocusable = true - homeChangeApiLoading.isFocusableInTouchMode = true - } - // home_bookmark_select?.isFocusable = true - // home_bookmark_select?.isFocusableInTouchMode = true - } else { - homeApiFab.isVisible = true - homeChangeApiLoading.isVisible = false - } - } + //TODO READD THIS /*for (syncApi in OAuth2Apis) { val login = syncApi.loginInfo() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt index 272c0e89..a304b43f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt @@ -401,7 +401,7 @@ class HomeParentItemAdapterPreview( observe(viewModel.bookmarks) { updateBookmarks(it) } - observe(viewModel.availableWatchStatusTypes) { (visible, checked) -> + observe(viewModel.availableWatchStatusTypes) { (checked, visible) -> for ((chip, watch) in toggleList) { chip.apply { isVisible = visible.contains(watch) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt index 57cca3e5..a00127fd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt @@ -74,15 +74,15 @@ abstract class AbstractPlayerFragment( var isBuffering = true protected open var hasPipModeSupport = true - lateinit var playerPausePlayHolderHolder : FrameLayout - lateinit var playerPausePlay : ImageView - lateinit var playerBuffering : ProgressBar + var playerPausePlayHolderHolder : FrameLayout? = null + var playerPausePlay : ImageView? = null + var playerBuffering : ProgressBar? = null var playerView : PlayerView? = null var piphide : FrameLayout? = null var subtitleHolder : FrameLayout? = null @LayoutRes - protected var layout: Int = R.layout.fragment_player + protected open var layout: Int = R.layout.fragment_player open fun nextEpisode() { throw NotImplementedError() @@ -141,15 +141,15 @@ abstract class AbstractPlayerFragment( isBuffering = CSPlayerLoading.IsBuffering == isPlaying if (isBuffering) { - playerPausePlayHolderHolder.isVisible = false - playerBuffering.isVisible = true + playerPausePlayHolderHolder?.isVisible = false + playerBuffering?.isVisible = true } else { - playerPausePlayHolderHolder.isVisible = true - playerBuffering.isVisible = false + playerPausePlayHolderHolder?.isVisible = true + playerBuffering?.isVisible = false if (wasPlaying != isPlaying) { - playerPausePlay.setImageResource(if (isPlayingRightNow) R.drawable.play_to_pause else R.drawable.pause_to_play) - val drawable = playerPausePlay.drawable + playerPausePlay?.setImageResource(if (isPlayingRightNow) R.drawable.play_to_pause else R.drawable.pause_to_play) + val drawable = playerPausePlay?.drawable var startedAnimation = false if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { @@ -171,10 +171,10 @@ abstract class AbstractPlayerFragment( // somehow the phone is wacked if (!startedAnimation) { - playerPausePlay.setImageResource(if (isPlayingRightNow) R.drawable.netflix_pause else R.drawable.netflix_play) + playerPausePlay?.setImageResource(if (isPlayingRightNow) R.drawable.netflix_pause else R.drawable.netflix_play) } } else { - playerPausePlay.setImageResource(if (isPlayingRightNow) R.drawable.netflix_pause else R.drawable.netflix_play) + playerPausePlay?.setImageResource(if (isPlayingRightNow) R.drawable.netflix_pause else R.drawable.netflix_play) } } 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 0b7c00ed..3dfb1f9c 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 @@ -10,7 +10,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView -import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.core.view.isGone import androidx.core.view.isVisible @@ -22,18 +21,16 @@ import com.google.android.material.chip.ChipDrawable import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.APIHolder.updateHasTrailers -import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.DubStatus import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.mvvm.* -import com.lagradost.cloudstream3.services.SubscriptionWorkManager import com.lagradost.cloudstream3.syncproviders.providers.Kitsu import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick -import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment +import com.lagradost.cloudstream3.ui.player.FullScreenPlayer import com.lagradost.cloudstream3.ui.result.EpisodeAdapter.Companion.getPlayerAction import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings @@ -47,7 +44,6 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.getVideoWatchState import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos 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.hideKeyboard import kotlinx.android.synthetic.main.fragment_result.download_button import kotlinx.android.synthetic.main.fragment_result.result_cast_items @@ -89,10 +85,8 @@ 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.result_sync.* -import kotlinx.android.synthetic.main.trailer_custom_layout.* +import kotlinx.android.synthetic.main.fragment_result_tv.result_resume_series_button_play +import kotlinx.android.synthetic.main.fragment_result_tv.temporary_no_focus import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking @@ -194,7 +188,7 @@ fun ResultEpisode.getWatchProgress(): Float { return (getDisplayPosition() / 1000).toFloat() / (duration / 1000).toFloat() } -open class ResultFragment : ResultTrailerPlayer() { +open class ResultFragment : FullScreenPlayer() { companion object { const val URL_BUNDLE = "url" const val API_NAME_BUNDLE = "apiName" @@ -251,7 +245,9 @@ open class ResultFragment : ResultTrailerPlayer() { protected lateinit var viewModel: ResultViewModel2 //by activityViewModels() protected lateinit var syncModel: SyncViewModel - protected open val resultLayout = R.layout.fragment_result_swipe + //protected open val resultLayout = R.layout.fragment_result_swipe + + override var layout = R.layout.fragment_result_swipe override fun onCreateView( inflater: LayoutInflater, @@ -263,7 +259,8 @@ open class ResultFragment : ResultTrailerPlayer() { syncModel = ViewModelProvider(this)[SyncViewModel::class.java] - return inflater.inflate(resultLayout, container, false) + return super.onCreateView(inflater, container, savedInstanceState) + //return inflater.inflate(resultLayout, container, false) } override fun onDestroyView() { @@ -285,167 +282,12 @@ open class ResultFragment : ResultTrailerPlayer() { super.onDestroy() } - /// 0 = LOADING, 1 = ERROR LOADING, 2 = LOADED - private fun updateVisStatus(state: Int) { - when (state) { - 0 -> { - result_bookmark_fab?.isGone = true - result_loading?.isVisible = true - result_finish_loading?.isVisible = false - result_loading_error?.isVisible = false - } - - 1 -> { - result_bookmark_fab?.isGone = true - result_loading?.isVisible = false - result_finish_loading?.isVisible = false - result_loading_error?.isVisible = true - result_reload_connection_open_in_browser?.isVisible = true - } - - 2 -> { - result_bookmark_fab?.isGone = isTrueTvSettings() - result_bookmark_fab?.extend() - //if (result_bookmark_button?.context?.isTrueTvSettings() == true) { - // when { - // result_play_movie?.isVisible == true -> { - // result_play_movie?.requestFocus() - // } - // result_resume_series_button?.isVisible == true -> { - // result_resume_series_button?.requestFocus() - // } - // else -> { - // result_bookmark_button?.requestFocus() - // } - // } - //} - - result_loading?.isVisible = false - result_finish_loading?.isVisible = true - result_loading_error?.isVisible = false - } - } - } - - open fun setRecommendations(rec: List?, validApiName: String?) { - - } private fun updateUI() { syncModel.updateUserData() viewModel.reloadEpisodes() } - open fun updateMovie(data: Resource>?) { - when (data) { - is Resource.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 - } - - val show = - viewModel.currentRepo?.api?.hasDownloadSupport == true && !isTvSettings() - if (show) { - download_button?.setDefaultClickListener( - VideoDownloadHelper.DownloadEpisodeCached( - ep.name, - ep.poster, - 0, - null, - ep.id, - ep.id, - null, - null, - System.currentTimeMillis(), - ) - ) { click -> - when (click.action) { - DOWNLOAD_ACTION_DOWNLOAD -> { - viewModel.handleAction( - activity, - EpisodeClickEvent(ACTION_DOWNLOAD_EPISODE, ep) - ) - } - - else -> handleDownloadClick(click) - } - } - } - download_button?.isVisible = show - } - } - - else -> { - download_button?.isVisible = false - result_play_movie?.isVisible = false - } - } - } - - open fun updateEpisodes(episodes: Resource>?) { - when (episodes) { - is Resource.Loading -> { - result_episode_loading?.isVisible = true - result_episodes?.isVisible = false - } - - is Resource.Success -> { - result_episodes?.isVisible = true - result_episode_loading?.isVisible = false - - /* - * Okay so what is this fuckery? - * Basically Android TV will crash if you request a new focus while - * the adapter gets updated. - * - * This means that if you load thumbnails and request a next focus at the same time - * the app will crash without any way to catch it! - * - * How to bypass this? - * This code basically steals the focus for 500ms and puts it in an inescapable view - * then lets out the focus by requesting focus to result_episodes - */ - - // Do not use this.isTv, that is the player - val isTv = isTvSettings() - val hasEpisodes = - !(result_episodes?.adapter as? EpisodeAdapter?)?.cardList.isNullOrEmpty() - - if (isTv && hasEpisodes) { - // Make it impossible to focus anywhere else! - temporary_no_focus?.isFocusable = true - temporary_no_focus?.requestFocus() - } - - (result_episodes?.adapter as? EpisodeAdapter)?.updateList(episodes.value) - - if (isTv && hasEpisodes) main { - delay(500) - temporary_no_focus?.isFocusable = false - // This might make some people sad as it changes the focus when leaving an episode :( - result_episodes?.requestFocus() - } - } - - else -> { - result_episode_loading?.isVisible = false - result_episodes?.isVisible = false - } - } - } - data class StoredData( val url: String?, val apiName: String, @@ -455,7 +297,7 @@ open class ResultFragment : ResultTrailerPlayer() { val playerAction: Int ) - private fun getStoredData(context: Context): StoredData? { + fun getStoredData(context: Context): StoredData? { val settingsManager = PreferenceManager.getDefaultSharedPreferences(context) val url = arguments?.getString(URL_BUNDLE) val apiName = arguments?.getString(API_NAME_BUNDLE) ?: return null @@ -537,7 +379,6 @@ open class ResultFragment : ResultTrailerPlayer() { context?.updateHasTrailers() activity?.loadCache() - fixPaddingStatusbar(result_top_bar) //activity?.fixPaddingStatusbar(result_barstatus) /* val backParameter = result_back.layoutParams as FrameLayout.LayoutParams @@ -554,35 +395,6 @@ open class ResultFragment : ResultTrailerPlayer() { val storedData = (activity ?: context)?.let { getStoredData(it) } - syncModel.addFromUrl(storedData?.url) - - val api = getApiFromNameNull(storedData?.apiName) - - result_episodes?.adapter = - EpisodeAdapter( - api?.hasDownloadSupport == true && !isTvSettings(), - { episodeClick -> - viewModel.handleAction(activity, episodeClick) - }, - { downloadClickEvent -> - handleDownloadClick(downloadClickEvent) - } - ) - - - observe(viewModel.episodeSynopsis) { description -> - view.context?.let { ctx -> - val builder: AlertDialog.Builder = - AlertDialog.Builder(ctx, R.style.AlertDialogCustom) - builder.setMessage(description.html()) - .setTitle(R.string.synopsis) - .setOnDismissListener { - viewModel.releaseEpisodeSynopsis() - } - .show() - } - } - // This is to band-aid FireTV navigation val isTv = isTvSettings() @@ -638,215 +450,6 @@ open class ResultFragment : ResultTrailerPlayer() { result_resume_series_button_play?.setOnClickListener(click) } - - - observeNullable(viewModel.episodes) { episodes -> - updateEpisodes(episodes) - } - - result_cast_items?.setOnFocusChangeListener { _, hasFocus -> - // Always escape focus - if (hasFocus) result_bookmark_button?.requestFocus() - } - - observe(viewModel.trailers) { trailers -> - setTrailers(trailers.flatMap { it.mirros }) // I dont care about subtitles yet! - } - - observe(viewModel.recommendations) { recommendations -> - setRecommendations(recommendations, null) - } - - observeNullable(viewModel.movie) { data -> - updateMovie(data) - } - - observe(viewModel.page) { data -> - if (data == null) return@observe - when (data) { - is Resource.Success -> { - val d = data.value - - updateVisStatus(2) - - result_vpn.setText(d.vpnText) - result_info.setText(d.metaText) - result_no_episodes.setText(d.noEpisodesFoundText) - result_title.setText(d.titleText) - result_meta_site.setText(d.apiName) - result_meta_type.setText(d.typeText) - result_meta_year.setText(d.yearText) - result_meta_duration.setText(d.durationText) - result_meta_rating.setText(d.ratingText) - result_cast_text.setText(d.actorsText) - result_next_airing.setText(d.nextAiringEpisode) - result_next_airing_time.setText(d.nextAiringDate) - result_poster.setImage(d.posterImage) - result_poster_background.setImage(d.posterBackgroundImage) - //result_trailer_thumbnail.setImage(d.posterBackgroundImage, fadeIn = false) - - if (d.posterImage != null && !isTrueTvSettings()) - result_poster_holder?.setOnClickListener { - try { - context?.let { ctx -> - runBlocking { - val sourceBuilder = AlertDialog.Builder(ctx) - sourceBuilder.setView(R.layout.result_poster) - - val sourceDialog = sourceBuilder.create() - sourceDialog.show() - - sourceDialog.findViewById(R.id.imgPoster) - ?.apply { - setImage(d.posterImage) - setOnClickListener { - sourceDialog.dismissSafe() - } - } - } - } - } catch (e: Exception) { - logError(e) - } - } - - - result_cast_items?.isVisible = d.actors != null - (result_cast_items?.adapter as? ActorAdaptor)?.apply { - updateList(d.actors ?: emptyList()) - } - - observeNullable(viewModel.subscribeStatus) { isSubscribed -> - result_subscribe?.isVisible = isSubscribed != null - if (isSubscribed == null) return@observeNullable - - val drawable = if (isSubscribed) { - R.drawable.ic_baseline_notifications_active_24 - } else { - R.drawable.baseline_notifications_none_24 - } - - result_subscribe?.setImageResource(drawable) - } - - result_subscribe?.setOnClickListener { - val isSubscribed = - viewModel.toggleSubscriptionStatus() ?: return@setOnClickListener - - val message = if (isSubscribed) { - // Kinda icky to have this here, but it works. - SubscriptionWorkManager.enqueuePeriodicWork(context) - R.string.subscription_new - } else { - R.string.subscription_deleted - } - - val name = (viewModel.page.value as? Resource.Success)?.value?.title - ?: txt(R.string.no_data).asStringNull(context) ?: "" - showToast(txt(message, name), Toast.LENGTH_SHORT) - } - - 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) - try { - startActivity(i) - } catch (e: Exception) { - logError(e) - } - } - - result_search?.setOnClickListener { - QuickSearchFragment.pushSearch(activity, d.title) - } - - result_share?.setOnClickListener { - try { - val i = Intent(ACTION_SEND) - i.type = "text/plain" - i.putExtra(EXTRA_SUBJECT, d.title) - i.putExtra(EXTRA_TEXT, d.url) - startActivity(createChooser(i, d.title)) - } catch (e: Exception) { - logError(e) - } - } - - if (syncModel.addSyncs(d.syncData)) { - syncModel.updateMetaAndUser() - syncModel.updateSynced() - } else { - syncModel.addFromUrl(d.url) - } - - 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() - } - } - - - d.comingSoon.let { soon -> - result_coming_soon?.isVisible = soon - result_data_holder?.isGone = soon - } - - val tags = d.tags - result_tag_holder?.isVisible = tags.isNotEmpty() - result_tag?.apply { - removeAllViews() - tags.forEach { tag -> - val chip = Chip(context) - val chipDrawable = ChipDrawable.createFromAttributes( - context, - null, - 0, - R.style.ChipFilled - ) - chip.setChipDrawable(chipDrawable) - chip.text = tag - chip.isChecked = false - chip.isCheckable = false - chip.isFocusable = false - chip.isClickable = false - chip.setTextColor(context.colorFromAttribute(R.attr.textColor)) - addView(chip) - } - } - // if (tags.isNotEmpty()) { - //result_tag_holder?.visibility = VISIBLE - //val isOnTv = isTrueTvSettings() - - - /*for ((index, tag) in tags.withIndex()) { - val viewBtt = layoutInflater.inflate(R.layout.result_tag, null) - val btt = viewBtt.findViewById(R.id.result_tag_card) - btt.text = tag - btt.isFocusable = !isOnTv - btt.isClickable = !isOnTv - result_tag?.addView(viewBtt, index) - }*/ - //} - } - - is Resource.Failure -> { - result_error_text.text = storedData?.url?.plus("\n") + data.errorString - updateVisStatus(1) - } - - is Resource.Loading -> { - updateVisStatus(0) - } - } - } - context?.let { ctx -> //result_bookmark_button?.isVisible = ctx.isTvSettings() @@ -879,17 +482,6 @@ open class ResultFragment : ResultTrailerPlayer() { } } - result_open_in_browser?.isVisible = storedData.url.startsWith("http") - result_open_in_browser?.setOnClickListener { - val i = Intent(ACTION_VIEW) - i.data = Uri.parse(storedData.url) - try { - startActivity(i) - } catch (e: Exception) { - logError(e) - } - } - // bloats the navigation on tv if (!isTrueTvSettings()) { result_meta_site?.setOnClickListener { 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 index 4c711126..1dc7b69d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt @@ -1,8 +1,11 @@ package com.lagradost.cloudstream3.ui.result +import android.annotation.SuppressLint import android.app.Dialog +import android.content.Intent import android.content.res.ColorStateList import android.graphics.Rect +import android.net.Uri import android.os.Build import android.os.Bundle import android.text.Editable @@ -15,6 +18,7 @@ import android.view.animation.DecelerateInterpolator import android.widget.AbsListView import android.widget.ArrayAdapter import android.widget.Toast +import androidx.appcompat.app.AlertDialog import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.widget.NestedScrollView @@ -41,12 +45,19 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.mvvm.observeNullable +import com.lagradost.cloudstream3.services.SubscriptionWorkManager import com.lagradost.cloudstream3.ui.WatchType +import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD +import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup import com.lagradost.cloudstream3.ui.player.CSPlayerEvent +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 +import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable import com.lagradost.cloudstream3.utils.AppUtils.openBrowser +import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant @@ -55,14 +66,25 @@ import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage +import com.lagradost.cloudstream3.utils.UIHelper.populateChips import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes +import com.lagradost.cloudstream3.utils.UIHelper.setImage +import com.lagradost.cloudstream3.utils.VideoDownloadHelper +import kotlinx.android.synthetic.main.download_button.result_download_movie +import kotlinx.android.synthetic.main.fragment_result.download_button +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_play_movie +import kotlinx.android.synthetic.main.fragment_result_tv.temporary_no_focus +import kotlinx.coroutines.delay -class ResultFragmentPhone : ResultFragment() { - private var binding: FragmentResultSwipeBinding? = null - private var resultBinding: FragmentResultBinding? = null - private var recommendationBinding: ResultRecommendationsBinding? = null - private var syncBinding: ResultSyncBinding? = null +open class ResultFragmentPhone : ResultFragment(), + PanelsChildGestureRegionObserver.GestureRegionsListener { + protected var binding: FragmentResultSwipeBinding? = null + protected var resultBinding: FragmentResultBinding? = null + protected var recommendationBinding: ResultRecommendationsBinding? = null + protected var syncBinding: ResultSyncBinding? = null override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -205,43 +227,158 @@ class ResultFragmentPhone : ResultFragment() { var selectSeason: String? = null + private fun setUrl(url : String?) { + if(url == null) { + binding?.resultOpenInBrowser?.isVisible = false + return + } + + binding?.resultOpenInBrowser?.apply { + isVisible = url.startsWith("http") + setOnClickListener { + val i = Intent(Intent.ACTION_VIEW) + i.data = Uri.parse(url) + try { + startActivity(i) + } catch (e: Exception) { + logError(e) + } + } + } + } + + @SuppressLint("SetTextI18n") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val apiName = arguments?.getString(API_NAME_BUNDLE) ?: return super.onViewCreated(view, savedInstanceState) + UIHelper.fixPaddingStatusbar(binding?.resultTopBar) + val storedData = (activity ?: context)?.let { + getStoredData(it) + } + setUrl(storedData?.url) + syncModel.addFromUrl(storedData?.url) - playerBinding?.playerOpenSource?.setOnClickListener { - currentTrailers.getOrNull(currentTrailerIndex)?.let { - context?.openBrowser(it.url) + val api = APIHolder.getApiFromNameNull(apiName) + + resultBinding?.apply { + resultEpisodes.adapter = + EpisodeAdapter( + api?.hasDownloadSupport == true, + { episodeClick -> + viewModel.handleAction(activity, episodeClick) + }, + { downloadClickEvent -> + DownloadButtonSetup.handleDownloadClick(downloadClickEvent) + } + ) + resultCastItems.let { + PanelsChildGestureRegionObserver.Provider.get().register(it) + } + resultScroll.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY -> + val dy = scrollY - oldScrollY + if (dy > 0) { //check for scroll down + binding?.resultBookmarkFab?.shrink() + } else if (dy < -5) { + binding?.resultBookmarkFab?.extend() + } + if (!isFullScreenPlayer && player.getIsPlaying()) { + if (scrollY > (resultBinding?.fragmentTrailer?.playerBackground?.height + ?: scrollY) + ) { + player.handleEvent(CSPlayerEvent.Pause) + } + } + }) + } + + binding?.apply { + resultOverlappingPanels.setStartPanelLockState(OverlappingPanelsLayout.LockState.CLOSE) + resultOverlappingPanels.setEndPanelLockState(OverlappingPanelsLayout.LockState.CLOSE) + resultBack.setOnClickListener { + activity?.popCurrentPage() + } + resultMiniSync.adapter = ImageAdapter( + nextFocusDown = R.id.result_sync_set_score, + clickCallback = { action -> + if (action == IMAGE_CLICK || action == IMAGE_LONG_CLICK) { + if (binding?.resultOverlappingPanels?.getSelectedPanel()?.ordinal == 1) { + binding?.resultOverlappingPanels?.openStartPanel() + } else { + binding?.resultOverlappingPanels?.closePanels() + } + } + }) + resultSubscribe.setOnClickListener { + val isSubscribed = + viewModel.toggleSubscriptionStatus() ?: return@setOnClickListener + + val message = if (isSubscribed) { + // Kinda icky to have this here, but it works. + SubscriptionWorkManager.enqueuePeriodicWork(context) + R.string.subscription_new + } else { + R.string.subscription_deleted + } + + val name = (viewModel.page.value as? Resource.Success)?.value?.title + ?: txt(R.string.no_data).asStringNull(context) ?: "" + CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT) + } + mediaRouteButton.apply { + val chromecastSupport = api?.hasChromecastSupport == true + alpha = if (chromecastSupport) 1f else 0.3f + if (!chromecastSupport) { + setOnClickListener { + CommonActivity.showToast( + R.string.no_chromecast_support_toast, + Toast.LENGTH_LONG + ) + } + } + activity?.let { act -> + if (act.isCastApiAvailable()) { + try { + CastButtonFactory.setUpMediaRouteButton(act, this) + val castContext = CastContext.getSharedInstance(act.applicationContext) + isGone = castContext.castState == CastState.NO_DEVICES_AVAILABLE + // this shit leaks for some reason + //castContext.addCastStateListener { state -> + // media_route_button?.isGone = state == CastState.NO_DEVICES_AVAILABLE + //} + } catch (e: Exception) { + logError(e) + } + } + } } } - binding?.resultOverlappingPanels?.setStartPanelLockState(OverlappingPanelsLayout.LockState.CLOSE) - binding?.resultOverlappingPanels?.setEndPanelLockState(OverlappingPanelsLayout.LockState.CLOSE) - recommendationBinding?.resultRecommendationsList?.apply { - spanCount = 3 - adapter = - SearchAdapter( - ArrayList(), - this, - ) { callback -> - SearchHelper.handleSearchClickCallback(callback) + playerBinding?.apply { + playerOpenSource.setOnClickListener { + currentTrailers.getOrNull(currentTrailerIndex)?.let { + context?.openBrowser(it.url) } + } + } + + recommendationBinding?.apply { + resultRecommendationsList.apply { + spanCount = 3 + adapter = + SearchAdapter( + ArrayList(), + this, + ) { callback -> + SearchHelper.handleSearchClickCallback(callback) + } + } } PanelsChildGestureRegionObserver.Provider.get().addGestureRegionsUpdateListener(this) - resultBinding?.resultCastItems?.let { - PanelsChildGestureRegionObserver.Provider.get().register(it) - } - - - binding?.resultBack?.setOnClickListener { - activity?.popCurrentPage() - } - /* result_bookmark_button?.setOnClickListener { it.popupMenuNoIcons( @@ -253,62 +390,165 @@ class ResultFragmentPhone : ResultFragment() { } }*/ - binding?.resultMiniSync?.adapter = ImageAdapter( - nextFocusDown = R.id.result_sync_set_score, - clickCallback = { action -> - if (action == IMAGE_CLICK || action == IMAGE_LONG_CLICK) { - if (binding?.resultOverlappingPanels?.getSelectedPanel()?.ordinal == 1) { - binding?.resultOverlappingPanels?.openStartPanel() + observeNullable(viewModel.subscribeStatus) { isSubscribed -> + binding?.resultSubscribe?.isVisible = isSubscribed != null + if (isSubscribed == null) return@observeNullable + + val drawable = if (isSubscribed) { + R.drawable.ic_baseline_notifications_active_24 + } else { + R.drawable.baseline_notifications_none_24 + } + + binding?.resultSubscribe?.setImageResource(drawable) + } + + observe(viewModel.trailers) { trailers -> + setTrailers(trailers.flatMap { it.mirros }) // I dont care about subtitles yet! + } + + observeNullable(viewModel.episodes) { episodes -> + resultBinding?.apply { + // no failure? + resultEpisodeLoading.isVisible = episodes is Resource.Loading + resultEpisodes.isVisible = episodes is Resource.Success + if(episodes is Resource.Success) { + (resultEpisodes.adapter as? EpisodeAdapter)?.updateList(episodes.value) + } + } + } + + observeNullable(viewModel.movie) { data -> + resultBinding?.apply { + resultPlayMovie.isVisible = data is Resource.Success + downloadButton.isVisible = + data is Resource.Success && viewModel.currentRepo?.api?.hasDownloadSupport == true + + (data as? Resource.Success)?.value?.let { (text, ep) -> + resultPlayMovie.setText(text) + resultPlayMovie.setOnClickListener { + viewModel.handleAction( + activity, + EpisodeClickEvent(ACTION_CLICK_DEFAULT, ep) + ) + } + resultPlayMovie.setOnLongClickListener { + viewModel.handleAction( + activity, + EpisodeClickEvent(ACTION_SHOW_OPTIONS, ep) + ) + return@setOnLongClickListener true + } + downloadButton.setDefaultClickListener( + VideoDownloadHelper.DownloadEpisodeCached( + ep.name, + ep.poster, + 0, + null, + ep.id, + ep.id, + null, + null, + System.currentTimeMillis(), + ) + ) { click -> + when (click.action) { + DOWNLOAD_ACTION_DOWNLOAD -> { + viewModel.handleAction( + activity, + EpisodeClickEvent(ACTION_DOWNLOAD_EPISODE, ep) + ) + } + + else -> DownloadButtonSetup.handleDownloadClick(click) + } + } + } + } + } + + observe(viewModel.page) { data -> + if (data == null) return@observe + resultBinding?.apply { + (data as? Resource.Success)?.value?.let { d -> + resultVpn.setText(d.vpnText) + resultInfo.setText(d.metaText) + resultNoEpisodes.setText(d.noEpisodesFoundText) + resultTitle.setText(d.titleText) + resultMetaSite.setText(d.apiName) + resultMetaType.setText(d.typeText) + resultMetaYear.setText(d.yearText) + resultMetaDuration.setText(d.durationText) + resultMetaRating.setText(d.ratingText) + resultCastText.setText(d.actorsText) + resultNextAiring.setText(d.nextAiringEpisode) + resultNextAiringTime.setText(d.nextAiringDate) + resultPoster.setImage(d.posterImage) + resultPosterBackground.setImage(d.posterBackgroundImage) + resultDescription.setTextHtml(d.plotText) + resultDescription.setOnClickListener { view -> + // todo bottom 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() + } + } + + populateChips(resultTag, d.tags) + + resultComingSoon.isVisible = d.comingSoon + resultDataHolder.isGone = d.comingSoon + + resultCastItems.isGone = d.actors.isNullOrEmpty() + (resultCastItems.adapter as? ActorAdaptor)?.updateList(d.actors ?: emptyList()) + + if (syncModel.addSyncs(d.syncData)) { + syncModel.updateMetaAndUser() + syncModel.updateSynced() } else { - binding?.resultOverlappingPanels?.closePanels() + syncModel.addFromUrl(d.url) + } + + binding?.apply { + resultSearch.setOnClickListener { + QuickSearchFragment.pushSearch(activity, d.title) + } + + resultShare.setOnClickListener { + try { + val i = Intent(Intent.ACTION_SEND) + i.type = "text/plain" + i.putExtra(Intent.EXTRA_SUBJECT, d.title) + i.putExtra(Intent.EXTRA_TEXT, d.url) + startActivity(Intent.createChooser(i, d.title)) + } catch (e: Exception) { + logError(e) + } + } + + setUrl(d.url) + resultBookmarkFab.apply { + isVisible = true + extend() + } } } - }) + (data as? Resource.Failure)?.let { data -> + resultErrorText.text = (storedData?.url?.plus("\n") ?: "") + data.errorString + } - resultBinding?.resultScroll?.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY -> - val dy = scrollY - oldScrollY - if (dy > 0) { //check for scroll down - binding?.resultBookmarkFab?.shrink() - } else if (dy < -5) { - binding?.resultBookmarkFab?.extend() - } - if (!isFullScreenPlayer && player.getIsPlaying()) { - if (scrollY > (resultBinding?.fragmentTrailer?.playerBackground?.height - ?: scrollY) - ) { - player.handleEvent(CSPlayerEvent.Pause) - } - } - //result_poster_blur_holder?.translationY = -scrollY.toFloat() - }) - val api = APIHolder.getApiFromNameNull(apiName) + binding?.resultBookmarkFab?.isVisible = data is Resource.Success + resultFinishLoading.isVisible = data is Resource.Success - binding?.mediaRouteButton?.apply { - val chromecastSupport = api?.hasChromecastSupport == true - alpha = if (chromecastSupport) 1f else 0.3f - if (!chromecastSupport) { - setOnClickListener { - CommonActivity.showToast( - R.string.no_chromecast_support_toast, - Toast.LENGTH_LONG - ) - } - } - activity?.let { act -> - if (act.isCastApiAvailable()) { - try { - CastButtonFactory.setUpMediaRouteButton(act, this) - val castContext = CastContext.getSharedInstance(act.applicationContext) - isGone = castContext.castState == CastState.NO_DEVICES_AVAILABLE - // this shit leaks for some reason - //castContext.addCastStateListener { state -> - // media_route_button?.isGone = state == CastState.NO_DEVICES_AVAILABLE - //} - } catch (e: Exception) { - logError(e) - } - } + resultLoading.isVisible = data is Resource.Loading + + resultLoadingError.isVisible = data is Resource.Failure + resultErrorText.isVisible = data is Resource.Failure + resultReloadConnectionOpenInBrowser.isVisible = data is Resource.Failure } } @@ -350,6 +590,7 @@ class ResultFragmentPhone : ResultFragment() { (binding?.resultMiniSync?.adapter as? ImageAdapter)?.updateList(newList.mapNotNull { it.icon }) } + var currentSyncProgress = 0 fun setSyncMaxEpisodes(totalEpisodes: Int?) { syncBinding?.resultSyncEpisodes?.max = (totalEpisodes ?: 0) * 1000 @@ -439,7 +680,22 @@ class ResultFragmentPhone : ResultFragment() { } binding?.resultOverlappingPanels?.setStartPanelLockState(if (closed) OverlappingPanelsLayout.LockState.CLOSE else OverlappingPanelsLayout.LockState.UNLOCKED) } - + observe(viewModel.recommendations) { recommendations -> + setRecommendations(recommendations, null) + } + observe(viewModel.episodeSynopsis) { description -> + // TODO bottom dialog + view.context?.let { ctx -> + val builder: AlertDialog.Builder = + AlertDialog.Builder(ctx, R.style.AlertDialogCustom) + builder.setMessage(description.html()) + .setTitle(R.string.synopsis) + .setOnDismissListener { + viewModel.releaseEpisodeSynopsis() + } + .show() + } + } context?.let { ctx -> val arrayAdapter = ArrayAdapter(ctx, R.layout.sort_bottom_single_choice) /* @@ -653,7 +909,7 @@ class ResultFragmentPhone : ResultFragment() { binding?.resultOverlappingPanels?.setChildGestureRegions(gestureRegions) } - override fun setRecommendations(rec: List?, validApiName: String?) { + private fun setRecommendations(rec: List?, validApiName: String?) { val isInvalid = rec.isNullOrEmpty() val matchAgainst = validApiName ?: rec?.firstOrNull()?.apiName 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 index 73934490..ce7b6850 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt @@ -1,11 +1,13 @@ package com.lagradost.cloudstream3.ui.result +import android.annotation.SuppressLint import android.app.Dialog import android.content.res.ColorStateList import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView @@ -20,19 +22,32 @@ import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.mvvm.observeNullable import com.lagradost.cloudstream3.ui.WatchType +import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD +import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup import com.lagradost.cloudstream3.ui.player.ExtractorLinkGenerator import com.lagradost.cloudstream3.ui.player.GeneratorPlayer import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchHelper +import com.lagradost.cloudstream3.ui.settings.SettingsFragment +import com.lagradost.cloudstream3.utils.AppUtils.html +import com.lagradost.cloudstream3.utils.Coroutines.main 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 import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage +import com.lagradost.cloudstream3.utils.UIHelper.setImage +import com.lagradost.cloudstream3.utils.VideoDownloadHelper +import kotlinx.android.synthetic.main.fragment_result.download_button +import kotlinx.android.synthetic.main.fragment_result.result_episodes +import kotlinx.android.synthetic.main.fragment_result.result_play_movie +import kotlinx.android.synthetic.main.fragment_result_tv.temporary_no_focus +import kotlinx.coroutines.delay class ResultFragmentTv : ResultFragment() { - override val resultLayout = R.layout.fragment_result_tv + override var layout = R.layout.fragment_result_tv private var binding: FragmentResultTvBinding? = null @@ -95,20 +110,6 @@ class ResultFragmentTv : ResultFragment() { return focus == binding?.resultRoot } - override fun updateEpisodes(episodes: Resource>?) { - super.updateEpisodes(episodes) - if (episodes is Resource.Success && hasNoFocus()) { - binding?.resultEpisodes?.requestFocus() - } - } - - override fun updateMovie(data: Resource>?) { - super.updateMovie(data) - if (data is Resource.Success && hasNoFocus()) { - binding?.resultPlayMovie?.requestFocus() - } - } - override fun setTrailers(trailers: List?) { context?.updateHasTrailers() if (!LoadResponse.isTrailersEnabled) return @@ -128,7 +129,7 @@ class ResultFragmentTv : ResultFragment() { } } - override fun setRecommendations(rec: List?, validApiName: String?) { + private fun setRecommendations(rec: List?, validApiName: String?) { currentRecommendations = rec ?: emptyList() val isInvalid = rec.isNullOrEmpty() binding?.apply { @@ -151,6 +152,8 @@ class ResultFragmentTv : ResultFragment() { var loadingDialog: Dialog? = null var popupDialog: Dialog? = null + + @SuppressLint("SetTextI18n") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding?.apply { @@ -163,6 +166,34 @@ class ResultFragmentTv : ResultFragment() { resultRangeSelection.setAdapter() resultDubSelection.setAdapter() resultRecommendationsFilterSelection.setAdapter() + + resultCastItems.setOnFocusChangeListener { _, hasFocus -> + // Always escape focus + if (hasFocus) binding?.resultBookmarkButton?.requestFocus() + } + resultBack.setOnClickListener { + activity?.popCurrentPage() + } + + resultRecommendationsList.spanCount = 8 + resultRecommendationsList.adapter = + SearchAdapter( + ArrayList(), + resultRecommendationsList, + ) { callback -> + SearchHelper.handleSearchClickCallback(callback) + } + + resultEpisodes.adapter = + EpisodeAdapter( + false, + { episodeClick -> + viewModel.handleAction(activity, episodeClick) + }, + { downloadClickEvent -> + DownloadButtonSetup.handleDownloadClick(downloadClickEvent) + } + ) } observe(viewModel.watchStatus) { watchType -> @@ -181,6 +212,30 @@ class ResultFragmentTv : ResultFragment() { } } + observeNullable(viewModel.movie) { data -> + binding?.apply { + resultPlayMovie.isVisible = data is Resource.Success + (data as? Resource.Success)?.value?.let { (text, ep) -> + resultPlayMovie.setText(text) + resultPlayMovie.setOnClickListener { + viewModel.handleAction( + activity, + EpisodeClickEvent(ACTION_CLICK_DEFAULT, ep) + ) + } + resultPlayMovie.setOnLongClickListener { + viewModel.handleAction( + activity, + EpisodeClickEvent(ACTION_SHOW_OPTIONS, ep) + ) + return@setOnLongClickListener true + } + if (hasNoFocus()) { + resultPlayMovie.requestFocus() + } + } + } + } observeNullable(viewModel.selectPopup) { popup -> if (popup == null) { @@ -257,21 +312,111 @@ class ResultFragmentTv : ResultFragment() { observe(viewModel.seasonSelections) { binding?.resultSeasonSelection.update(it) } - - binding?.apply { - resultBack.setOnClickListener { - activity?.popCurrentPage() + observe(viewModel.recommendations) { recommendations -> + setRecommendations(recommendations, null) + } + observe(viewModel.episodeSynopsis) { description -> + view.context?.let { ctx -> + val builder: AlertDialog.Builder = + AlertDialog.Builder(ctx, R.style.AlertDialogCustom) + builder.setMessage(description.html()) + .setTitle(R.string.synopsis) + .setOnDismissListener { + viewModel.releaseEpisodeSynopsis() + } + .show() } + } + observeNullable(viewModel.episodes) { episodes -> + binding?.apply { + resultEpisodes.isVisible = episodes is Resource.Success + resultEpisodeLoading.isVisible = episodes is Resource.Loading + if (episodes is Resource.Success) { + /* + * Okay so what is this fuckery? + * Basically Android TV will crash if you request a new focus while + * the adapter gets updated. + * + * This means that if you load thumbnails and request a next focus at the same time + * the app will crash without any way to catch it! + * + * How to bypass this? + * This code basically steals the focus for 500ms and puts it in an inescapable view + * then lets out the focus by requesting focus to result_episodes + */ - resultRecommendationsList.spanCount = 8 - resultRecommendationsList.adapter = - SearchAdapter( - ArrayList(), - resultRecommendationsList, - ) { callback -> - SearchHelper.handleSearchClickCallback(callback) + val hasEpisodes = + !(resultEpisodes.adapter as? EpisodeAdapter?)?.cardList.isNullOrEmpty() + + if (hasEpisodes) { + // Make it impossible to focus anywhere else! + temporaryNoFocus.isFocusable = true + temporaryNoFocus.requestFocus() + } + + (resultEpisodes.adapter as? EpisodeAdapter)?.updateList(episodes.value) + + if (hasEpisodes) main { + delay(500) + temporaryNoFocus.isFocusable = false + // This might make some people sad as it changes the focus when leaving an episode :( + temporaryNoFocus.requestFocus() + } + + if (hasNoFocus()) + binding?.resultEpisodes?.requestFocus() } + } } + observeNullable(viewModel.page) { data -> + if (data == null) return@observeNullable + binding?.apply { + when (data) { + is Resource.Success -> { + val d = data.value + resultVpn.setText(d.vpnText) + resultInfo.setText(d.metaText) + resultNoEpisodes.setText(d.noEpisodesFoundText) + resultTitle.setText(d.titleText) + resultMetaSite.setText(d.apiName) + resultMetaType.setText(d.typeText) + resultMetaYear.setText(d.yearText) + resultMetaDuration.setText(d.durationText) + resultMetaRating.setText(d.ratingText) + resultCastText.setText(d.actorsText) + resultNextAiring.setText(d.nextAiringEpisode) + resultNextAiringTime.setText(d.nextAiringDate) + resultPoster.setImage(d.posterImage) + resultDescription.setTextHtml(d.plotText) + + resultComingSoon.isVisible = d.comingSoon + resultDataHolder.isGone = d.comingSoon + UIHelper.populateChips(resultTag, d.tags) + resultCastItems.isGone = d.actors.isNullOrEmpty() + (resultCastItems.adapter as? ActorAdaptor)?.updateList( + d.actors ?: emptyList() + ) + } + + is Resource.Loading -> { + + } + + is Resource.Failure -> { + resultErrorText.text = + (this@ResultFragmentTv.context?.let { getStoredData(it) }?.url?.plus("\n") + ?: "") + data.errorString + } + } + + resultFinishLoading.isVisible = data is Resource.Success + + resultLoading.isVisible = data is Resource.Loading + + resultLoadingError.isVisible = data is Resource.Failure + resultReloadConnectionOpenInBrowser.isVisible = data is Resource.Failure + } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt index d1667229..1f663e31 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt @@ -10,21 +10,13 @@ import android.view.ViewGroup import android.widget.FrameLayout import androidx.core.view.isGone import androidx.core.view.isVisible -import com.discord.panels.PanelsChildGestureRegionObserver import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.ui.player.CSPlayerEvent -import com.lagradost.cloudstream3.ui.player.FullScreenPlayer import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.utils.IOnBackPressed -import kotlinx.android.synthetic.main.fragment_result.* -import kotlinx.android.synthetic.main.fragment_result.result_smallscreen_holder -import kotlinx.android.synthetic.main.fragment_result_swipe.* -import kotlinx.android.synthetic.main.fragment_result_tv.* -import kotlinx.android.synthetic.main.fragment_trailer.* -open class ResultTrailerPlayer : FullScreenPlayer(), - PanelsChildGestureRegionObserver.GestureRegionsListener, IOnBackPressed { +open class ResultTrailerPlayer : ResultFragmentPhone(), IOnBackPressed { override var lockRotation = false override var isFullScreenPlayer = false @@ -60,13 +52,13 @@ open class ResultTrailerPlayer : FullScreenPlayer(), screenHeight } - result_trailer_loading?.isVisible = false - result_smallscreen_holder?.isVisible = !isFullScreenPlayer - result_fullscreen_holder?.isVisible = isFullScreenPlayer + //result_trailer_loading?.isVisible = false + resultBinding?.resultSmallscreenHolder?.isVisible = !isFullScreenPlayer + binding?.resultFullscreenHolder?.isVisible = isFullScreenPlayer val to = sw * h / w - player_background?.apply { + resultBinding?.fragmentTrailer?.playerBackground?.apply { isVisible = true layoutParams = FrameLayout.LayoutParams( @@ -79,12 +71,13 @@ open class ResultTrailerPlayer : FullScreenPlayer(), layoutParams = FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, - result_top_holder?.measuredHeight ?: FrameLayout.LayoutParams.MATCH_PARENT + resultBinding?.resultTopHolder?.measuredHeight + ?: FrameLayout.LayoutParams.MATCH_PARENT ) } if (playerBinding?.playerIntroPlay?.isGone == true) { - result_top_holder?.apply { + resultBinding?.resultTopHolder?.apply { val anim = ValueAnimator.ofInt( measuredHeight, @@ -135,20 +128,26 @@ open class ResultTrailerPlayer : FullScreenPlayer(), playerBinding?.playerFullscreen?.setImageResource(if (fullscreen) R.drawable.baseline_fullscreen_exit_24 else R.drawable.baseline_fullscreen_24) if (fullscreen) { enterFullscreen() - result_top_bar?.isVisible = false - result_fullscreen_holder?.isVisible = true - result_main_holder?.isVisible = false - player_background?.let { view -> - (view.parent as ViewGroup?)?.removeView(view) - result_fullscreen_holder?.addView(view) + binding?.apply { + resultTopBar.isVisible = false + resultFullscreenHolder.isVisible = true + resultMainHolder.isVisible = false } - } else { - result_top_bar?.isVisible = true - result_fullscreen_holder?.isVisible = false - result_main_holder?.isVisible = true - player_background?.let { view -> + + resultBinding?.fragmentTrailer?.playerBackground?.let { view -> (view.parent as ViewGroup?)?.removeView(view) - result_smallscreen_holder?.addView(view) + binding?.resultFullscreenHolder?.addView(view) + } + + } else { + binding?.apply { + resultTopBar.isVisible = true + resultFullscreenHolder.isVisible = false + resultMainHolder.isVisible = true + resultBinding?.fragmentTrailer?.playerBackground?.let { view -> + (view.parent as ViewGroup?)?.removeView(view) + resultBinding?.resultSmallscreenHolder?.addView(view) + } } exitFullscreen() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt index e5f2f2dc..817e9235 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt @@ -13,7 +13,7 @@ import java.util.concurrent.TimeUnit object SyncUtil { private val regexs = listOf( - Regex("""(9anime)\.(?:to|center|id)/watch/(?:.*?)\.([^/?]*)"""), + Regex("""(9anime)\.(?:to|center|id)/watch/.*?\.([^/?]*)"""), Regex("""(gogoanime|gogoanimes)\..*?/category/([^/?]*)"""), Regex("""(twist\.moe)/a/([^/?]*)"""), ) @@ -44,6 +44,13 @@ object SyncUtil { matchList[site]?.let { realSite -> getIdsFromSlug(slug, realSite)?.let { return it + } ?: kotlin.run { + if (slug.endsWith("-dub")) { + println("testing non -dub slug $slug") + getIdsFromSlug(slug.removeSuffix("-dub"), realSite)?.let { + return it + } + } } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt index a1aca907..bd81b960 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt @@ -46,12 +46,16 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.RequestOptions.bitmapTransform import com.bumptech.glide.request.target.Target +import com.google.android.material.chip.Chip +import com.google.android.material.chip.ChipDrawable +import com.google.android.material.chip.ChipGroup import com.lagradost.cloudstream3.CommonActivity.activity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.result.UiImage import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import jp.wasabeef.glide.transformations.BlurTransformation import kotlin.math.roundToInt @@ -73,6 +77,30 @@ object UIHelper { || Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) } + fun populateChips(view: ChipGroup?, tags : List) { + if(view == null) return + view.removeAllViews() + val context = view.context ?: return + + tags.forEach { tag -> + val chip = Chip(context) + val chipDrawable = ChipDrawable.createFromAttributes( + context, + null, + 0, + R.style.ChipFilled + ) + chip.setChipDrawable(chipDrawable) + chip.text = tag + chip.isChecked = false + chip.isCheckable = false + chip.isFocusable = false + chip.isClickable = false + chip.setTextColor(context.colorFromAttribute(R.attr.textColor)) + view.addView(chip) + } + } + fun Activity.requestRW() { ActivityCompat.requestPermissions( this, diff --git a/app/src/main/res/layout/fragment_result_tv.xml b/app/src/main/res/layout/fragment_result_tv.xml index f74dc339..0fc3d2e9 100644 --- a/app/src/main/res/layout/fragment_result_tv.xml +++ b/app/src/main/res/layout/fragment_result_tv.xml @@ -465,6 +465,7 @@ - - - - + + + + android:layout_height="match_parent" + android:visibility="gone"> + android:layout_marginTop="2dp" + android:visibility="gone"> @@ -206,33 +211,7 @@ app:layout_constraintTop_toTopOf="parent" tools:visibility="visible" /> - - - - + + + +