Initial commit

This commit is contained in:
Finnley Somdahl
2023-10-17 18:42:43 -05:00
commit 21bfbfb139
520 changed files with 47819 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
package ani.dantotsu.media.anime
import ani.dantotsu.media.Author
import ani.dantotsu.media.Studio
import java.io.Serializable
data class Anime(
var totalEpisodes: Int? = null,
var episodeDuration: Int? = null,
var season: String? = null,
var seasonYear: Int? = null,
var op: ArrayList<String> = arrayListOf(),
var ed: ArrayList<String> = arrayListOf(),
var mainStudio: Studio? = null,
var author: Author?=null,
var youtube: String? = null,
var nextAiringEpisode: Int? = null,
var nextAiringEpisodeTime: Long? = null,
var selectedEpisode: String? = null,
var episodes: MutableMap<String, Episode>? = null,
var slug: String? = null,
var kitsuEpisodes: Map<String, Episode>? = null,
var fillerEpisodes: Map<String, Episode>? = null,
) : Serializable

View File

@@ -0,0 +1,21 @@
package ani.dantotsu.media.anime
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.media.SourceAdapter
import ani.dantotsu.media.SourceSearchDialogFragment
import ani.dantotsu.parsers.ShowResponse
import kotlinx.coroutines.CoroutineScope
class AnimeSourceAdapter(
sources: List<ShowResponse>,
val model: MediaDetailsViewModel,
val i: Int,
val id: Int,
fragment: SourceSearchDialogFragment,
scope: CoroutineScope
) : SourceAdapter(sources, fragment, scope) {
override suspend fun onItemClick(source: ShowResponse) {
model.overrideEpisodes(i, source, id)
}
}

View File

