android tv resultview testing

This commit is contained in:
reduplicated 2022-08-05 00:26:33 +02:00
parent cd9bdb8ba7
commit 1bc4c7e56d
11 changed files with 1287 additions and 395 deletions

View file

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

View file

@ -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<ExtractorLink>?) { }
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<ExtractorLink> = emptyList()
var currentTrailerIndex = 0
open fun setRecommendations(rec: List<SearchResponse>?, 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<ExtractorLink>?) {
context?.updateHasTrailers()
if (!LoadResponse.isTrailersEnabled) return
currentTrailers = trailers?.sortedBy { -it.quality } ?: emptyList()
loadTrailer()
}
private fun setRecommendations(rec: List<SearchResponse>?, 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<String>(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<Rect>) {
result_overlapping_panels?.setChildGestureRegions(gestureRegions)
}
}

View file

@ -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<ExtractorLink> = 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<ExtractorLink>?) {
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<Rect>) {
result_overlapping_panels?.setChildGestureRegions(gestureRegions)
}
override fun setRecommendations(rec: List<SearchResponse>?, 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 })
}
}
}
}

View file

@ -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<SearchResponse>?, validApiName: String?) {
}
}

View file

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

View file

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

View file

@ -465,6 +465,7 @@
android:textColor="?attr/grayTextColor"
android:textSize="15sp"
tools:text="@string/provider_info_meta" />
<TextView
android:id="@+id/result_no_episodes"
android:layout_width="match_parent"

View file

