Dantotsu/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchFragment.kt
2023-10-29 19:45:11 -05:00

412 lines
No EOL
17 KiB
Kotlin

package ani.dantotsu.media.anime
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.Toast
import androidx.cardview.widget.CardView
import androidx.core.math.MathUtils
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager
import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.*
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.parsers.AnimeParser
import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.HAnimeSources
import ani.dantotsu.settings.ExtensionsActivity
import ani.dantotsu.settings.InstalledAnimeExtensionsFragment
import ani.dantotsu.settings.PlayerSettings
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
import ani.dantotsu.subcriptions.Notifications
import ani.dantotsu.subcriptions.Notifications.Group.ANIME_GROUP
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
import ani.dantotsu.subcriptions.SubscriptionHelper
import ani.dantotsu.subcriptions.SubscriptionHelper.Companion.saveSubscription
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.navigationrail.NavigationRailView
import com.google.android.material.tabs.TabLayout
import com.google.android.material.textfield.TextInputLayout
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.roundToInt
class AnimeWatchFragment : Fragment() {
private var _binding: FragmentAnimeWatchBinding? = null
private val binding get() = _binding!!
private val model: MediaDetailsViewModel by activityViewModels()
private lateinit var media: Media
private var start = 0
private var end: Int? = null
private var style: Int? = null
private var reverse = false
private lateinit var headerAdapter: AnimeWatchAdapter
private lateinit var episodeAdapter: EpisodeAdapter
var screenWidth = 0f
private var progress = View.VISIBLE
var continueEp: Boolean = false
var loaded = false
lateinit var playerSettings: PlayerSettings
lateinit var uiSettings: UserInterfaceSettings
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentAnimeWatchBinding.inflate(inflater, container, false)
return _binding?.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight)
screenWidth = resources.displayMetrics.widthPixels.dp
var maxGridSize = (screenWidth / 100f).roundToInt()
maxGridSize = max(4, maxGridSize - (maxGridSize % 2))
playerSettings =
loadData("player_settings", toast = false) ?: PlayerSettings().apply { saveData("player_settings", this) }
uiSettings = loadData("ui_settings", toast = false) ?: UserInterfaceSettings().apply { saveData("ui_settings", this) }
val gridLayoutManager = GridLayoutManager(requireContext(), maxGridSize)
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
val style = episodeAdapter.getItemViewType(position)
return when (position) {
0 -> maxGridSize
else -> when (style) {
0 -> maxGridSize
1 -> 2
2 -> 1
else -> maxGridSize
}
}
}
}
binding.animeSourceRecycler.layoutManager = gridLayoutManager
model.scrolledToTop.observe(viewLifecycleOwner) {
if (it) binding.animeSourceRecycler.scrollToPosition(0)
}
continueEp = model.continueMedia ?: false
model.getMedia().observe(viewLifecycleOwner) {
if (it != null) {
media = it
media.selected = model.loadSelected(media)
subscribed = SubscriptionHelper.getSubscriptions(requireContext()).containsKey(media.id)
style = media.selected!!.recyclerStyle
reverse = media.selected!!.recyclerReversed
progress = View.GONE
binding.mediaInfoProgressBar.visibility = progress
if (!loaded) {
model.watchSources = if (media.isAdult) HAnimeSources else AnimeSources
headerAdapter = AnimeWatchAdapter(it, this, model.watchSources!!)
episodeAdapter = EpisodeAdapter(style ?: uiSettings.animeDefaultView, media, this)
binding.animeSourceRecycler.adapter = ConcatAdapter(headerAdapter, episodeAdapter)
lifecycleScope.launch(Dispatchers.IO) {
awaitAll(
async { model.loadKitsuEpisodes(media) },
async { model.loadFillerEpisodes(media) }
)
model.loadEpisodes(media, media.selected!!.sourceIndex)
}
loaded = true
} else {
reload()
}
}
}
model.getEpisodes().observe(viewLifecycleOwner) { loadedEpisodes ->
if (loadedEpisodes != null) {
val episodes = loadedEpisodes[media.selected!!.sourceIndex]
if (episodes != null) {
episodes.forEach { (i, episode) ->
if (media.anime?.fillerEpisodes != null) {
if (media.anime!!.fillerEpisodes!!.containsKey(i)) {
episode.title = episode.title ?: media.anime!!.fillerEpisodes!![i]?.title
episode.filler = media.anime!!.fillerEpisodes!![i]?.filler ?: false
}
}
if (media.anime?.kitsuEpisodes != null) {
if (media.anime!!.kitsuEpisodes!!.containsKey(i)) {
episode.desc = episode.desc ?: media.anime!!.kitsuEpisodes!![i]?.desc
episode.title = episode.title ?: media.anime!!.kitsuEpisodes!![i]?.title
episode.thumb = episode.thumb ?: media.anime!!.kitsuEpisodes!![i]?.thumb ?: FileUrl[media.cover]
}
}
}
media.anime?.episodes = episodes
//CHIP GROUP
val total = episodes.size
val divisions = total.toDouble() / 10
start = 0
end = null
val limit = when {
(divisions < 25) -> 25
(divisions < 50) -> 50
else -> 100
}
headerAdapter.clearChips()
if (total > limit) {
val arr = media.anime!!.episodes!!.keys.toTypedArray()
val stored = ceil((total).toDouble() / limit).toInt()
val position = MathUtils.clamp(media.selected!!.chip, 0, stored - 1)
val last = if (position + 1 == stored) total else (limit * (position + 1))
start = limit * (position)
end = last - 1
headerAdapter.updateChips(
limit,
arr,
(1..stored).toList().toTypedArray(),
position
)
}
headerAdapter.subscribeButton(true)
reload()
}
}
}
model.getKitsuEpisodes().observe(viewLifecycleOwner) { i ->
if (i != null)
media.anime?.kitsuEpisodes = i
}
model.getFillerEpisodes().observe(viewLifecycleOwner) { i ->
if (i != null)
media.anime?.fillerEpisodes = i
}
}
fun onSourceChange(i: Int): AnimeParser {
media.anime?.episodes = null
reload()
val selected = model.loadSelected(media)
model.watchSources?.get(selected.sourceIndex)?.showUserTextListener = null
selected.sourceIndex = i
selected.server = null
model.saveSelected(media.id, selected, requireActivity())
media.selected = selected
return model.watchSources?.get(i)!!
}
fun onLangChange(i: Int) {
val selected = model.loadSelected(media)
selected.langIndex = i
model.saveSelected(media.id, selected, requireActivity())
media.selected = selected
}
fun onDubClicked(checked: Boolean) {
val selected = model.loadSelected(media)
model.watchSources?.get(selected.sourceIndex)?.selectDub = checked
selected.preferDub = checked
model.saveSelected(media.id, selected, requireActivity())
media.selected = selected
lifecycleScope.launch(Dispatchers.IO) { model.forceLoadEpisode(media, selected.sourceIndex) }
}
fun loadEpisodes(i: Int, invalidate: Boolean) {
lifecycleScope.launch(Dispatchers.IO) { model.loadEpisodes(media, i, invalidate) }
}
fun onIconPressed(viewType: Int, rev: Boolean) {
style = viewType
reverse = rev
media.selected!!.recyclerStyle = style
media.selected!!.recyclerReversed = reverse
model.saveSelected(media.id, media.selected!!, requireActivity())
reload()
}
fun onChipClicked(i: Int, s: Int, e: Int) {
media.selected!!.chip = i
start = s
end = e
model.saveSelected(media.id, media.selected!!, requireActivity())
reload()
}
var subscribed = false
fun onNotificationPressed(subscribed: Boolean, source: String) {
this.subscribed = subscribed
saveSubscription(requireContext(), media, subscribed)
if (!subscribed)
Notifications.deleteChannel(requireContext(), getChannelId(true, media.id))
else
Notifications.createChannel(
requireContext(),
ANIME_GROUP,
getChannelId(true, media.id),
media.userPreferredName
)
snackString(
if (subscribed) getString(R.string.subscribed_notification, source)
else getString(R.string.unsubscribed_notification)
)
}
fun openSettings(pkg: AnimeExtension.Installed){
val changeUIVisibility: (Boolean) -> Unit = { show ->
val activity = requireActivity() as MediaDetailsActivity
val visibility = if (show) View.VISIBLE else View.GONE
activity.findViewById<AppBarLayout>(R.id.mediaAppBar).visibility = visibility
activity.findViewById<ViewPager2>(R.id.mediaViewPager).visibility = visibility
activity.findViewById<CardView>(R.id.mediaCover).visibility = visibility
activity.findViewById<CardView>(R.id.mediaClose).visibility = visibility
try{
activity.findViewById<CustomBottomNavBar>(R.id.mediaTab).visibility = visibility
}catch (e: ClassCastException){
activity.findViewById<NavigationRailView>(R.id.mediaTab).visibility = visibility
}
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
if (show) View.GONE else View.VISIBLE
}
val allSettings = pkg.sources.filterIsInstance<ConfigurableAnimeSource>()
if (allSettings.isNotEmpty()) {
var selectedSetting = allSettings[0]
if (allSettings.size > 1) {
val names = allSettings.map { it.lang }.toTypedArray()
var selectedIndex = 0
AlertDialog.Builder(requireContext())
.setTitle("Select a Source")
.setSingleChoiceItems(names, selectedIndex) { _, which ->
selectedIndex = which
}
.setPositiveButton("OK") { dialog, _ ->
selectedSetting = allSettings[selectedIndex]
dialog.dismiss()
// Move the fragment transaction here
val fragment =
AnimeSourcePreferencesFragment().getInstance(selectedSetting.id){
changeUIVisibility(true)
loadEpisodes(media.selected!!.sourceIndex, true)
}
parentFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
.replace(R.id.fragmentExtensionsContainer, fragment)
.addToBackStack(null)
.commit()
}
.setNegativeButton("Cancel") { dialog, _ ->
dialog.cancel()
changeUIVisibility(true)
return@setNegativeButton
}
.show()
} else {
// If there's only one setting, proceed with the fragment transaction
val fragment = AnimeSourcePreferencesFragment().getInstance(selectedSetting.id){
changeUIVisibility(true)
loadEpisodes(media.selected!!.sourceIndex, true)
}
parentFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
.replace(R.id.fragmentExtensionsContainer, fragment)
.addToBackStack(null)
.commit()
}
changeUIVisibility(false)
} else {
Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT)
.show()
}
}
fun onEpisodeClick(i: String) {
model.continueMedia = false
model.saveSelected(media.id, media.selected!!, requireActivity())
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager)
}
@SuppressLint("NotifyDataSetChanged")
private fun reload() {
val selected = model.loadSelected(media)
//Find latest episode for subscription
selected.latest =
media.anime?.episodes?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f
selected.latest =
media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: selected.latest
model.saveSelected(media.id, selected, requireActivity())
headerAdapter.handleEpisodes()
episodeAdapter.notifyItemRangeRemoved(0, episodeAdapter.arr.size)
var arr: ArrayList<Episode> = arrayListOf()
if (media.anime!!.episodes != null) {
val end = if (end != null && end!! < media.anime!!.episodes!!.size) end else null
arr.addAll(
media.anime!!.episodes!!.values.toList()
.slice(start..(end ?: (media.anime!!.episodes!!.size - 1)))
)
if (reverse)
arr = (arr.reversed() as? ArrayList<Episode>) ?: arr
}
episodeAdapter.arr = arr
episodeAdapter.updateType(style ?: uiSettings.animeDefaultView)
episodeAdapter.notifyItemRangeInserted(0, arr.size)
}
override fun onDestroy() {
model.watchSources?.flushText()
super.onDestroy()
}
var state: Parcelable? = null
override fun onResume() {
super.onResume()
binding.mediaInfoProgressBar.visibility = progress
binding.animeSourceRecycler.layoutManager?.onRestoreInstanceState(state)
}
override fun onPause() {
super.onPause()
state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState()
}
}