From 1bc4c7e56d8ff87b796bda6a9dd259c8ea21df26 Mon Sep 17 00:00:00 2001 From: reduplicated <110570621+reduplicated@users.noreply.github.com> Date: Fri, 5 Aug 2022 00:26:33 +0200 Subject: [PATCH] android tv resultview testing --- .../lagradost/cloudstream3/MainActivity.kt | 6 +- .../cloudstream3/ui/result/ResultFragment.kt | 420 ++--------- .../ui/result/ResultFragmentPhone.kt | 380 ++++++++++ .../ui/result/ResultFragmentTv.kt | 11 + .../cloudstream3/ui/result/UiText.kt | 17 +- .../lagradost/cloudstream3/utils/AppUtils.kt | 15 +- app/src/main/res/layout/fragment_result.xml | 1 + .../main/res/layout/fragment_result_tv.xml | 699 ++++++++++++++++++ app/src/main/res/layout/result_selection.xml | 8 + .../main/res/navigation/mobile_navigation.xml | 104 ++- app/src/main/res/values/styles.xml | 21 +- 11 files changed, 1287 insertions(+), 395 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt create mode 100644 app/src/main/res/layout/fragment_result_tv.xml create mode 100644 app/src/main/res/layout/result_selection.xml diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 8719936e..12a39d18 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -156,7 +156,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, 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 5ce63a8a..111bc010 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,12 +1,9 @@ package com.lagradost.cloudstream3.ui.result import android.annotation.SuppressLint -import android.app.Dialog 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 @@ -26,17 +23,18 @@ 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.bottomsheet.BottomSheetDialog import com.google.android.material.button.MaterialButton -import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings import com.lagradost.cloudstream3.APIHolder.getApiFromName import com.lagradost.cloudstream3.APIHolder.updateHasTrailers 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 @@ -45,8 +43,6 @@ import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownload import com.lagradost.cloudstream3.ui.download.EasyDownloadButton 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.Companion.isTrueTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.utils.* @@ -59,21 +55,56 @@ import com.lagradost.cloudstream3.utils.Coroutines.ioWork import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog -import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar -import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.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.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_scroll +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.runBlocking const val START_ACTION_RESUME_LATEST = 1 @@ -159,7 +190,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" @@ -168,6 +199,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 { @@ -211,8 +243,11 @@ class ResultFragment : ResultTrailerPlayer() { private var updateUIListener: (() -> Unit)? = null } - private lateinit var viewModel: ResultViewModel2 //by activityViewModels() - private lateinit var syncModel: SyncViewModel + 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, @@ -224,7 +259,7 @@ class ResultFragment : ResultTrailerPlayer() { 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 @@ -232,12 +267,7 @@ class ResultFragment : ResultTrailerPlayer() { 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() } @@ -289,121 +319,8 @@ class ResultFragment : ResultTrailerPlayer() { } } - var currentTrailers: List = emptyList() - var currentTrailerIndex = 0 + open fun setRecommendations(rec: List?, validApiName: String?) { - 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 - } - - private fun setTrailers(trailers: List?) { - context?.updateHasTrailers() - if (!LoadResponse.isTrailersEnabled) return - currentTrailers = trailers?.sortedBy { -it.quality } ?: emptyList() - loadTrailer() - } - - 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 updateUI() { @@ -411,27 +328,11 @@ class ResultFragment : ResultTrailerPlayer() { viewModel.reloadEpisodes() } - var loadingDialog: Dialog? = null - var popupDialog: Dialog? = null - @SuppressLint("SetTextI18n") 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 @@ -515,10 +416,6 @@ class ResultFragment : ResultTrailerPlayer() { //result_poster_blur_holder?.translationY = -scrollY.toFloat() }) - result_back.setOnClickListener { - activity?.popCurrentPage() - } - result_episodes.adapter = EpisodeAdapter( ArrayList(), @@ -531,15 +428,6 @@ class ResultFragment : ResultTrailerPlayer() { } ) - 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) @@ -567,23 +455,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 - - context?.let { ctx -> val arrayAdapter = ArrayAdapter(ctx, R.layout.sort_bottom_single_choice) /* @@ -633,19 +509,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 } @@ -769,8 +632,9 @@ class ResultFragment : ResultTrailerPlayer() { } result_resume_series_button?.isVisible = !value.isMovie + result_resume_series_button_play?.isVisible = !value.isMovie - result_resume_series_button?.setOnClickListener { + val click = View.OnClickListener { viewModel.handleAction( activity, EpisodeClickEvent( @@ -778,6 +642,9 @@ class ResultFragment : ResultTrailerPlayer() { ) ) } + + result_resume_series_button?.setOnClickListener(click) + result_resume_series_button_play?.setOnClickListener(click) } is Some.None -> { result_resume_parent?.isVisible = false @@ -803,156 +670,6 @@ class ResultFragment : ResultTrailerPlayer() { } } - observe(viewModel.selectedSeason) { text -> - result_season_button.setText(text) - - // If the season button is visible the result season button will be next focus down - if (result_season_button?.isVisible == true) - if (result_resume_parent?.isVisible == true) - setFocusUpAndDown(result_resume_series_button, result_season_button) - else - setFocusUpAndDown(result_bookmark_button, result_season_button) - } - - observe(viewModel.selectedDubStatus) { status -> - result_dub_select?.setText(status) - - if (result_dub_select?.isVisible == true) - if (result_season_button?.isVisible != true && result_episode_select?.isVisible != true) { - if (result_resume_parent?.isVisible == true) - setFocusUpAndDown(result_resume_series_button, result_dub_select) - else - setFocusUpAndDown(result_bookmark_button, result_dub_select) - } - } - - observe(viewModel.selectPopup) { popup -> - when (popup) { - is Some.Success -> { - popupDialog?.dismissSafe(activity) - - popupDialog = activity?.let { act -> - val pop = popup.value - val options = pop.getOptions(act) - val title = pop.getTitle(act) - - act.showBottomDialogInstant( - options, title, { - popupDialog = null - pop.callback(null) - }, { - popupDialog = null - pop.callback(it) - } - ) - } - } - is Some.None -> { - popupDialog?.dismissSafe(activity) - popupDialog = null - } - } - - //showBottomDialogInstant - } - - observe(viewModel.loadedLinks) { load -> - when (load) { - is Some.Success -> { - if (loadingDialog?.isShowing != true) { - loadingDialog?.dismissSafe(activity) - loadingDialog = null - } - loadingDialog = loadingDialog ?: context?.let { ctx -> - val builder = - BottomSheetDialog(ctx) - builder.setContentView(R.layout.bottom_loading) - builder.setOnDismissListener { - loadingDialog = null - viewModel.cancelLinks() - } - //builder.setOnCancelListener { - // it?.dismiss() - //} - builder.setCanceledOnTouchOutside(true) - - builder.show() - - builder - } - } - is Some.None -> { - loadingDialog?.dismissSafe(activity) - loadingDialog = null - } - } - } - - observe(viewModel.selectedRange) { range -> - result_episode_select.setText(range) - - // If Season button is invisible then the bookmark button next focus is episode select - if (result_episode_select?.isVisible == true) - if (result_season_button?.isVisible != true) { - if (result_resume_parent?.isVisible == true) - setFocusUpAndDown(result_resume_series_button, result_episode_select) - else - setFocusUpAndDown(result_bookmark_button, result_episode_select) - } - } - -// val preferDub = context?.getApiDubstatusSettings()?.all { it == DubStatus.Dubbed } == true - - observe(viewModel.dubSubSelections) { range -> - 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) - } - } - } - } - result_cast_items?.setOnFocusChangeListener { _, hasFocus -> // Always escape focus if (hasFocus) result_bookmark_button?.requestFocus() @@ -1178,14 +895,6 @@ 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 @@ -1239,16 +948,5 @@ class ResultFragment : ResultTrailerPlayer() { } } } - - 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..9b179152 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt @@ -0,0 +1,380 @@ +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 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.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.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_recommendations?.spanCount = 3 + result_overlapping_panels?.setStartPanelLockState(OverlappingPanelsLayout.LockState.CLOSE) + result_overlapping_panels?.setEndPanelLockState(OverlappingPanelsLayout.LockState.CLOSE) + + 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() + } + } + }) + + + observe(viewModel.selectPopup) { popup -> + when (popup) { + is Some.Success -> { + popupDialog?.dismissSafe(activity) + + popupDialog = activity?.let { act -> + val pop = popup.value + val options = pop.getOptions(act) + val title = pop.getTitle(act) + + act.showBottomDialogInstant( + options, title, { + popupDialog = null + pop.callback(null) + }, { + popupDialog = null + pop.callback(it) + } + ) + } + } + is Some.None -> { + popupDialog?.dismissSafe(activity) + popupDialog = null + } + } + + //showBottomDialogInstant + } + + observe(viewModel.loadedLinks) { load -> + when (load) { + is Some.Success -> { + if (loadingDialog?.isShowing != true) { + loadingDialog?.dismissSafe(activity) + loadingDialog = null + } + loadingDialog = loadingDialog ?: context?.let { ctx -> + val builder = + BottomSheetDialog(ctx) + builder.setContentView(R.layout.bottom_loading) + builder.setOnDismissListener { + loadingDialog = null + viewModel.cancelLinks() + } + //builder.setOnCancelListener { + // it?.dismiss() + //} + builder.setCanceledOnTouchOutside(true) + + builder.show() + + builder + } + } + is Some.None -> { + loadingDialog?.dismissSafe(activity) + loadingDialog = null + } + } + } + observe(viewModel.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..78664761 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt @@ -0,0 +1,11 @@ +package com.lagradost.cloudstream3.ui.result + +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.SearchResponse + +class ResultFragmentTv : ResultFragment() { + override val resultLayout = R.layout.fragment_result_tv + override fun setRecommendations(rec: List?, validApiName: String?) { + + } +} \ No newline at end of file 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 index 6bd7ee62..0ca232e1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt @@ -21,11 +21,13 @@ sealed class 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)" }}" + override fun toString(): String = + "resId = $resId\nargs = ${args.toList().map { "(${it::class} = $it)" }}" } fun asStringNull(context: Context?): String? { @@ -137,7 +139,14 @@ fun TextView?.setText(text: UiText?) { if (text == null) { this.isVisible = false } else { - val str = text.asStringNull(context) + val str = text.asStringNull(context)?.let { + if (this.maxLines == 1) { + it.replace("\n", " ") + } else { + it + } + } + this.isGone = str.isNullOrBlank() this.text = str } @@ -155,9 +164,9 @@ fun TextView?.setTextHtml(text: UiText?) { } fun TextView?.setTextHtml(text: Some?) { - setTextHtml(if(text is Some.Success) text.value else null) + 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) + 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/utils/AppUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt index 79061e24..2e773310 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,8 @@ 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.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 @@ -313,6 +315,15 @@ object AppUtils { //private val viewModel: ResultViewModel by activityViewModels() + private fun getResultsId(context: Context) : Int { + return R.id.global_to_navigation_results_phone + //return if(context.isTvSettings()) { + // 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/res/layout/fragment_result.xml b/app/src/main/res/layout/fragment_result.xml index eed860b0..35e78c7f 100644 --- a/app/src/main/res/layout/fragment_result.xml +++ b/app/src/main/res/layout/fragment_result.xml @@ -465,6 +465,7 @@ android:textColor="?attr/grayTextColor" android:textSize="15sp" tools:text="@string/provider_info_meta" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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..e379201b --- /dev/null +++ b/app/src/main/res/layout/result_selection.xml @@ -0,0 +1,8 @@ + + \ No newline at end of file diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index b3e64c1b..57f867f3 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"> - + + + + + + + - - - + + + + + + + + + + ?attr/colorPrimary + - - + +