@@ -0,0 +1,271 @@
package ani.dantotsu.media.anime
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.LinearLayout
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.*
import ani.dantotsu.databinding.ItemAnimeWatchBinding
import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.SourceSearchDialogFragment
import ani.dantotsu.parsers.WatchSources
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
import com.google.android.material.chip.Chip
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
class AnimeWatchAdapter(
private val media: Media,
private val fragment: AnimeWatchFragment,
private val watchSources: WatchSources
) : RecyclerView.Adapter<AnimeWatchAdapter.ViewHolder>() {
var subscribe: MediaDetailsActivity.PopImageButton? = null
private var _binding: ItemAnimeWatchBinding? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val bind = ItemAnimeWatchBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(bind)
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val binding = holder.binding
_binding = binding
//Youtube
if (media.anime!!.youtube != null && fragment.uiSettings.showYtButton) {
binding.animeSourceYT.visibility = View.VISIBLE
binding.animeSourceYT.setOnClickListener {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(media.anime.youtube))
fragment.requireContext().startActivity(intent)
}
}
binding.animeSourceDubbed.isChecked = media.selected!!.preferDub
binding.animeSourceDubbedText.text = if (media.selected!!.preferDub) currActivity()!!.getString(R.string.dubbed) else currActivity()!!.getString(R.string.subbed)
//PreferDub
var changing = false
binding.animeSourceDubbed.setOnCheckedChangeListener { _, isChecked ->
binding.animeSourceDubbedText.text = if (isChecked) currActivity()!!.getString(R.string.dubbed) else currActivity()!!.getString(R.string.subbed)
if (!changing) fragment.onDubClicked(isChecked)
}
//Wrong Title
binding.animeSourceSearch.setOnClickListener {
SourceSearchDialogFragment().show(fragment.requireActivity().supportFragmentManager, null)
}
//Source Selection
val source = media.selected!!.source.let { if (it >= watchSources.names.size) 0 else it }
if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) {
binding.animeSource.setText(watchSources.names[source])
watchSources[source].apply {
this.selectDub = media.selected!!.preferDub
binding.animeSourceTitle.text = showUserText
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
binding.animeSourceDubbedCont.visibility =
if (isDubAvailableSeparately) View.VISIBLE else View.GONE
}
}
binding.animeSource.setAdapter(ArrayAdapter(fragment.requireContext(), R.layout.item_dropdown, watchSources.names))
binding.animeSourceTitle.isSelected = true
binding.animeSource.setOnItemClickListener { _, _, i, _ ->
fragment.onSourceChange(i).apply {
binding.animeSourceTitle.text = showUserText
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
changing = true
binding.animeSourceDubbed.isChecked = selectDub
changing = false
binding.animeSourceDubbedCont.visibility = if (isDubAvailableSeparately) View.VISIBLE else View.GONE
}
subscribeButton(false)
fragment.loadEpisodes(i)
}
//Subscription
subscribe = MediaDetailsActivity.PopImageButton(
fragment.lifecycleScope,
binding.animeSourceSubscribe,
R.drawable.ic_round_notifications_active_24,
R.drawable.ic_round_notifications_none_24,
R.color.bg_opp,
R.color.violet_400,
fragment.subscribed
) {
fragment.onNotificationPressed(it, binding.animeSource.text.toString())
}
subscribeButton(false)
binding.animeSourceSubscribe.setOnLongClickListener {
openSettings(fragment.requireContext(),getChannelId(true,media.id))
}
//Icons
var reversed = media.selected!!.recyclerReversed
var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.animeDefaultView
binding.animeSourceTop.rotation = if (reversed) -90f else 90f
binding.animeSourceTop.setOnClickListener {
reversed = !reversed
binding.animeSourceTop.rotation = if (reversed) -90f else 90f
fragment.onIconPressed(style, reversed)
}
var selected = when (style) {
0 -> binding.animeSourceList
1 -> binding.animeSourceGrid
2 -> binding.animeSourceCompact
else -> binding.animeSourceList
}
selected.alpha = 1f
fun selected(it: ImageView) {
selected.alpha = 0.33f
selected = it
selected.alpha = 1f
}
binding.animeSourceList.setOnClickListener {
selected(it as ImageView)
style = 0
fragment.onIconPressed(style, reversed)
}
binding.animeSourceGrid.setOnClickListener {
selected(it as ImageView)
style = 1
fragment.onIconPressed(style, reversed)
}
binding.animeSourceCompact.setOnClickListener {
selected(it as ImageView)
style = 2
fragment.onIconPressed(style, reversed)
}
//Episode Handling
handleEpisodes()
}
fun subscribeButton(enabled : Boolean) {
subscribe?.enabled(enabled)
}
//Chips
@SuppressLint("SetTextI18n")
fun updateChips(limit: Int, names: Array<String>, arr: Array<Int>, selected: Int = 0) {
val binding = _binding
if (binding != null) {
val screenWidth = fragment.screenWidth.px
var select: Chip? = null
for (position in arr.indices) {
val last = if (position + 1 == arr.size) names.size else (limit * (position + 1))
val chip =
ItemChipBinding.inflate(LayoutInflater.from(fragment.context), binding.animeSourceChipGroup, false).root
chip.isCheckable = true
fun selected() {
chip.isChecked = true
binding.animeWatchChipScroll.smoothScrollTo((chip.left - screenWidth / 2) + (chip.width / 2), 0)
}
chip.text = "${names[limit * (position)]} - ${names[last - 1]}"
chip.setOnClickListener {
selected()
fragment.onChipClicked(position, limit * (position), last - 1)
}
binding.animeSourceChipGroup.addView(chip)
if (selected == position) {
selected()
select = chip
}
}
if (select != null)
binding.animeWatchChipScroll.apply { post { scrollTo((select.left - screenWidth / 2) + (select.width / 2), 0) } }
}
}
fun clearChips() {
_binding?.animeSourceChipGroup?.removeAllViews()
}
@SuppressLint("SetTextI18n")
fun handleEpisodes() {
val binding = _binding
if (binding != null) {
if (media.anime?.episodes != null) {
val episodes = media.anime.episodes!!.keys.toTypedArray()
val anilistEp = (media.userProgress ?: 0).plus(1)
val appEp = loadData<String>("${media.id}_current_ep")?.toIntOrNull() ?: 1
var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString()
if (episodes.contains(continueEp)) {
binding.animeSourceContinue.visibility = View.VISIBLE
handleProgress(
binding.itemEpisodeProgressCont,
binding.itemEpisodeProgress,
binding.itemEpisodeProgressEmpty,
media.id,
continueEp
)
if ((binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams).weight > fragment.playerSettings.watchPercentage) {
val e = episodes.indexOf(continueEp)
if (e != -1 && e + 1 < episodes.size) {
continueEp = episodes[e + 1]
handleProgress(
binding.itemEpisodeProgressCont,
binding.itemEpisodeProgress,
binding.itemEpisodeProgressEmpty,
media.id,
continueEp
)
}
}
val ep = media.anime.episodes!![continueEp]!!
binding.itemEpisodeImage.loadImage(ep.thumb ?: FileUrl[media.banner ?: media.cover], 0)
if (ep.filler) binding.itemEpisodeFillerView.visibility = View.VISIBLE
binding.animeSourceContinueText.text =
currActivity()!!.getString(R.string.continue_episode) + "${ep.number}${if (ep.filler) " - Filler" else ""}${if (ep.title != null) "\n${ep.title}" else ""}"
binding.animeSourceContinue.setOnClickListener {
fragment.onEpisodeClick(continueEp)
}
if (fragment.continueEp) {
if ((binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams).weight < fragment.playerSettings.watchPercentage) {
binding.animeSourceContinue.performClick()
fragment.continueEp = false
}
}
} else {
binding.animeSourceContinue.visibility = View.GONE
}
binding.animeSourceProgressBar.visibility = View.GONE
if (media.anime.episodes!!.isNotEmpty())
binding.animeSourceNotFound.visibility = View.GONE
else
binding.animeSourceNotFound.visibility = View.VISIBLE
} else {
binding.animeSourceContinue.visibility = View.GONE
binding.animeSourceNotFound.visibility = View.GONE
clearChips()
binding.animeSourceProgressBar.visibility = View.VISIBLE
}
}
}
override fun getItemCount(): Int = 1
inner class ViewHolder(val binding: ItemAnimeWatchBinding) : RecyclerView.ViewHolder(binding.root) {
init {
//Timer
countDown(media, binding.animeSourceContainer)
}
}
}