@ -0,0 +1,699 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/result_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
style="@style/DarkFragment"
android:background="?attr/primaryBlackBackground"
android:clickable="true"
android:focusable="true">
<com.facebook.shimmer.ShimmerFrameLayout
android:id="@+id/result_loading"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:orientation="vertical"
app:shimmer_auto_start="true"
app:shimmer_base_alpha="0.2"
app:shimmer_duration="@integer/loading_time"
app:shimmer_highlight_alpha="0.3"
tools:visibility="gone">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/result_padding"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="@dimen/loading_margin"
android:orientation="horizontal">
<include layout="@layout/loading_poster" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="@dimen/loading_margin"
android:layout_marginEnd="@dimen/loading_margin"
android:orientation="vertical">
<include layout="@layout/loading_line" />
<include layout="@layout/loading_line" />
<include layout="@layout/loading_line" />
<include layout="@layout/loading_line" />
<include layout="@layout/loading_line_short" />
</LinearLayout>
</LinearLayout>
<ImageView
android:layout_width="match_parent"
android:layout_height="20dp"
tools:ignore="ContentDescription" />
<include layout="@layout/loading_episode" />
<include layout="@layout/loading_episode" />
<include layout="@layout/loading_episode" />
</LinearLayout>
</com.facebook.shimmer.ShimmerFrameLayout>
<LinearLayout
android:id="@+id/result_loading_error"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="gone">
<com.google.android.material.button.MaterialButton
android:id="@+id/result_reload_connectionerror"
style="@style/WhiteButton"
android:layout_width="wrap_content"
android:layout_gravity="center"
android:layout_margin="5dp"
android:minWidth="200dp"
android:text="@string/reload_error"
app:icon="@drawable/ic_baseline_autorenew_24" />
<com.google.android.material.button.MaterialButton
android:id="@+id/result_reload_connection_open_in_browser"
style="@style/BlackButton"
android:layout_width="wrap_content"
android:layout_gravity="center"
android:layout_margin="5dp"
android:minWidth="200dp"
android:text="@string/result_open_in_browser"
app:icon="@drawable/ic_baseline_public_24" />
<TextView
android:id="@+id/result_error_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="5dp"
android:gravity="center"
android:textColor="?attr/textColor" />
</LinearLayout>
<LinearLayout
android:id="@+id/result_finish_loading"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
tools:visibility="visible"
android:orientation="vertical">
<androidx.core.widget.NestedScrollView
android:id="@+id/result_scroll"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/primaryGrayBackground">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/primaryBlackBackground"
android:orientation="vertical">
<com.facebook.shimmer.ShimmerFrameLayout
tools:visibility="gone"
android:visibility="gone"
android:id="@+id/result_trailer_loading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical"
app:shimmer_auto_start="true"
app:shimmer_base_alpha="0.2"
app:shimmer_duration="@integer/loading_time"
app:shimmer_highlight_alpha="0.3">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/result_padding"
android:orientation="vertical">
<androidx.cardview.widget.CardView
android:background="@color/grayShimmer"
app:cardCornerRadius="@dimen/loading_radius"
android:layout_width="match_parent"
android:layout_height="150dp"
android:foreground="@drawable/outline_drawable" />
</LinearLayout>
</com.facebook.shimmer.ShimmerFrameLayout>
<FrameLayout
android:descendantFocusability="blocksDescendants"
android:id="@+id/result_smallscreen_holder"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<include layout="@layout/fragment_trailer" />
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingStart="@dimen/result_padding"
android:paddingEnd="@dimen/result_padding">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="15dp"
android:orientation="horizontal"
android:visibility="visible">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/result_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:maxLines="2"
android:textColor="?attr/textColor"
android:textSize="20sp"
android:textStyle="bold"
tools:text="The Perfect Run The Perfect Run" />
<com.lagradost.cloudstream3.widget.FlowLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:itemSpacing="10dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/result_meta_site"
style="@style/SmallBlackButton"
android:layout_gravity="center_vertical"
tools:text="Gogoanime" />
<TextView
android:id="@+id/result_meta_type"
style="@style/ResultInfoText"
tools:text="Movie" />
<TextView
android:id="@+id/result_meta_year"
style="@style/ResultInfoText"
tools:text="2022" />
<TextView
android:id="@+id/result_meta_rating"
style="@style/ResultInfoText"
tools:text="Rated: 8.5/10.0" />
<TextView
android:id="@+id/result_meta_status"
style="@style/ResultInfoText"
tools:text="Ongoing" />
<TextView
android:id="@+id/result_meta_duration"
style="@style/ResultInfoText"
tools:text="121min" />
</com.lagradost.cloudstream3.widget.FlowLayout>
<!--
This has half margin and half padding to make TV focus on description look better.
The focus outline now settles between the poster and text.
-->
<TextView
android:padding="5dp"
android:maxLength="1000"
android:ellipsize="end"
android:id="@+id/result_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="@drawable/outline_drawable"
android:nextFocusUp="@id/result_back"
android:nextFocusDown="@id/result_bookmark_button"
android:textColor="?attr/textColor"
android:textSize="15sp"
tools:text="Ryan Quicksave Romano is an eccentric adventurer with a strange power: he can create a save-point in time and redo his life whenever he dies. Arriving in New Rome, the glitzy capital of sin of a rebuilding Europe, he finds the city torn between mega-corporations, sponsored heroes, superpowered criminals, and true monsters. It's a time of chaos, where potions can grant the power to rule the world and dangers lurk everywhere. " />
</LinearLayout>
</LinearLayout>
<TextView
android:id="@+id/result_cast_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:ellipsize="end"
android:maxLines="2"
android:textColor="?attr/grayTextColor"
android:textSize="15sp"
tools:text="Cast: Joe Ligma" />
<androidx.recyclerview.widget.RecyclerView
tools:visibility="gone"
android:nextFocusUp="@id/result_bookmark_button"
android:nextFocusDown="@id/result_play_movie"
android:id="@+id/result_cast_items"
android:layout_width="match_parent"
android:descendantFocusability="afterDescendants"
android:layout_height="wrap_content"
android:fadingEdge="horizontal"
android:focusableInTouchMode="false"
android:focusable="false"
android:orientation="horizontal"
android:paddingTop="5dp"
android:requiresFadingEdge="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="2"
tools:listitem="@layout/cast_item" />
<TextView
android:id="@+id/result_vpn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?attr/grayTextColor"
android:textSize="15sp"
tools:text="@string/vpn_torrent" />
<TextView
android:id="@+id/result_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:textColor="?attr/grayTextColor"
android:textSize="15sp"
tools:text="@string/provider_info_meta" />
<TextView
android:id="@+id/result_no_episodes"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:textColor="?attr/grayTextColor"
android:textSize="15sp"
tools:text="@string/no_episodes_found" />
<TextView
android:id="@+id/result_tag_holder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:text="@string/result_tags"
android:textColor="?attr/textColor"
android:textSize="17sp"
android:textStyle="normal"
android:visibility="gone" />
<com.lagradost.cloudstream3.widget.FlowLayout
android:id="@+id/result_tag"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/result_coming_soon"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:paddingTop="50dp"
android:text="@string/coming_soon"
android:textColor="?attr/textColor"
android:textSize="20sp"
android:textStyle="bold"
android:visibility="gone" />
<LinearLayout
android:id="@+id/result_data_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.button.MaterialButton
android:id="@+id/result_add_sync"
style="@style/WhiteButton"
android:layout_width="match_parent"
android:layout_gravity="center_vertical"
android:layout_marginStart="0dp"
android:layout_marginBottom="10dp"
android:text="@string/add_sync"
android:visibility="gone"
app:icon="@drawable/ic_baseline_add_24" />
<LinearLayout
android:id="@+id/result_movie_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:orientation="horizontal"
tools:visibility="visible">
<com.google.android.material.button.MaterialButton
android:id="@+id/result_play_movie"
style="@style/WhiteButton"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_marginStart="0dp"
android:layout_marginEnd="5dp"
android:layout_marginBottom="10dp"
android:nextFocusUp="@id/result_bookmark_button"
android:nextFocusDown="@id/result_download_movie"
android:text="@string/play_movie_button"
android:visibility="visible"
app:icon="@drawable/ic_baseline_play_arrow_24">
<requestFocus />
</com.google.android.material.button.MaterialButton>
<FrameLayout
android:layout_marginStart="5dp"
android:id="@+id/result_movie_progress_downloaded_holder"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content">
<com.google.android.material.button.MaterialButton
android:id="@+id/result_download_movie"
style="@style/BlackButton"
android:layout_width="match_parent"
android:layout_gravity="center_vertical"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:clickable="true"
android:focusable="true"
android:nextFocusUp="@id/result_play_movie"
android:nextFocusDown="@id/result_season_button"
android:visibility="visible" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:orientation="horizontal">
<androidx.core.widget.ContentLoadingProgressBar
android:id="@+id/result_movie_progress_downloaded"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_gravity="end|center_vertical"
android:layout_margin="5dp"
android:background="@drawable/circle_shape"
android:indeterminate="false"
android:max="100"
android:paddingStart="5dp"
android:paddingEnd="5dp"
android:progress="30"
android:progressDrawable="@drawable/circular_progress_bar_filled"
android:visibility="visible" />
<ImageView
android:id="@+id/result_movie_download_icon"
android:layout_width="30dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/download"
android:src="@drawable/ic_baseline_play_arrow_24"
android:visibility="visible"
app:tint="?attr/white" />
<TextView
android:id="@+id/result_movie_download_text"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:letterSpacing="0.09"
android:textAllCaps="false"
android:textColor="?attr/textColor"
android:textSize="15sp"
android:textStyle="bold"
tools:text="Downloading" />
<TextView
android:id="@+id/result_movie_download_text_precentage"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:letterSpacing="0.09"
android:paddingStart="5dp"
android:paddingEnd="5dp"
android:textAllCaps="false"
android:textColor="?attr/textColor"
android:textSize="15sp"
android:textStyle="bold"
android:visibility="gone"
tools:text="68%" />
</LinearLayout>
</FrameLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/result_resume_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible">
<com.google.android.material.button.MaterialButton
android:id="@+id/result_next_series_button"
style="@style/WhiteButton"
android:layout_width="match_parent"
android:layout_gravity="center_vertical"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:layout_marginBottom="10dp"
android:nextFocusUp="@id/result_bookmark_button"
android:nextFocusDown="@id/result_download_movie"
android:text="@string/next_episode"
android:visibility="gone"
app:icon="@drawable/cast_ic_mini_controller_skip_next" />
<LinearLayout
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:layout_marginEnd="10dp"
android:id="@+id/result_resume_series_button_play"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_gravity="center"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/download"
android:src="@drawable/ic_baseline_play_arrow_24"
android:visibility="visible"
app:tint="?attr/white" />
<TextView
android:layout_gravity="center"
android:gravity="center"
android:id="@+id/result_resume_series_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="?attr/textColor"
android:textSize="17sp"
android:textStyle="bold"
tools:text="S1E1 Episode 1" />
<TextView
android:maxLines="1"
android:id="@+id/result_resume_series_progress_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="0"
android:gravity="center"
android:paddingStart="10dp"
android:textColor="?attr/grayTextColor"
tools:ignore="RtlSymmetry"
tools:text="69m remaining" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/result_resume_progress_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="10dp"
android:visibility="gone"
tools:visibility="visible">
<androidx.core.widget.ContentLoadingProgressBar
android:id="@+id/result_resume_series_progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_gravity="end|center_vertical"
android:layout_weight="1"
android:indeterminate="false"
android:max="100"
android:progress="0"
android:progressBackgroundTint="?attr/colorPrimary"
android:visibility="visible"
tools:progress="50"
tools:visibility="visible" />
</LinearLayout>
<LinearLayout
android:id="@+id/result_episodes_tab"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:paddingBottom="10dp"
android:id="@+id/result_season_selection"
tools:listitem="@layout/result_selection"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
</androidx.recyclerview.widget.RecyclerView>
<androidx.recyclerview.widget.RecyclerView
android:paddingBottom="10dp"
android:id="@+id/result_range_selection"
tools:listitem="@layout/result_selection"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
</androidx.recyclerview.widget.RecyclerView>
<androidx.recyclerview.widget.RecyclerView
android:paddingBottom="10dp"
android:id="@+id/result_dub_selection"
tools:listitem="@layout/result_selection"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
</androidx.recyclerview.widget.RecyclerView>
<LinearLayout
android:id="@+id/result_next_airing_holder"
android:layout_gravity="start"
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:gravity="center"
android:id="@+id/result_next_airing"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?attr/grayTextColor"
android:textSize="17sp"
android:textStyle="normal"
tools:text="Episode 1022 will be released in" />
<TextView
android:paddingEnd="5dp"
android:paddingStart="5dp"
android:gravity="center"
android:id="@+id/result_next_airing_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?attr/textColor"
android:textSize="17sp"
android:textStyle="normal"
tools:text="5d 3h 30m" />
</LinearLayout>
<com.facebook.shimmer.ShimmerFrameLayout
android:id="@+id/result_episode_loading"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_marginTop="15dp"
android:orientation="vertical"
app:shimmer_auto_start="true"
app:shimmer_base_alpha="0.2"
app:shimmer_duration="@integer/loading_time"
app:shimmer_highlight_alpha="0.3"
tools:visibility="visible">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<include layout="@layout/loading_episode" />
<include layout="@layout/loading_episode" />
<include layout="@layout/loading_episode" />
<include layout="@layout/loading_episode" />
</LinearLayout>
</com.facebook.shimmer.ShimmerFrameLayout>
<!--<androidx.core.widget.ContentLoadingProgressBar
android:id="@+id/result_episode_loading"
style="@style/Widget.AppCompat.ProgressBar"
android:layout_gravity="center"
android:layout_width="50dp"
android:layout_height="50dp" />-->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/result_episodes"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="0dp"
android:clipToPadding="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:descendantFocusability="afterDescendants"
android:paddingBottom="100dp"
tools:listitem="@layout/result_episode" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>
</FrameLayout>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.button.MaterialButton android:id="@+id/result_season_button"
style="@style/SelectableButton"
android:layout_marginStart="0dp"
android:layout_marginEnd="10dp"
tools:text="Season 1"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" />

