2022-08-04 22:26:33 +00:00
package com.lagradost.cloudstream3.ui.result
2023-07-18 20:18:14 +00:00
import android.annotation.SuppressLint
2022-08-04 22:26:33 +00:00
import android.app.Dialog
2023-07-18 20:18:14 +00:00
import android.content.Intent
2023-07-18 01:55:00 +00:00
import android.content.res.ColorStateList
2022-08-04 22:26:33 +00:00
import android.graphics.Rect
2023-07-18 01:55:00 +00:00
import android.os.Build
2022-08-04 22:26:33 +00:00
import android.os.Bundle
2023-07-18 01:55:00 +00:00
import android.text.Editable
import android.view.LayoutInflater
2022-08-04 22:26:33 +00:00
import android.view.View
import android.view.ViewGroup
2022-11-01 17:01:29 +00:00
import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import android.view.animation.DecelerateInterpolator
2023-07-18 01:55:00 +00:00
import android.widget.AbsListView
import android.widget.ArrayAdapter
2022-08-13 11:20:07 +00:00
import android.widget.Toast
2023-07-18 20:18:14 +00:00
import androidx.appcompat.app.AlertDialog
2022-08-04 22:26:33 +00:00
import androidx.core.view.isGone
import androidx.core.view.isVisible
2022-08-05 23:41:35 +00:00
import androidx.core.widget.NestedScrollView
2023-07-18 01:55:00 +00:00
import androidx.core.widget.doOnTextChanged
2023-07-18 23:51:17 +00:00
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.RecyclerView
2022-08-04 22:26:33 +00:00
import com.discord.panels.OverlappingPanelsLayout
import com.discord.panels.PanelsChildGestureRegionObserver
2022-08-13 11:20:07 +00:00
import com.google.android.gms.cast.framework.CastButtonFactory
import com.google.android.gms.cast.framework.CastContext
import com.google.android.gms.cast.framework.CastState
2022-08-04 22:26:33 +00:00
import com.google.android.material.bottomsheet.BottomSheetDialog
2023-07-18 01:55:00 +00:00
import com.lagradost.cloudstream3.APIHolder
2022-08-04 22:26:33 +00:00
import com.lagradost.cloudstream3.APIHolder.updateHasTrailers
2023-07-18 01:55:00 +00:00
import com.lagradost.cloudstream3.CommonActivity
import com.lagradost.cloudstream3.DubStatus
import com.lagradost.cloudstream3.LoadResponse
2023-07-18 23:51:17 +00:00
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
2023-07-18 01:55:00 +00:00
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.databinding.FragmentResultBinding
import com.lagradost.cloudstream3.databinding.FragmentResultSwipeBinding
import com.lagradost.cloudstream3.databinding.ResultRecommendationsBinding
import com.lagradost.cloudstream3.databinding.ResultSyncBinding
import com.lagradost.cloudstream3.mvvm.Resource
2022-08-13 11:20:07 +00:00
import com.lagradost.cloudstream3.mvvm.logError
2023-07-18 01:55:00 +00:00
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
2022-08-04 22:26:33 +00:00
import com.lagradost.cloudstream3.mvvm.observe
2023-07-13 21:18:37 +00:00
import com.lagradost.cloudstream3.mvvm.observeNullable
2023-07-18 20:18:14 +00:00
import com.lagradost.cloudstream3.services.SubscriptionWorkManager
2023-07-18 01:55:00 +00:00
import com.lagradost.cloudstream3.ui.WatchType
2023-07-18 20:18:14 +00:00
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD
2023-07-19 15:58:40 +00:00
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_LONG_CLICK
2023-07-18 20:18:14 +00:00
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup
2022-08-05 23:41:35 +00:00
import com.lagradost.cloudstream3.ui.player.CSPlayerEvent
2023-07-18 23:51:17 +00:00
import com.lagradost.cloudstream3.ui.player.FullScreenPlayer
2023-07-18 20:18:14 +00:00
import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment
2023-07-18 23:51:17 +00:00
import com.lagradost.cloudstream3.ui.result.ResultFragment.getStoredData
import com.lagradost.cloudstream3.ui.result.ResultFragment.updateUIEvent
2022-08-04 22:26:33 +00:00
import com.lagradost.cloudstream3.ui.search.SearchAdapter
import com.lagradost.cloudstream3.ui.search.SearchHelper
2023-07-18 23:51:17 +00:00
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull
2023-07-18 20:18:14 +00:00
import com.lagradost.cloudstream3.utils.AppUtils.html
2022-08-13 11:20:07 +00:00
import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable
2023-07-27 19:47:42 +00:00
import com.lagradost.cloudstream3.utils.AppUtils.isLtr
import com.lagradost.cloudstream3.utils.AppUtils.isRtl
2023-07-18 23:51:17 +00:00
import com.lagradost.cloudstream3.utils.AppUtils.loadCache
2022-08-04 22:26:33 +00:00
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
2022-10-28 16:43:14 +00:00
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
2023-07-18 01:55:00 +00:00
import com.lagradost.cloudstream3.utils.UIHelper
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
2022-08-04 22:26:33 +00:00
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
2023-07-18 23:51:17 +00:00
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
2022-08-04 22:26:33 +00:00
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
2023-07-18 20:18:14 +00:00
import com.lagradost.cloudstream3.utils.UIHelper.populateChips
2022-08-04 22:26:33 +00:00
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
2023-07-18 20:18:14 +00:00
import com.lagradost.cloudstream3.utils.UIHelper.setImage
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
2023-07-18 23:51:17 +00:00
open class ResultFragmentPhone : FullScreenPlayer ( ) ,
2023-07-18 20:18:14 +00:00
PanelsChildGestureRegionObserver . GestureRegionsListener {
2023-07-18 23:51:17 +00:00
protected lateinit var viewModel : ResultViewModel2
protected lateinit var syncModel : SyncViewModel
2023-07-18 20:18:14 +00:00
protected var binding : FragmentResultSwipeBinding ? = null
protected var resultBinding : FragmentResultBinding ? = null
protected var recommendationBinding : ResultRecommendationsBinding ? = null
protected var syncBinding : ResultSyncBinding ? = null
2023-07-18 23:51:17 +00:00
override var layout = R . layout . fragment _result _swipe
2023-07-18 01:55:00 +00:00
override fun onCreateView (
inflater : LayoutInflater ,
container : ViewGroup ? ,
savedInstanceState : Bundle ?
) : View ? {
2023-07-18 23:51:17 +00:00
viewModel =
ViewModelProvider ( this ) [ ResultViewModel2 :: class . java ]
syncModel =
ViewModelProvider ( this ) [ SyncViewModel :: class . java ]
updateUIEvent += :: updateUI
2023-07-18 01:55:00 +00:00
val root = super . onCreateView ( inflater , container , savedInstanceState ) ?: return null
FragmentResultSwipeBinding . bind ( root ) . let { bind ->
resultBinding =
bind . fragmentResult //FragmentResultBinding.bind(binding.root.findViewById(R.id.fragment_result))
recommendationBinding = bind . resultRecommendations
syncBinding = bind . resultSync
binding = bind
}
return root
}
2022-08-04 22:26:33 +00:00
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
}
2022-11-01 17:01:29 +00:00
//result_trailer_thumbnail?.setImageBitmap(result_poster_background?.drawable?.toBitmap())
2023-07-18 01:55:00 +00:00
// result_trailer_loading?.isVisible = isSuccess
2022-11-01 17:01:29 +00:00
val turnVis = !is Success && !is FullScreenPlayer
2023-07-18 01:55:00 +00:00
resultBinding ?. apply {
resultSmallscreenHolder . isVisible = turnVis
resultPosterBackgroundHolder . apply {
val fadeIn : Animation = AlphaAnimation ( alpha , if ( turnVis ) 1.0f else 0.0f ) . apply {
interpolator = DecelerateInterpolator ( )
duration = 200
fillAfter = true
}
clearAnimation ( )
startAnimation ( fadeIn )
}
// We don't want the trailer to be focusable if it's not visible
resultSmallscreenHolder . descendantFocusability = if ( isSuccess ) {
ViewGroup . FOCUS _AFTER _DESCENDANTS
} else {
ViewGroup . FOCUS _BLOCK _DESCENDANTS
2022-11-01 17:01:29 +00:00
}
2023-07-18 01:55:00 +00:00
binding ?. resultFullscreenHolder ?. isVisible = !is Success && isFullScreenPlayer
2022-11-01 17:01:29 +00:00
}
2023-07-18 01:55:00 +00:00
2022-11-01 17:01:29 +00:00
//player_view?.apply {
//alpha = 0.0f
//ObjectAnimator.ofFloat(player_view, "alpha", 1f).apply {
// duration = 200
// start()
//}
//val fadeIn: Animation = AlphaAnimation(0.0f, 1f).apply {
// interpolator = DecelerateInterpolator()
// duration = 2000
// fillAfter = true
//}
//startAnimation(fadeIn)
// }
2022-08-04 22:26:33 +00:00
2023-07-18 01:55:00 +00:00
2022-08-04 22:26:33 +00:00
}
2023-07-18 23:51:17 +00:00
private fun setTrailers ( trailers : List < ExtractorLink > ? ) {
2022-08-04 22:26:33 +00:00
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 ->
2023-07-18 01:55:00 +00:00
resultBinding ?. resultCastItems ?. let {
2022-08-04 22:26:33 +00:00
obs . unregister ( it )
}
obs . removeGestureRegionsUpdateListener ( this )
}
2023-07-18 23:51:17 +00:00
updateUIEvent -= :: updateUI
2023-07-18 01:55:00 +00:00
binding = null
resultBinding = null
syncBinding = null
recommendationBinding = null
2022-08-04 22:26:33 +00:00
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
}
2022-11-01 17:01:29 +00:00
var selectSeason : String ? = null
2022-10-28 16:43:14 +00:00
2023-07-18 23:51:17 +00:00
private fun setUrl ( url : String ? ) {
if ( url == null ) {
2023-07-18 20:18:14 +00:00
binding ?. resultOpenInBrowser ?. isVisible = false
return
}
2023-07-18 23:51:17 +00:00
val valid = url . startsWith ( " http " )
2023-07-18 20:18:14 +00:00
binding ?. resultOpenInBrowser ?. apply {
2023-07-18 23:51:17 +00:00
isVisible = valid
2023-07-18 20:18:14 +00:00
setOnClickListener {
2023-07-18 23:51:17 +00:00
context ?. openBrowser ( url )
2023-07-18 20:18:14 +00:00
}
}
2023-07-18 23:51:17 +00:00
resultBinding ?. resultReloadConnectionOpenInBrowser ?. setOnClickListener {
view ?. context ?. openBrowser ( url )
}
resultBinding ?. resultMetaSite ?. setOnClickListener {
view ?. context ?. openBrowser ( url )
}
}
private fun reloadViewModel ( forceReload : Boolean ) {
if ( ! viewModel . hasLoaded ( ) || forceReload ) {
val storedData = getStoredData ( ) ?: return
viewModel . load (
activity ,
storedData . url ,
storedData . apiName ,
storedData . showFillers ,
storedData . dubStatus ,
storedData . start
)
}
}
override fun onResume ( ) {
afterPluginsLoadedEvent += :: reloadViewModel
activity ?. let {
it . window ?. navigationBarColor =
it . colorFromAttribute ( R . attr . primaryBlackBackground )
}
super . onResume ( )
}
override fun onStop ( ) {
afterPluginsLoadedEvent -= :: reloadViewModel
super . onStop ( )
}
2023-08-04 03:37:41 +00:00
private fun updateUI ( id : Int ? ) {
2023-07-18 23:51:17 +00:00
syncModel . updateUserData ( )
viewModel . reloadEpisodes ( )
2023-07-18 20:18:14 +00:00
}
@SuppressLint ( " SetTextI18n " )
2022-08-04 22:26:33 +00:00
override fun onViewCreated ( view : View , savedInstanceState : Bundle ? ) {
super . onViewCreated ( view , savedInstanceState )
2023-07-18 23:51:17 +00:00
// ===== setup =====
2023-07-18 20:18:14 +00:00
UIHelper . fixPaddingStatusbar ( binding ?. resultTopBar )
2023-07-18 23:51:17 +00:00
val storedData = getStoredData ( ) ?: return
activity ?. window ?. decorView ?. clearFocus ( )
activity ?. loadCache ( )
context ?. updateHasTrailers ( )
hideKeyboard ( )
if ( storedData . restart || ! viewModel . hasLoaded ( ) )
viewModel . load (
activity ,
storedData . url ,
storedData . apiName ,
storedData . showFillers ,
storedData . dubStatus ,
storedData . start
)
setUrl ( storedData . url )
syncModel . addFromUrl ( storedData . url )
val api = APIHolder . getApiFromNameNull ( storedData . apiName )
PanelsChildGestureRegionObserver . Provider . get ( ) . addGestureRegionsUpdateListener ( this )
// ===== ===== =====
2023-07-18 01:55:00 +00:00
2023-07-18 23:51:17 +00:00
resultBinding ?. apply {
resultReloadConnectionerror . setOnClickListener {
viewModel . load (
activity ,
storedData . url ,
storedData . apiName ,
storedData . showFillers ,
storedData . dubStatus ,
storedData . start
)
}
2023-07-18 01:55:00 +00:00
2023-08-04 03:37:41 +00:00
resultCastItems . setLinearListLayout (
isHorizontal = true ,
nextLeft = FOCUS _SELF ,
nextRight = FOCUS _SELF
)
/ * resultCastItems . layoutManager = object : LinearListLayout ( view . context ) {
2023-07-18 23:51:17 +00:00
override fun onRequestChildFocus (
parent : RecyclerView ,
state : RecyclerView . State ,
child : View ,
focused : View ?
) : Boolean {
// Make the cast always focus the first visible item when focused
// from somewhere else. Otherwise it jumps to the last item.
return if ( parent . focusedChild == null ) {
scrollToPosition ( this . findFirstCompletelyVisibleItemPosition ( ) )
true
} else {
super . onRequestChildFocus ( parent , state , child , focused )
}
}
} . apply {
this . orientation = RecyclerView . HORIZONTAL
2023-08-04 03:37:41 +00:00
} * /
2023-07-18 23:51:17 +00:00
resultCastItems . adapter = ActorAdaptor ( )
2023-07-18 01:55:00 +00:00
2023-07-18 20:18:14 +00:00
resultEpisodes . adapter =
EpisodeAdapter (
api ?. hasDownloadSupport == true ,
{ episodeClick ->
2023-07-18 23:51:17 +00:00
viewModel . handleAction ( episodeClick )
2023-07-18 20:18:14 +00:00
} ,
{ downloadClickEvent ->
DownloadButtonSetup . handleDownloadClick ( downloadClickEvent )
}
)
resultCastItems . let {
PanelsChildGestureRegionObserver . Provider . get ( ) . register ( it )
}
resultScroll . setOnScrollChangeListener ( NestedScrollView . OnScrollChangeListener { _ , _ , scrollY , _ , oldScrollY ->
val dy = scrollY - oldScrollY
if ( dy > 0 ) { //check for scroll down
binding ?. resultBookmarkFab ?. shrink ( )
} else if ( dy < - 5 ) {
binding ?. resultBookmarkFab ?. extend ( )
2023-07-18 01:55:00 +00:00
}
2023-07-18 20:18:14 +00:00
if ( !is FullScreenPlayer && player . getIsPlaying ( ) ) {
if ( scrollY > ( resultBinding ?. fragmentTrailer ?. playerBackground ?. height
?: scrollY )
) {
player . handleEvent ( CSPlayerEvent . Pause )
}
}
} )
2023-07-18 01:55:00 +00:00
}
2023-07-18 20:18:14 +00:00
binding ?. apply {
resultOverlappingPanels . setStartPanelLockState ( OverlappingPanelsLayout . LockState . CLOSE )
resultOverlappingPanels . setEndPanelLockState ( OverlappingPanelsLayout . LockState . CLOSE )
resultBack . setOnClickListener {
activity ?. popCurrentPage ( )
}
2023-07-18 23:51:17 +00:00
2023-07-18 20:18:14 +00:00
resultMiniSync . adapter = ImageAdapter (
nextFocusDown = R . id . result _sync _set _score ,
clickCallback = { action ->
if ( action == IMAGE _CLICK || action == IMAGE _LONG _CLICK ) {
if ( binding ?. resultOverlappingPanels ?. getSelectedPanel ( ) ?. ordinal == 1 ) {
binding ?. resultOverlappingPanels ?. openStartPanel ( )
} else {
binding ?. resultOverlappingPanels ?. closePanels ( )
}
}
} )
resultSubscribe . setOnClickListener {
val isSubscribed =
viewModel . toggleSubscriptionStatus ( ) ?: return @setOnClickListener
val message = if ( isSubscribed ) {
// Kinda icky to have this here, but it works.
SubscriptionWorkManager . enqueuePeriodicWork ( context )
R . string . subscription _new
} else {
R . string . subscription _deleted
}
2022-08-04 22:26:33 +00:00
2023-07-18 20:18:14 +00:00
val name = ( viewModel . page . value as ? Resource . Success ) ?. value ?. title
?: txt ( R . string . no _data ) . asStringNull ( context ) ?: " "
CommonActivity . showToast ( txt ( message , name ) , Toast . LENGTH _SHORT )
}
mediaRouteButton . apply {
val chromecastSupport = api ?. hasChromecastSupport == true
alpha = if ( chromecastSupport ) 1f else 0.3f
if ( ! chromecastSupport ) {
setOnClickListener {
CommonActivity . showToast (
R . string . no _chromecast _support _toast ,
Toast . LENGTH _LONG
)
}
}
activity ?. let { act ->
if ( act . isCastApiAvailable ( ) ) {
try {
CastButtonFactory . setUpMediaRouteButton ( act , this )
val castContext = CastContext . getSharedInstance ( act . applicationContext )
isGone = castContext . castState == CastState . NO _DEVICES _AVAILABLE
// this shit leaks for some reason
//castContext.addCastStateListener { state ->
// media_route_button?.isGone = state == CastState.NO_DEVICES_AVAILABLE
//}
} catch ( e : Exception ) {
logError ( e )
}
}
}
}
2022-08-04 22:26:33 +00:00
}
2023-07-18 20:18:14 +00:00
playerBinding ?. apply {
playerOpenSource . setOnClickListener {
currentTrailers . getOrNull ( currentTrailerIndex ) ?. let {
context ?. openBrowser ( it . url )
}
}
}
2022-08-04 22:26:33 +00:00
2023-07-18 20:18:14 +00:00
recommendationBinding ?. apply {
resultRecommendationsList . apply {
spanCount = 3
adapter =
SearchAdapter (
ArrayList ( ) ,
this ,
) { callback ->
SearchHelper . handleSearchClickCallback ( callback )
}
}
2022-08-04 22:26:33 +00:00
}
2023-07-18 20:18:14 +00:00
2022-08-20 01:06:35 +00:00
/ *
2022-08-04 22:26:33 +00:00
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 ) )
}
2022-08-20 01:06:35 +00:00
} * /
2022-08-04 22:26:33 +00:00
2023-07-18 23:51:17 +00:00
observeNullable ( viewModel . resumeWatching ) { resume ->
resultBinding ?. apply {
if ( resume == null ) {
resultResumeParent . isVisible = false
return @observeNullable
}
resultResumeParent . isVisible = true
resume . progress ?. let { progress ->
resultResumeSeriesTitle . apply {
isVisible = ! resume . isMovie
text =
if ( resume . isMovie ) null else context ?. getNameFull (
resume . result . name ,
resume . result . episode ,
resume . result . season
)
}
resultResumeSeriesProgressText . setText ( progress . progressLeft )
resultResumeSeriesProgress . apply {
isVisible = true
this . max = progress . maxProgress
this . progress = progress . progress
}
resultResumeProgressHolder . isVisible = true
} ?: run {
resultResumeProgressHolder . isVisible = false
resultResumeSeriesProgress . isVisible = false
resultResumeSeriesTitle . isVisible = false
resultResumeSeriesProgressText . isVisible = false
}
resultResumeSeriesButton . isVisible = ! resume . isMovie
resultResumeSeriesButton . setOnClickListener {
viewModel . handleAction (
EpisodeClickEvent (
storedData . playerAction , //?: ACTION_PLAY_EPISODE_IN_PLAYER,
resume . result
)
)
}
}
}
2023-07-18 20:18:14 +00:00
observeNullable ( viewModel . subscribeStatus ) { isSubscribed ->
binding ?. resultSubscribe ?. isVisible = isSubscribed != null
if ( isSubscribed == null ) return @observeNullable
2022-08-04 22:26:33 +00:00
2023-07-18 20:18:14 +00:00
val drawable = if ( isSubscribed ) {
R . drawable . ic _baseline _notifications _active _24
} else {
R . drawable . baseline _notifications _none _24
2022-08-05 23:41:35 +00:00
}
2023-07-18 20:18:14 +00:00
binding ?. resultSubscribe ?. setImageResource ( drawable )
}
observe ( viewModel . trailers ) { trailers ->
setTrailers ( trailers . flatMap { it . mirros } ) // I dont care about subtitles yet!
}
observeNullable ( viewModel . episodes ) { episodes ->
resultBinding ?. apply {
// no failure?
resultEpisodeLoading . isVisible = episodes is Resource . Loading
resultEpisodes . isVisible = episodes is Resource . Success
2023-07-18 23:51:17 +00:00
if ( episodes is Resource . Success ) {
2023-07-18 20:18:14 +00:00
( resultEpisodes . adapter as ? EpisodeAdapter ) ?. updateList ( episodes . value )
2022-08-05 23:41:35 +00:00
}
}
2023-07-18 20:18:14 +00:00
}
2022-08-13 11:20:07 +00:00
2023-07-18 20:18:14 +00:00
observeNullable ( viewModel . movie ) { data ->
resultBinding ?. apply {
resultPlayMovie . isVisible = data is Resource . Success
downloadButton . isVisible =
data is Resource . Success && viewModel . currentRepo ?. api ?. hasDownloadSupport == true
( data as ? Resource . Success ) ?. value ?. let { ( text , ep ) ->
resultPlayMovie . setText ( text )
resultPlayMovie . setOnClickListener {
viewModel . handleAction (
EpisodeClickEvent ( ACTION _CLICK _DEFAULT , ep )
)
}
resultPlayMovie . setOnLongClickListener {
viewModel . handleAction (
EpisodeClickEvent ( ACTION _SHOW _OPTIONS , ep )
)
return @setOnLongClickListener true
}
downloadButton . setDefaultClickListener (
VideoDownloadHelper . DownloadEpisodeCached (
ep . name ,
ep . poster ,
0 ,
null ,
ep . id ,
ep . id ,
null ,
null ,
System . currentTimeMillis ( ) ,
2023-07-19 15:27:47 +00:00
) ,
null
2023-07-18 20:18:14 +00:00
) { click ->
when ( click . action ) {
DOWNLOAD _ACTION _DOWNLOAD -> {
viewModel . handleAction (
EpisodeClickEvent ( ACTION _DOWNLOAD _EPISODE , ep )
)
}
2023-08-04 03:37:41 +00:00
2023-07-19 15:58:40 +00:00
DOWNLOAD _ACTION _LONG _CLICK -> {
2023-08-04 03:37:41 +00:00
viewModel . handleAction (
EpisodeClickEvent (
ACTION _DOWNLOAD _MIRROR ,
ep
)
)
2023-07-19 15:58:40 +00:00
}
2023-07-18 20:18:14 +00:00
else -> DownloadButtonSetup . handleDownloadClick ( click )
}
}
2022-08-13 11:20:07 +00:00
}
}
2023-07-18 20:18:14 +00:00
}
observe ( viewModel . page ) { data ->
if ( data == null ) return @observe
resultBinding ?. apply {
( data as ? Resource . Success ) ?. value ?. let { d ->
resultVpn . setText ( d . vpnText )
resultInfo . setText ( d . metaText )
resultNoEpisodes . setText ( d . noEpisodesFoundText )
resultTitle . setText ( d . titleText )
resultMetaSite . setText ( d . apiName )
resultMetaType . setText ( d . typeText )
resultMetaYear . setText ( d . yearText )
resultMetaDuration . setText ( d . durationText )
resultMetaRating . setText ( d . ratingText )
resultCastText . setText ( d . actorsText )
resultNextAiring . setText ( d . nextAiringEpisode )
resultNextAiringTime . setText ( d . nextAiringDate )
resultPoster . setImage ( d . posterImage )
resultPosterBackground . setImage ( d . posterBackgroundImage )
resultDescription . setTextHtml ( d . plotText )
resultDescription . setOnClickListener { view ->
// todo bottom view?
view . context ?. let { ctx ->
val builder : AlertDialog . Builder =
AlertDialog . Builder ( ctx , R . style . AlertDialogCustom )
builder . setMessage ( d . plotText . asString ( ctx ) . html ( ) )
. setTitle ( d . plotHeaderText . asString ( ctx ) )
. show ( )
}
}
populateChips ( resultTag , d . tags )
resultComingSoon . isVisible = d . comingSoon
resultDataHolder . isGone = d . comingSoon
resultCastItems . isGone = d . actors . isNullOrEmpty ( )
( resultCastItems . adapter as ? ActorAdaptor ) ?. updateList ( d . actors ?: emptyList ( ) )
if ( syncModel . addSyncs ( d . syncData ) ) {
syncModel . updateMetaAndUser ( )
syncModel . updateSynced ( )
} else {
syncModel . addFromUrl ( d . url )
}
binding ?. apply {
resultSearch . setOnClickListener {
QuickSearchFragment . pushSearch ( activity , d . title )
}
resultShare . setOnClickListener {
try {
val i = Intent ( Intent . ACTION _SEND )
i . type = " text/plain "
i . putExtra ( Intent . EXTRA _SUBJECT , d . title )
i . putExtra ( Intent . EXTRA _TEXT , d . url )
startActivity ( Intent . createChooser ( i , d . title ) )
} catch ( e : Exception ) {
logError ( e )
}
}
setUrl ( d . url )
resultBookmarkFab . apply {
isVisible = true
extend ( )
}
2022-08-13 11:20:07 +00:00
}
}
2023-07-18 20:18:14 +00:00
( data as ? Resource . Failure ) ?. let { data ->
2023-07-18 23:51:17 +00:00
resultErrorText . text = storedData . url . plus ( " \n " ) + data . errorString
2023-07-18 20:18:14 +00:00
}
binding ?. resultBookmarkFab ?. isVisible = data is Resource . Success
resultFinishLoading . isVisible = data is Resource . Success
resultLoading . isVisible = data is Resource . Loading
resultLoadingError . isVisible = data is Resource . Failure
resultErrorText . isVisible = data is Resource . Failure
resultReloadConnectionOpenInBrowser . isVisible = data is Resource . Failure
2022-08-13 11:20:07 +00:00
}
}
2022-08-05 23:41:35 +00:00
2023-07-13 21:18:37 +00:00
observeNullable ( viewModel . episodesCountText ) { count ->
2023-07-18 01:55:00 +00:00
resultBinding ?. resultEpisodesText . setText ( count )
2022-08-06 18:36:45 +00:00
}
2023-07-13 21:18:37 +00:00
observeNullable ( viewModel . selectPopup ) { popup ->
if ( popup == null ) {
popupDialog ?. dismissSafe ( activity )
popupDialog = null
return @observeNullable
}
popupDialog ?. dismissSafe ( activity )
popupDialog = activity ?. let { act ->
val options = popup . getOptions ( act )
val title = popup . getTitle ( act )
act . showBottomDialogInstant (
options , title , {
popupDialog = null
popup . callback ( null )
} , {
popupDialog = null
popup . callback ( it )
2022-08-04 22:26:33 +00:00
}
2023-07-13 21:18:37 +00:00
)
2022-08-04 22:26:33 +00:00
}
2023-07-18 01:55:00 +00:00
}
observe ( syncModel . synced ) { list ->
syncBinding ?. resultSyncNames ?. text =
list . filter { it . isSynced && it . hasAccount } . joinToString { it . name }
val newList = list . filter { it . isSynced && it . hasAccount }
binding ?. resultMiniSync ?. isVisible = newList . isNotEmpty ( )
( binding ?. resultMiniSync ?. adapter as ? ImageAdapter ) ?. updateList ( newList . mapNotNull { it . icon } )
}
2023-07-18 20:18:14 +00:00
2023-07-18 01:55:00 +00:00
var currentSyncProgress = 0
fun setSyncMaxEpisodes ( totalEpisodes : Int ? ) {
syncBinding ?. resultSyncEpisodes ?. max = ( totalEpisodes ?: 0 ) * 1000
normalSafeApiCall {
val ctx = syncBinding ?. resultSyncEpisodes ?. context
syncBinding ?. resultSyncMaxEpisodes ?. text =
totalEpisodes ?. let { episodes ->
ctx ?. getString ( R . string . sync _total _episodes _some ) ?. format ( episodes )
} ?: run {
ctx ?. getString ( R . string . sync _total _episodes _none )
}
}
}
observe ( syncModel . metadata ) { meta ->
when ( meta ) {
is Resource . Success -> {
val d = meta . value
syncBinding ?. resultSyncEpisodes ?. progress = currentSyncProgress * 1000
setSyncMaxEpisodes ( d . totalEpisodes )
viewModel . setMeta ( d , syncModel . getSyncs ( ) )
}
is Resource . Loading -> {
syncBinding ?. resultSyncMaxEpisodes ?. text =
syncBinding ?. resultSyncMaxEpisodes ?. context ?. getString ( R . string . sync _total _episodes _none )
}
else -> { }
}
}
observe ( syncModel . userData ) { status ->
var closed = false
syncBinding ?. apply {
when ( status ) {
is Resource . Failure -> {
resultSyncLoadingShimmer . stopShimmer ( )
resultSyncLoadingShimmer . isVisible = false
resultSyncHolder . isVisible = false
closed = true
}
is Resource . Loading -> {
resultSyncLoadingShimmer . startShimmer ( )
resultSyncLoadingShimmer . isVisible = true
resultSyncHolder . isVisible = false
}
is Resource . Success -> {
resultSyncLoadingShimmer . stopShimmer ( )
resultSyncLoadingShimmer . isVisible = false
resultSyncHolder . isVisible = true
val d = status . value
resultSyncRating . value = d . score ?. toFloat ( ) ?: 0.0f
resultSyncCheck . setItemChecked ( d . status + 1 , true )
val watchedEpisodes = d . watchedEpisodes ?: 0
currentSyncProgress = watchedEpisodes
d . maxEpisodes ?. let {
// don't directly call it because we don't want to override metadata observe
setSyncMaxEpisodes ( it )
}
if ( Build . VERSION . SDK _INT >= Build . VERSION_CODES . N ) {
resultSyncEpisodes . setProgress ( watchedEpisodes * 1000 , true )
} else {
resultSyncEpisodes . progress = watchedEpisodes * 1000
}
resultSyncCurrentEpisodes . text =
Editable . Factory . getInstance ( ) ?. newEditable ( watchedEpisodes . toString ( ) )
normalSafeApiCall { // format might fail
context ?. getString ( R . string . sync _score _format ) ?. format ( d . score ?: 0 )
?. let {
resultSyncScoreText . text = it
}
}
}
null -> {
closed = false
}
}
}
binding ?. resultOverlappingPanels ?. setStartPanelLockState ( if ( closed ) OverlappingPanelsLayout . LockState . CLOSE else OverlappingPanelsLayout . LockState . UNLOCKED )
}
2023-07-18 20:18:14 +00:00
observe ( viewModel . recommendations ) { recommendations ->
setRecommendations ( recommendations , null )
}
observe ( viewModel . episodeSynopsis ) { description ->
// TODO bottom dialog
view . context ?. let { ctx ->
val builder : AlertDialog . Builder =
AlertDialog . Builder ( ctx , R . style . AlertDialogCustom )
builder . setMessage ( description . html ( ) )
. setTitle ( R . string . synopsis )
. setOnDismissListener {
viewModel . releaseEpisodeSynopsis ( )
}
. show ( )
}
}
2023-07-18 01:55:00 +00:00
context ?. let { ctx ->
val arrayAdapter = ArrayAdapter < String > ( ctx , R . layout . sort _bottom _single _choice )
/ *
- 1 -> None
0 -> Watching
1 -> Completed
2 -> OnHold
3 -> Dropped
4 -> PlanToWatch
5 -> ReWatching
* /
val items = listOf (
R . string . none ,
R . string . type _watching ,
R . string . type _completed ,
R . string . type _on _hold ,
R . string . type _dropped ,
R . string . type _plan _to _watch ,
R . string . type _re _watching
) . map { ctx . getString ( it ) }
arrayAdapter . addAll ( items )
syncBinding ?. apply {
resultSyncCheck . choiceMode = AbsListView . CHOICE _MODE _SINGLE
resultSyncCheck . adapter = arrayAdapter
UIHelper . setListViewHeightBasedOnItems ( resultSyncCheck )
resultSyncCheck . setOnItemClickListener { _ , _ , which , _ ->
syncModel . setStatus ( which - 1 )
}
2023-07-13 21:18:37 +00:00
2023-07-18 01:55:00 +00:00
resultSyncRating . addOnChangeListener { _ , value , _ ->
syncModel . setScore ( value . toInt ( ) )
}
resultSyncAddEpisode . setOnClickListener {
syncModel . setEpisodesDelta ( 1 )
}
resultSyncSubEpisode . setOnClickListener {
syncModel . setEpisodesDelta ( - 1 )
}
resultSyncCurrentEpisodes . doOnTextChanged { text , _ , before , count ->
if ( count == before ) return @doOnTextChanged
text ?. toString ( ) ?. toIntOrNull ( ) ?. let { ep ->
syncModel . setEpisodes ( ep )
}
}
}
}
syncBinding ?. resultSyncSetScore ?. setOnClickListener {
syncModel . publishUserData ( )
}
observe ( viewModel . watchStatus ) { watchType ->
binding ?. resultBookmarkFab ?. apply {
if ( watchType == WatchType . NONE ) {
context ?. colorFromAttribute ( R . attr . white )
} else {
context ?. colorFromAttribute ( R . attr . colorPrimary )
} ?. let {
val colorState = ColorStateList . valueOf ( it )
iconTint = colorState
setTextColor ( colorState )
}
2023-07-13 21:18:37 +00:00
2023-07-18 01:55:00 +00:00
setOnClickListener { fab ->
activity ?. showBottomDialog (
WatchType . values ( ) . map { fab . context . getString ( it . stringRes ) } . toList ( ) ,
watchType . ordinal ,
fab . context . getString ( R . string . action _add _to _bookmarks ) ,
showApply = false ,
{ } ) {
viewModel . updateWatchStatus ( WatchType . values ( ) [ it ] )
}
}
}
2022-08-04 22:26:33 +00:00
}
2023-07-18 01:55:00 +00:00
2023-07-19 15:58:40 +00:00
observeNullable ( viewModel . loadedLinks ) { load ->
2023-07-13 21:18:37 +00:00
if ( load == null ) {
loadingDialog ?. dismissSafe ( activity )
loadingDialog = null
2023-07-19 15:58:40 +00:00
return @observeNullable
2023-07-13 21:18:37 +00:00
}
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 {
2022-08-04 22:26:33 +00:00
loadingDialog = null
2023-07-13 21:18:37 +00:00
viewModel . cancelLinks ( )
2022-08-04 22:26:33 +00:00
}
2023-07-13 21:18:37 +00:00
//builder.setOnCancelListener {
// it?.dismiss()
//}
builder . setCanceledOnTouchOutside ( true )
builder . show ( )
builder
2022-08-04 22:26:33 +00:00
}
}
2022-08-06 18:51:32 +00:00
2023-07-13 21:18:37 +00:00
observeNullable ( viewModel . selectedSeason ) { text ->
2023-07-18 01:55:00 +00:00
resultBinding ?. apply {
resultSeasonButton . setText ( text )
selectSeason =
text ?. asStringNull ( resultSeasonButton . context )
// If the season button is visible the result season button will be next focus down
if ( resultSeasonButton . isVisible && resultResumeParent . isVisible ) {
setFocusUpAndDown ( resultResumeSeriesButton , resultSeasonButton )
}
}
2022-08-04 22:26:33 +00:00
}
2023-07-13 21:18:37 +00:00
observeNullable ( viewModel . selectedDubStatus ) { status ->
2023-07-18 01:55:00 +00:00
resultBinding ?. apply {
resultDubSelect . setText ( status )
if ( resultDubSelect . isVisible && ! resultSeasonButton . isVisible && ! resultEpisodeSelect . isVisible && resultResumeParent . isVisible ) {
setFocusUpAndDown ( resultResumeSeriesButton , resultDubSelect )
2022-08-04 22:26:33 +00:00
}
2023-07-18 01:55:00 +00:00
}
2022-08-04 22:26:33 +00:00
}
2023-07-13 21:18:37 +00:00
observeNullable ( viewModel . selectedRange ) { range ->
2023-07-18 01:55:00 +00:00
resultBinding ?. apply {
resultEpisodeSelect . setText ( range )
// If Season button is invisible then the bookmark button next focus is episode select
if ( resultEpisodeSelect . isVisible && ! resultSeasonButton . isVisible && resultResumeParent . isVisible ) {
setFocusUpAndDown ( resultResumeSeriesButton , resultEpisodeSelect )
2022-08-04 22:26:33 +00:00
}
2023-07-18 01:55:00 +00:00
}
2022-08-04 22:26:33 +00:00
}
// val preferDub = context?.getApiDubstatusSettings()?.all { it == DubStatus.Dubbed } == true
observe ( viewModel . dubSubSelections ) { range ->
2023-07-18 01:55:00 +00:00
resultBinding ?. resultDubSelect ?. setOnClickListener { view ->
2022-08-04 22:26:33 +00:00
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 ->
2023-07-18 01:55:00 +00:00
resultBinding ?. resultEpisodeSelect ?. setOnClickListener { view ->
2022-08-04 22:26:33 +00:00
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 ->
2023-07-18 01:55:00 +00:00
resultBinding ?. resultSeasonButton ?. setOnClickListener { view ->
2022-10-28 16:43:14 +00:00
2022-08-04 22:26:33 +00:00
view ?. context ?. let { ctx ->
val names = seasonList
. mapNotNull { ( text , r ) ->
r to ( text ?. asStringNull ( ctx ) ?: return @mapNotNull null )
}
2022-11-01 17:01:29 +00:00
activity ?. showDialog (
names . map { it . second } ,
names . indexOfFirst { it . second == selectSeason } ,
" " ,
false ,
{ } ) { itemId ->
2022-08-04 22:26:33 +00:00
viewModel . changeSeason ( names [ itemId ] . first )
}
2022-10-28 16:43:14 +00:00
//view.popupMenuNoIconsAndNoStringRes(names.mapIndexed { index, (_, name) ->
// index to name
//}) {
// viewModel.changeSeason(names[itemId].first)
//}
2022-08-04 22:26:33 +00:00
}
}
}
}
override fun onPause ( ) {
super . onPause ( )
PanelsChildGestureRegionObserver . Provider . get ( ) . addGestureRegionsUpdateListener ( this )
}
override fun onGestureRegionsUpdate ( gestureRegions : List < Rect > ) {
2023-07-18 01:55:00 +00:00
binding ?. resultOverlappingPanels ?. setChildGestureRegions ( gestureRegions )
2022-08-04 22:26:33 +00:00
}
2023-07-18 20:18:14 +00:00
private fun setRecommendations ( rec : List < SearchResponse > ? , validApiName : String ? ) {
2022-08-04 22:26:33 +00:00
val isInvalid = rec . isNullOrEmpty ( )
val matchAgainst = validApiName ?: rec ?. firstOrNull ( ) ?. apiName
2023-07-18 01:55:00 +00:00
recommendationBinding ?. apply {
root . isGone = isInvalid
root . post {
rec ?. let { list ->
( resultRecommendationsList . adapter as ? SearchAdapter ) ?. updateList ( list . filter { it . apiName == matchAgainst } )
2022-08-04 22:26:33 +00:00
}
}
}
2023-07-18 01:55:00 +00:00
binding ?. apply {
resultRecommendationsBtt . isGone = isInvalid
resultRecommendationsBtt . setOnClickListener {
val nextFocusDown = if ( resultOverlappingPanels . getSelectedPanel ( ) . ordinal == 1 ) {
resultOverlappingPanels . openEndPanel ( )
R . id . result _recommendations
} else {
resultOverlappingPanels . closePanels ( )
R . id . result _description
}
resultBinding ?. apply {
resultRecommendationsBtt . nextFocusDownId = nextFocusDown
resultSearch . nextFocusDownId = nextFocusDown
resultOpenInBrowser . nextFocusDownId = nextFocusDown
resultShare . nextFocusDownId = nextFocusDown
}
}
resultOverlappingPanels . setEndPanelLockState ( if ( isInvalid ) OverlappingPanelsLayout . LockState . CLOSE else OverlappingPanelsLayout . LockState . UNLOCKED )
rec ?. map { it . apiName } ?. distinct ( ) ?. let { apiNames ->
// very dirty selection
recommendationBinding ?. resultRecommendationsFilterButton ?. apply {
isVisible = apiNames . size > 1
text = matchAgainst
setOnClickListener { _ ->
activity ?. showBottomDialog (
apiNames ,
apiNames . indexOf ( matchAgainst ) ,
getString ( R . string . home _change _provider _img _des ) , false , { }
) {
setRecommendations ( rec , apiNames [ it ] )
}
}
}
} ?: run {
recommendationBinding ?. resultRecommendationsFilterButton ?. isVisible = false
2022-08-04 22:26:33 +00:00
}
}
}
}