View File

@@ -0,0 +1,315 @@
package ani.dantotsu.media.anime
import android.annotation.SuppressLint
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
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 ani.dantotsu.*
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.parsers.AnimeParser
import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.HAnimeSources
import ani.dantotsu.settings.PlayerSettings
import ani.dantotsu.settings.UserInterfaceSettings
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch
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!!.source)
}
loaded = true
} else {
reload()
}
}
}
model.getEpisodes().observe(viewLifecycleOwner) { loadedEpisodes ->
if (loadedEpisodes != null) {
val episodes = loadedEpisodes[media.selected!!.source]
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.source)?.showUserTextListener = null
selected.source = i
selected.server = null
model.saveSelected(media.id, selected, requireActivity())
media.selected = selected
return model.watchSources?.get(i)!!
}
fun onDubClicked(checked: Boolean) {
val selected = model.loadSelected(media)
model.watchSources?.get(selected.source)?.selectDub = checked
selected.preferDub = checked
model.saveSelected(media.id, selected, requireActivity())
media.selected = selected
lifecycleScope.launch(Dispatchers.IO) { model.forceLoadEpisode(media, selected.source) }
}
fun loadEpisodes(i: Int) {
lifecycleScope.launch(Dispatchers.IO) { model.loadEpisodes(media, i) }
}
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 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()
}
}

View File

@@ -0,0 +1,26 @@
package ani.dantotsu.media.anime
import ani.dantotsu.FileUrl
import ani.dantotsu.parsers.VideoExtractor
import java.io.Serializable
data class Episode(
val number: String,
var link: String? = null,
var title: String? = null,
var desc: String? = null,
var thumb: FileUrl? = null,
var filler: Boolean = false,
var selectedExtractor: String? = null,
var selectedVideo: Int = 0,
var selectedSubtitle: Int? = -1,
var extractors: MutableList<VideoExtractor>?=null,
@Transient var extractorCallback: ((VideoExtractor) -> Unit)?=null,
var allStreams: Boolean = false,
var watched: Long? = null,
var maxLength: Long? = null,
val extra: Map<String,String>?=null,
val sEpisode: eu.kanade.tachiyomi.animesource.model.SEpisode? = null
) : Serializable

View File