View file

@ -4,10 +4,35 @@
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/mobile_navigation"
app:startDestination="@+id/navigation_home">
<action
android:id="@+id/global_to_navigation_results"
app:destination="@id/navigation_results"
android:id="@+id/global_to_navigation_results_tv"
app:destination="@id/navigation_results_tv"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim">
<argument
android:name="url"
app:argType="string" />
<argument
android:name="apiName"
app:argType="string" />
<argument
android:name="startAction"
android:defaultValue="0"
app:argType="integer" />
<argument
android:name="startValue"
android:defaultValue="0"
app:argType="integer" />
<argument
android:name="restart"
android:defaultValue="false"
app:argType="boolean" />
</action>
<action
android:id="@+id/global_to_navigation_results_phone"
app:destination="@id/navigation_results_phone"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
@ -181,13 +206,6 @@
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim"
tools:layout="@layout/fragment_home">
<action
android:id="@+id/action_navigation_home_to_navigation_results"
app:destination="@id/navigation_results"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
<action
android:id="@+id/action_navigation_home_to_navigation_quick_search"
app:destination="@id/navigation_quick_search"
@ -206,13 +224,6 @@
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim"
tools:layout="@layout/fragment_search">
<action
android:id="@+id/action_navigation_search_to_navigation_results"
app:destination="@id/navigation_results"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
</fragment>
<fragment
@ -239,13 +250,6 @@
android:name="folder"
app:argType="string" />
</action>
<action
android:id="@+id/action_navigation_downloads_to_navigation_results"
app:destination="@id/navigation_results"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
<action
android:id="@+id/action_navigation_downloads_to_navigation_player"
app:destination="@id/navigation_player"
@ -360,6 +364,56 @@
</fragment>
<fragment
android:id="@+id/navigation_results_phone"
android:name="com.lagradost.cloudstream3.ui.result.ResultFragmentPhone"
android:layout_height="match_parent"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim"
tools:layout="@layout/fragment_result_swipe">
<action
android:id="@+id/action_navigation_results_phone_to_navigation_quick_search"
app:destination="@id/navigation_quick_search"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
<action
android:id="@+id/action_navigation_results_phone_to_navigation_player"
app:destination="@id/navigation_player"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
</fragment>
<fragment
android:id="@+id/navigation_results_tv"
android:name="com.lagradost.cloudstream3.ui.result.ResultFragmentTv"
android:layout_height="match_parent"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim"
tools:layout="@layout/fragment_result_swipe">
<action
android:id="@+id/action_navigation_results_tv_to_navigation_quick_search"
app:destination="@id/navigation_quick_search"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
<action
android:id="@+id/action_navigation_results_tv_to_navigation_player"
app:destination="@id/navigation_player"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
</fragment>
<!--<fragment
android:id="@+id/navigation_results"
android:name="com.lagradost.cloudstream3.ui.result.ResultFragment"
android:layout_height="match_parent"
@ -382,7 +436,7 @@
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
</fragment>
</fragment>-->
<fragment
android:id="@+id/navigation_player"

