AquaStream/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt

1116 lines
44 KiB
Kotlin
Raw Normal View History

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
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
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
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
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
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
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
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
import com.lagradost.cloudstream3.ui.player.FullScreenPlayer
2023-07-18 20:18:14 +00:00
import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment
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
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull
2023-07-18 20:18:14 +00:00
import com.lagradost.cloudstream3.utils.AppUtils.html
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
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
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
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
open class ResultFragmentPhone : FullScreenPlayer(),
2023-07-18 20:18:14 +00:00
PanelsChildGestureRegionObserver.GestureRegionsListener {
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
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? {
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
}
//result_trailer_thumbnail?.setImageBitmap(result_poster_background?.drawable?.toBitmap())
2023-07-18 01:55:00 +00:00
// result_trailer_loading?.isVisible = isSuccess
val turnVis = !isSuccess && !isFullScreenPlayer
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
}
2023-07-18 01:55:00 +00:00
binding?.resultFullscreenHolder?.isVisible = !isSuccess && isFullScreenPlayer
}
2023-07-18 01:55:00 +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
}
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)
}
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
}
var selectSeason: String? = null
private fun setUrl(url: String?) {
if (url == null) {
2023-07-18 20:18:14 +00:00
binding?.resultOpenInBrowser?.isVisible = false
return
}
val valid = url.startsWith("http")
2023-07-18 20:18:14 +00:00
binding?.resultOpenInBrowser?.apply {
isVisible = valid
2023-07-18 20:18:14 +00:00
setOnClickListener {
context?.openBrowser(url)
2023-07-18 20:18:14 +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()
}
private fun updateUI(id: Int?) {
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)
// ===== setup =====
2023-07-18 20:18:14 +00:00
UIHelper.fixPaddingStatusbar(binding?.resultTopBar)
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
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
resultCastItems.setLinearListLayout(
isHorizontal = true,
nextLeft = FOCUS_SELF,
nextRight = FOCUS_SELF
)
/*resultCastItems.layoutManager = object : LinearListLayout(view.context) {
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
}*/
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 ->
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 (!isFullScreenPlayer && 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 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
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
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
}
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-07-19 15:58:40 +00:00
DOWNLOAD_ACTION_LONG_CLICK -> {
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)
}
}
}
}
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()
}
}
}
2023-07-18 20:18:14 +00:00
(data as? Resource.Failure)?.let { data ->
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-05 23:41:35 +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
}
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
}
)
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-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-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 ->
if (load == null) {
loadingDialog?.dismissSafe(activity)
loadingDialog = null
2023-07-19 15:58:40 +00:00
return@observeNullable
}
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
viewModel.cancelLinks()
2022-08-04 22:26:33 +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
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
}
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
}
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-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)
}
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)
}
//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
}
}
}
}