@@ -0,0 +1,221 @@
package ani.dantotsu.media.anime
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.*
import ani.dantotsu.connections.updateProgress
import ani.dantotsu.databinding.ItemEpisodeCompactBinding
import ani.dantotsu.databinding.ItemEpisodeGridBinding
import ani.dantotsu.databinding.ItemEpisodeListBinding
import ani.dantotsu.media.Media
import com.bumptech.glide.Glide
import com.bumptech.glide.load.model.GlideUrl
fun handleProgress(cont: LinearLayout, bar: View, empty: View, mediaId: Int, ep: String) {
val curr = loadData<Long>("${mediaId}_${ep}")
val max = loadData<Long>("${mediaId}_${ep}_max")
if (curr != null && max != null) {
cont.visibility = View.VISIBLE
val div = curr.toFloat() / max.toFloat()
val barParams = bar.layoutParams as LinearLayout.LayoutParams
barParams.weight = div
bar.layoutParams = barParams
val params = empty.layoutParams as LinearLayout.LayoutParams
params.weight = 1 - div
empty.layoutParams = params
} else {
cont.visibility = View.GONE
}
}
class EpisodeAdapter(
private var type: Int,
private val media: Media,
private val fragment: AnimeWatchFragment,
var arr: List<Episode> = arrayListOf()
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return (when (viewType) {
0 -> EpisodeListViewHolder(ItemEpisodeListBinding.inflate(LayoutInflater.from(parent.context), parent, false))
1 -> EpisodeGridViewHolder(ItemEpisodeGridBinding.inflate(LayoutInflater.from(parent.context), parent, false))
2 -> EpisodeCompactViewHolder(
ItemEpisodeCompactBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
else -> throw IllegalArgumentException()
})
}
override fun getItemViewType(position: Int): Int {
return type
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val ep = arr[position]
val title =
"${if (!ep.title.isNullOrEmpty() && ep.title != "null") "" else currContext()!!.getString(R.string.episode_singular)} ${if (!ep.title.isNullOrEmpty() && ep.title != "null") ep.title else ep.number}"
when (holder) {
is EpisodeListViewHolder -> {
val binding = holder.binding
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
val thumb = ep.thumb?.let { if(it.url.isNotEmpty()) GlideUrl(it.url) { it.headers } else null }
Glide.with(binding.itemEpisodeImage).load(thumb?:media.cover).override(400,0).into(binding.itemEpisodeImage)
binding.itemEpisodeNumber.text = ep.number
binding.itemEpisodeTitle.text = title
if (ep.filler) {
binding.itemEpisodeFiller.visibility = View.VISIBLE
binding.itemEpisodeFillerView.visibility = View.VISIBLE
} else {
binding.itemEpisodeFiller.visibility = View.GONE
binding.itemEpisodeFillerView.visibility = View.GONE
}
binding.itemEpisodeDesc.visibility = if (ep.desc != null && ep.desc?.trim(' ') != "") View.VISIBLE else View.GONE
binding.itemEpisodeDesc.text = ep.desc ?: ""
if (media.userProgress != null) {
if ((ep.number.toFloatOrNull() ?: 9999f) <= media.userProgress!!.toFloat()) {
binding.itemEpisodeViewedCover.visibility = View.VISIBLE
binding.itemEpisodeViewed.visibility = View.VISIBLE
} else {
binding.itemEpisodeViewedCover.visibility = View.GONE
binding.itemEpisodeViewed.visibility = View.GONE
binding.itemEpisodeCont.setOnLongClickListener {
updateProgress(media, ep.number)
true
}
}
} else {
binding.itemEpisodeViewedCover.visibility = View.GONE
binding.itemEpisodeViewed.visibility = View.GONE
}
handleProgress(
binding.itemEpisodeProgressCont,
binding.itemEpisodeProgress,
binding.itemEpisodeProgressEmpty,
media.id,
ep.number
)
}
is EpisodeGridViewHolder -> {
val binding = holder.binding
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
val thumb = ep.thumb?.let { if(it.url.isNotEmpty()) GlideUrl(it.url) { it.headers } else null }
Glide.with(binding.itemEpisodeImage).load(thumb?:media.cover).override(400,0).into(binding.itemEpisodeImage)
binding.itemEpisodeNumber.text = ep.number
binding.itemEpisodeTitle.text = title
if (ep.filler) {
binding.itemEpisodeFiller.visibility = View.VISIBLE
binding.itemEpisodeFillerView.visibility = View.VISIBLE
} else {
binding.itemEpisodeFiller.visibility = View.GONE
binding.itemEpisodeFillerView.visibility = View.GONE
}
if (media.userProgress != null) {
if ((ep.number.toFloatOrNull() ?: 9999f) <= media.userProgress!!.toFloat()) {
binding.itemEpisodeViewedCover.visibility = View.VISIBLE
binding.itemEpisodeViewed.visibility = View.VISIBLE
} else {
binding.itemEpisodeViewedCover.visibility = View.GONE
binding.itemEpisodeViewed.visibility = View.GONE
binding.itemEpisodeCont.setOnLongClickListener {
updateProgress(media, ep.number)
true
}
}
} else {
binding.itemEpisodeViewedCover.visibility = View.GONE
binding.itemEpisodeViewed.visibility = View.GONE
}
handleProgress(
binding.itemEpisodeProgressCont,
binding.itemEpisodeProgress,
binding.itemEpisodeProgressEmpty,
media.id,
ep.number
)
}
is EpisodeCompactViewHolder -> {
val binding = holder.binding
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
binding.itemEpisodeNumber.text = ep.number
binding.itemEpisodeFillerView.visibility = if (ep.filler) View.VISIBLE else View.GONE
if (media.userProgress != null) {
if ((ep.number.toFloatOrNull() ?: 9999f) <= media.userProgress!!.toFloat())
binding.itemEpisodeViewedCover.visibility = View.VISIBLE
else {
binding.itemEpisodeViewedCover.visibility = View.GONE
binding.itemEpisodeCont.setOnLongClickListener {
updateProgress(media, ep.number)
true
}
}
}
handleProgress(
binding.itemEpisodeProgressCont,
binding.itemEpisodeProgress,
binding.itemEpisodeProgressEmpty,
media.id,
ep.number
)
}
}
}
override fun getItemCount(): Int = arr.size
inner class EpisodeCompactViewHolder(val binding: ItemEpisodeCompactBinding) : RecyclerView.ViewHolder(binding.root) {
init {
itemView.setOnClickListener {
if (bindingAdapterPosition < arr.size && bindingAdapterPosition >= 0)
fragment.onEpisodeClick(arr[bindingAdapterPosition].number)
}
}
}
inner class EpisodeGridViewHolder(val binding: ItemEpisodeGridBinding) : RecyclerView.ViewHolder(binding.root) {
init {
itemView.setOnClickListener {
if (bindingAdapterPosition < arr.size && bindingAdapterPosition >= 0)
fragment.onEpisodeClick(arr[bindingAdapterPosition].number)
}
}
}
inner class EpisodeListViewHolder(val binding: ItemEpisodeListBinding) : RecyclerView.ViewHolder(binding.root) {
init {
itemView.setOnClickListener {
if (bindingAdapterPosition < arr.size && bindingAdapterPosition >= 0)
fragment.onEpisodeClick(arr[bindingAdapterPosition].number)
}
binding.itemEpisodeDesc.setOnClickListener {
if (binding.itemEpisodeDesc.maxLines == 3)
binding.itemEpisodeDesc.maxLines = 100
else
binding.itemEpisodeDesc.maxLines = 3
}
}
}
fun updateType(t: Int) {
type = t
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,307 @@
package ani.dantotsu.media.anime
import android.annotation.SuppressLint
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.HapticFeedbackConstants
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.*
import ani.dantotsu.databinding.BottomSheetSelectorBinding
import ani.dantotsu.databinding.ItemStreamBinding
import ani.dantotsu.databinding.ItemUrlBinding
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.others.Download.download
import ani.dantotsu.parsers.VideoExtractor
import ani.dantotsu.parsers.VideoType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.text.DecimalFormat
class SelectorDialogFragment : BottomSheetDialogFragment() {
private var _binding: BottomSheetSelectorBinding? = null
private val binding get() = _binding!!
val model: MediaDetailsViewModel by activityViewModels()
private var scope: CoroutineScope = lifecycleScope
private var media: Media? = null
private var episode: Episode? = null
private var prevEpisode: String? = null
private var makeDefault = false
private var selected: String? = null
private var launch: Boolean? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
selected = it.getString("server")
launch = it.getBoolean("launch", true)
prevEpisode = it.getString("prev")
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = BottomSheetSelectorBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
var loaded = false
model.getMedia().observe(viewLifecycleOwner) { m ->
media = m
if (media != null && !loaded) {
loaded = true
val ep = media?.anime?.episodes?.get(media?.anime?.selectedEpisode)
episode = ep
if (ep != null) {
if (selected != null) {
binding.selectorListContainer.visibility = View.GONE
binding.selectorAutoListContainer.visibility = View.VISIBLE
binding.selectorAutoText.text = selected
binding.selectorCancel.setOnClickListener {
media!!.selected!!.server = null
model.saveSelected(media!!.id, media!!.selected!!, requireActivity())
tryWith {
dismiss()
}
}
fun fail() {
snackString(getString(R.string.auto_select_server_error))
binding.selectorCancel.performClick()
}
fun load() {
val size = ep.extractors?.find { it.server.name == selected }?.videos?.size
if (size!=null && size >= media!!.selected!!.video) {
media!!.anime!!.episodes?.get(media!!.anime!!.selectedEpisode!!)?.selectedExtractor = selected
media!!.anime!!.episodes?.get(media!!.anime!!.selectedEpisode!!)?.selectedVideo = media!!.selected!!.video
startExoplayer(media!!)
} else fail()
}
if (ep.extractors.isNullOrEmpty()) {
model.getEpisode().observe(this) {
if (it != null) {
episode = it
load()
}
}
scope.launch {
if (withContext(Dispatchers.IO) {
!model.loadEpisodeSingleVideo(
ep,
media!!.selected!!
)
}) fail()
}
} else load()
}
else {
binding.selectorRecyclerView.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
}
binding.selectorRecyclerView.adapter = null
binding.selectorProgressBar.visibility = View.VISIBLE
makeDefault = loadData("make_default") ?: true
binding.selectorMakeDefault.isChecked = makeDefault
binding.selectorMakeDefault.setOnClickListener {
makeDefault = binding.selectorMakeDefault.isChecked
saveData("make_default", makeDefault)
}
binding.selectorRecyclerView.layoutManager =
LinearLayoutManager(requireActivity(), LinearLayoutManager.VERTICAL, false)
val adapter = ExtractorAdapter()
binding.selectorRecyclerView.adapter = adapter
if (!ep.allStreams ) {
ep.extractorCallback = {
scope.launch {
adapter.add(it)
}
}
model.getEpisode().observe(this) {
if (it != null) {
media!!.anime?.episodes?.set(media!!.anime?.selectedEpisode!!, ep)
}
}
scope.launch(Dispatchers.IO) {
model.loadEpisodeVideos(ep, media!!.selected!!.source)
withContext(Dispatchers.Main){
binding.selectorProgressBar.visibility = View.GONE
}
}
} else {
media!!.anime?.episodes?.set(media!!.anime?.selectedEpisode!!, ep)
adapter.addAll(ep.extractors)
binding.selectorProgressBar.visibility = View.GONE
}
}
}
}
}
super.onViewCreated(view, savedInstanceState)
}
@SuppressLint("UnsafeOptInUsageError")
fun startExoplayer(media: Media) {
prevEpisode = null
dismiss()
if (launch!!) {
stopAddingToList()
val intent = Intent(activity, ExoplayerView::class.java)
ExoplayerView.media = media
ExoplayerView.initialized = true
startActivity(intent)
} else {
model.setEpisode(media.anime!!.episodes!![media.anime.selectedEpisode!!]!!, "startExo no launch")
}
}
private fun stopAddingToList() {
episode?.extractorCallback = null
episode?.also {
it.extractors = it.extractors?.toMutableList()
}
}
private inner class ExtractorAdapter : RecyclerView.Adapter<ExtractorAdapter.StreamViewHolder>() {
val links = mutableListOf<VideoExtractor>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StreamViewHolder =
StreamViewHolder(ItemStreamBinding.inflate(LayoutInflater.from(parent.context), parent, false))
override fun onBindViewHolder(holder: StreamViewHolder, position: Int) {
val extractor = links[position]
holder.binding.streamName.text = extractor.server.name
holder.binding.streamRecyclerView.layoutManager = LinearLayoutManager(requireContext())
holder.binding.streamRecyclerView.adapter = VideoAdapter(extractor)
}
override fun getItemCount(): Int = links.size
fun add(videoExtractor: VideoExtractor){
if(videoExtractor.videos.isNotEmpty()) {
links.add(videoExtractor)
notifyItemInserted(links.size - 1)
}
}
fun addAll(extractors: List<VideoExtractor>?) {
links.addAll(extractors?:return)
notifyItemRangeInserted(0,extractors.size)
}
private inner class StreamViewHolder(val binding: ItemStreamBinding) : RecyclerView.ViewHolder(binding.root)
}
private inner class VideoAdapter(private val extractor : VideoExtractor) : RecyclerView.Adapter<VideoAdapter.UrlViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UrlViewHolder {
return UrlViewHolder(ItemUrlBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: UrlViewHolder, position: Int) {
val binding = holder.binding
val video = extractor.videos[position]
binding.urlQuality.text = if(video.quality!=null) "${video.quality}p" else "Default Quality"
binding.urlNote.text = video.extraNote ?: ""
binding.urlNote.visibility = if (video.extraNote != null) View.VISIBLE else View.GONE
binding.urlDownload.visibility = View.VISIBLE
binding.urlDownload.setSafeOnClickListener {
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedExtractor = extractor.server.name
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedVideo = position
binding.urlDownload.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
download(
requireActivity(),
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!,
media!!.userPreferredName
)
dismiss()
}
if (video.format == VideoType.CONTAINER) {
binding.urlSize.visibility = if (video.size != null) View.VISIBLE else View.GONE
binding.urlSize.text =
(if (video.extraNote != null) " : " else "") + DecimalFormat("#.##").format(video.size ?: 0).toString() + " MB"
}
else {
binding.urlQuality.text = "Multi Quality"
if ((loadData<Int>("settings_download_manager") ?: 0) == 0) {
binding.urlDownload.visibility = View.GONE
}
}
}
override fun getItemCount(): Int = extractor.videos.size
private inner class UrlViewHolder(val binding: ItemUrlBinding) : RecyclerView.ViewHolder(binding.root) {
init {
itemView.setSafeOnClickListener {
tryWith(true) {
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedExtractor = extractor.server.name
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedVideo = bindingAdapterPosition
if (makeDefault) {
media!!.selected!!.server = extractor.server.name
media!!.selected!!.video = bindingAdapterPosition
model.saveSelected(media!!.id, media!!.selected!!, requireActivity())
}
startExoplayer(media!!)
}
}
itemView.setOnLongClickListener {
val video = extractor.videos[bindingAdapterPosition]
val intent= Intent(Intent.ACTION_VIEW).apply {
setDataAndType(Uri.parse(video.file.url),"video/*")
}
copyToClipboard(video.file.url,true)
dismiss()
startActivity(Intent.createChooser(intent,"Open Video in :"))
true
}
}
}
}
companion object {
fun newInstance(server: String? = null, la: Boolean = true, prev: String? = null): SelectorDialogFragment =
SelectorDialogFragment().apply {
arguments = Bundle().apply {
putString("server", server)
putBoolean("launch", la)
putString("prev", prev)
}
}
}
override fun onSaveInstanceState(outState: Bundle) {}
override fun onDismiss(dialog: DialogInterface) {
if (launch == false) {
activity?.hideSystemBars()
model.epChanged.postValue(true)
if (prevEpisode != null) {
media?.anime?.selectedEpisode = prevEpisode
model.setEpisode(media?.anime?.episodes?.get(prevEpisode) ?: return, "prevEp")
}
}
super.onDismiss(dialog)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@@ -0,0 +1,119 @@
package ani.dantotsu.media.anime
import android.app.Activity
import android.graphics.Color.TRANSPARENT
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.R
import ani.dantotsu.databinding.BottomSheetSubtitlesBinding
import ani.dantotsu.databinding.ItemSubtitleTextBinding
import ani.dantotsu.loadData
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.parsers.Subtitle
import ani.dantotsu.saveData
class SubtitleDialogFragment : BottomSheetDialogFragment() {
private var _binding: BottomSheetSubtitlesBinding? = null
private val binding get() = _binding!!
val model: MediaDetailsViewModel by activityViewModels()
private lateinit var episode: Episode
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = BottomSheetSubtitlesBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
model.getMedia().observe(viewLifecycleOwner) { media ->
episode = media?.anime?.episodes?.get(media.anime.selectedEpisode) ?: return@observe
val currentExtractor = episode.extractors?.find { it.server.name == episode.selectedExtractor } ?: return@observe
binding.subtitlesRecycler.layoutManager = LinearLayoutManager(requireContext())
binding.subtitlesRecycler.adapter = SubtitleAdapter(currentExtractor.subtitles)
}
}
inner class SubtitleAdapter(val subtitles: List<Subtitle>) : RecyclerView.Adapter<SubtitleAdapter.StreamViewHolder>() {
inner class StreamViewHolder(val binding: ItemSubtitleTextBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StreamViewHolder =
StreamViewHolder(ItemSubtitleTextBinding.inflate(LayoutInflater.from(parent.context), parent, false))
override fun onBindViewHolder(holder: StreamViewHolder, position: Int) {
val binding = holder.binding
if (position == 0) {
binding.subtitleTitle.setText(R.string.none)
model.getMedia().observe(viewLifecycleOwner) { media ->
val mediaID: Int = media.id
val selSubs: String? = loadData("subLang_${mediaID}", activity)
if (episode.selectedSubtitle != null && selSubs != "None") {
binding.root.setCardBackgroundColor(TRANSPARENT)
}
}
binding.root.setOnClickListener {
episode.selectedSubtitle = null
model.setEpisode(episode, "Subtitle")
model.getMedia().observe(viewLifecycleOwner){media ->
val mediaID: Int = media.id
saveData("subLang_${mediaID}", "None", activity)
}
dismiss()
}
} else {
binding.subtitleTitle.text = when (subtitles[position - 1].language) {
"ja-JP" -> "[ja-JP] Japanese"
"en-US" -> "[en-US] English"
"de-DE" -> "[de-DE] German"
"es-ES" -> "[es-ES] Spanish"
"es-419" -> "[es-419] Spanish"
"fr-FR" -> "[fr-FR] French"
"it-IT" -> "[it-IT] Italian"
"pt-BR" -> "[pt-BR] Portuguese (Brazil)"
"pt-PT" -> "[pt-PT] Portuguese (Portugal)"
"ru-RU" -> "[ru-RU] Russian"
"zh-CN" -> "[zh-CN] Chinese (Simplified)"
"tr-TR" -> "[tr-TR] Turkish"
"ar-ME" -> "[ar-ME] Arabic"
"ar-SA" -> "[ar-SA] Arabic (Saudi Arabia)"
"uk-UK" -> "[uk-UK] Ukrainian"
"he-IL" -> "[he-IL] Hebrew"
"pl-PL" -> "[pl-PL] Polish"
"ro-RO" -> "[ro-RO] Romanian"
"sv-SE" -> "[sv-SE] Swedish"
else -> if(subtitles[position - 1].language matches Regex("([a-z]{2})-([A-Z]{2}|\\d{3})")) "[${subtitles[position - 1].language}]" else subtitles[position - 1].language
}
model.getMedia().observe(viewLifecycleOwner) { media ->
val mediaID: Int = media.id
val selSubs: String? = loadData("subLang_${mediaID}", activity)
if (episode.selectedSubtitle != position - 1 && selSubs != subtitles[position - 1].language) {
binding.root.setCardBackgroundColor(TRANSPARENT)
}
}
val activity: Activity = requireActivity() as ExoplayerView
binding.root.setOnClickListener {
episode.selectedSubtitle = position - 1
model.setEpisode(episode, "Subtitle")
model.getMedia().observe(viewLifecycleOwner){media ->
val mediaID: Int = media.id
saveData("subLang_${mediaID}", subtitles[position - 1].language, activity)
}
dismiss()
}
}
}
override fun getItemCount(): Int = subtitles.size + 1
}
override fun onDestroy() {
_binding = null
super.onDestroy()
}
}

View File

@@ -0,0 +1,28 @@
package ani.dantotsu.media.anime
import android.content.Context
import androidx.media3.common.util.UnstableApi
import androidx.media3.database.StandaloneDatabaseProvider
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
import androidx.media3.datasource.cache.SimpleCache
import java.io.File
@UnstableApi
object VideoCache {
private var simpleCache: SimpleCache? = null
fun getInstance(context: Context): SimpleCache {
val databaseProvider = StandaloneDatabaseProvider(context)
if (simpleCache == null)
simpleCache = SimpleCache(
File(context.cacheDir, "exoplayer").also { it.deleteOnExit() }, // Ensures always fresh file
LeastRecentlyUsedCacheEvictor(300L * 1024L * 1024L),
databaseProvider
)
return simpleCache as SimpleCache
}
fun release() {
simpleCache?.release()
simpleCache = null
}
}