View file

@ -338,6 +338,7 @@
<style name="DarkFragment" parent="AppTheme">
<item name="android:navigationBarColor">?attr/colorPrimary</item>
</style>
<style name="AlertDialogCustom" parent="Theme.AppCompat.Dialog.Alert">
<item name="android:windowFullscreen">true</item>
<item name="android:textColor">?attr/textColor</item>
@ -476,7 +477,6 @@
</style>
<style name="BlackButton" parent="NiceButton">
<item name="strokeColor">?attr/textColor</item>
<item name="backgroundTint">?attr/iconGrayBackground</item>
@ -544,6 +544,23 @@
<style name="MultiSelectButton" parent="BlackButton">
<item name="android:layout_height">40dp</item>
<item name="android:layout_width">wrap_content</item>
<item name="strokeColor">?attr/textColor</item>
<item name="backgroundTint">?attr/iconGrayBackground</item>
<item name="iconTint">?attr/textColor</item>
<item name="android:textColor">?attr/textColor</item>
<item name="rippleColor">?attr/textColor</item>
</style>
<style name="SelectableButton" parent="NiceButton">
<item name="android:layout_height">40dp</item>
<item name="android:layout_width">wrap_content</item>
<item name="strokeColor">@color/selectable_black</item>
<item name="backgroundTint">@color/selectable_white</item>
<item name="iconTint">@color/selectable_black</item>
<item name="android:textColor">@color/selectable_black</item>
<item name="rippleColor">@color/selectable_black</item>
</style>
<style name="VideoButton">