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,9 @@
package ani.dantotsu.media
import java.io.Serializable
data class Author(
val id: String,
val name: String,
var yearMedia: MutableMap<String, ArrayList<Media>>? = null
) : Serializable

View file

@ -0,0 +1,111 @@
package ani.dantotsu.media
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager
import ani.dantotsu.*
import ani.dantotsu.databinding.ActivityAuthorBinding
import ani.dantotsu.others.getSerialized
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class AuthorActivity : AppCompatActivity() {
private lateinit var binding: ActivityAuthorBinding
private val scope = lifecycleScope
private val model: OtherDetailsViewModel by viewModels()
private var author: Author? = null
private var loaded = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityAuthorBinding.inflate(layoutInflater)
setContentView(binding.root)
initActivity(this)
this.window.statusBarColor = ContextCompat.getColor(this, R.color.nav_bg)
val screenWidth = resources.displayMetrics.run { widthPixels / density }
binding.root.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
binding.studioRecycler.updatePadding(bottom = 64f.px + navBarHeight)
binding.studioTitle.isSelected = true
author = intent.getSerialized("author")
binding.studioTitle.text = author?.name
binding.studioClose.setOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
model.getAuthor().observe(this) {
if (it != null) {
author = it
loaded = true
binding.studioProgressBar.visibility = View.GONE
binding.studioRecycler.visibility = View.VISIBLE
val titlePosition = arrayListOf<Int>()
val concatAdapter = ConcatAdapter()
val map = author!!.yearMedia ?: return@observe
val keys = map.keys.toTypedArray()
var pos = 0
val gridSize = (screenWidth / 124f).toInt()
val gridLayoutManager = GridLayoutManager(this, gridSize)
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return when (position in titlePosition) {
true -> gridSize
else -> 1
}
}
}
for (i in keys.indices) {
val medias = map[keys[i]]!!
val empty = if (medias.size >= 4) medias.size % 4 else 4 - medias.size
titlePosition.add(pos)
pos += (empty + medias.size + 1)
concatAdapter.addAdapter(TitleAdapter("${keys[i]} (${medias.size})"))
concatAdapter.addAdapter(MediaAdaptor(0, medias, this, true))
concatAdapter.addAdapter(EmptyAdapter(empty))
}
binding.studioRecycler.adapter = concatAdapter
binding.studioRecycler.layoutManager = gridLayoutManager
}
}
val live = Refresh.activity.getOrPut(this.hashCode()) { MutableLiveData(true) }
live.observe(this) {
if (it) {
scope.launch {
if (author != null)
withContext(Dispatchers.IO) { model.loadAuthor(author!!) }
live.postValue(false)
}
}
}
}
override fun onDestroy() {
if (Refresh.activity.containsKey(this.hashCode())) {
Refresh.activity.remove(this.hashCode())
}
super.onDestroy()
}
override fun onResume() {
binding.studioProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE
super.onResume()
}
}

View file

@ -0,0 +1,70 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.View
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.lifecycleScope
import ani.dantotsu.R
import ani.dantotsu.Refresh
import ani.dantotsu.databinding.ActivityListBinding
import ani.dantotsu.media.user.ListViewPagerAdapter
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class CalendarActivity : AppCompatActivity() {
private lateinit var binding: ActivityListBinding
private val scope = lifecycleScope
private var selectedTabIdx = 1
private val model: OtherDetailsViewModel by viewModels()
@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityListBinding.inflate(layoutInflater)
setContentView(binding.root)
window.statusBarColor = ContextCompat.getColor(this, R.color.nav_bg)
binding.listTitle.setText(R.string.release_calendar)
binding.listSort.visibility = View.GONE
binding.listTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
this@CalendarActivity.selectedTabIdx = tab?.position ?: 1
}
override fun onTabUnselected(tab: TabLayout.Tab?) { }
override fun onTabReselected(tab: TabLayout.Tab?) { }
})
model.getCalendar().observe(this) {
if (it != null) {
binding.listProgressBar.visibility = View.GONE
binding.listViewPager.adapter = ListViewPagerAdapter(it.size, true,this)
val keys = it.keys.toList()
val values = it.values.toList()
val savedTab = this.selectedTabIdx
TabLayoutMediator(binding.listTabLayout, binding.listViewPager) { tab, position ->
tab.text = "${keys[position]} (${values[position].size})"
}.attach()
binding.listViewPager.setCurrentItem(savedTab, false)
}
}
val live = Refresh.activity.getOrPut(this.hashCode()) { MutableLiveData(true) }
live.observe(this) {
if (it) {
scope.launch {
withContext(Dispatchers.IO) { model.loadCalendar() }
live.postValue(false)
}
}
}
}
}

View file

@ -0,0 +1,18 @@
package ani.dantotsu.media
import ani.dantotsu.connections.anilist.api.FuzzyDate
import java.io.Serializable
data class Character(
val id: Int,
val name: String?,
val image: String?,
val banner: String?,
val role: String,
var description: String? = null,
var age: String? = null,
var gender: String? = null,
var dateOfBirth: FuzzyDate? = null,
var roles: ArrayList<Media>? = null
) : Serializable

View file

@ -0,0 +1,56 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat
import androidx.core.util.Pair
import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.databinding.ItemCharacterBinding
import ani.dantotsu.loadData
import ani.dantotsu.loadImage
import ani.dantotsu.setAnimation
import ani.dantotsu.settings.UserInterfaceSettings
import java.io.Serializable
class CharacterAdapter(
private val characterList: ArrayList<Character>
) : RecyclerView.Adapter<CharacterAdapter.CharacterViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CharacterViewHolder {
val binding = ItemCharacterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return CharacterViewHolder(binding)
}
private val uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: CharacterViewHolder, position: Int) {
val binding = holder.binding
setAnimation(binding.root.context, holder.binding.root, uiSettings)
val character = characterList[position]
binding.itemCompactRelation.text = character.role + " "
binding.itemCompactImage.loadImage(character.image)
binding.itemCompactTitle.text = character.name
}
override fun getItemCount(): Int = characterList.size
inner class CharacterViewHolder(val binding: ItemCharacterBinding) : RecyclerView.ViewHolder(binding.root) {
init {
itemView.setOnClickListener {
val char = characterList[bindingAdapterPosition]
ContextCompat.startActivity(
itemView.context,
Intent(itemView.context, CharacterDetailsActivity::class.java).putExtra("character", char as Serializable),
ActivityOptionsCompat.makeSceneTransitionAnimation(
itemView.context as Activity,
Pair.create(binding.itemCompactImage, ViewCompat.getTransitionName(binding.itemCompactImage)!!),
).toBundle()
)
}
}
}
}

View file

@ -0,0 +1,130 @@
package ani.dantotsu.media
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.math.MathUtils.clamp
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager
import ani.dantotsu.*
import ani.dantotsu.databinding.ActivityCharacterBinding
import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.others.getSerialized
import ani.dantotsu.settings.UserInterfaceSettings
import com.google.android.material.appbar.AppBarLayout
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlin.math.abs
class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener {
private lateinit var binding: ActivityCharacterBinding
private val scope = lifecycleScope
private val model: OtherDetailsViewModel by viewModels()
private lateinit var character: Character
private var loaded = false
val uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityCharacterBinding.inflate(layoutInflater)
setContentView(binding.root)
initActivity(this)
screenWidth = resources.displayMetrics.run { widthPixels / density }
if (uiSettings.immersiveMode) this.window.statusBarColor = ContextCompat.getColor(this, R.color.status)
val banner = if (uiSettings.bannerAnimations) binding.characterBanner else binding.characterBannerNoKen
banner.updateLayoutParams { height += statusBarHeight }
binding.characterClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
binding.characterCollapsing.minimumHeight = statusBarHeight
binding.characterCover.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
binding.characterRecyclerView.updatePadding(bottom = 64f.px + navBarHeight)
binding.characterTitle.isSelected = true
binding.characterAppBar.addOnOffsetChangedListener(this)
binding.characterClose.setOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
character = intent.getSerialized("character") ?: return
binding.characterTitle.text = character.name
banner.loadImage(character.banner)
binding.characterCoverImage.loadImage(character.image)
binding.characterCoverImage.setOnLongClickListener { ImageViewDialog.newInstance(this, character.name, character.image) }
model.getCharacter().observe(this) {
if (it != null && !loaded) {
character = it
loaded = true
binding.characterProgress.visibility = View.GONE
binding.characterRecyclerView.visibility = View.VISIBLE
val roles = character.roles
if (roles != null) {
val mediaAdaptor = MediaAdaptor(0, roles, this, matchParent = true)
val concatAdaptor = ConcatAdapter(CharacterDetailsAdapter(character, this), mediaAdaptor)
val gridSize = (screenWidth / 124f).toInt()
val gridLayoutManager = GridLayoutManager(this, gridSize)
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return when (position) {
0 -> gridSize
else -> 1
}
}
}
binding.characterRecyclerView.adapter = concatAdaptor
binding.characterRecyclerView.layoutManager = gridLayoutManager
}
}
}
val live = Refresh.activity.getOrPut(this.hashCode()) { MutableLiveData(true) }
live.observe(this) {
scope.launch(Dispatchers.IO) {
model.loadCharacter(character)
}
}
}
override fun onResume() {
binding.characterProgress.visibility = if (!loaded) View.VISIBLE else View.GONE
super.onResume()
}
private var isCollapsed = false
private val percent = 30
private var mMaxScrollSize = 0
private var screenWidth: Float = 0f
override fun onOffsetChanged(appBar: AppBarLayout, i: Int) {
if (mMaxScrollSize == 0) mMaxScrollSize = appBar.totalScrollRange
val percentage = abs(i) * 100 / mMaxScrollSize
val cap = clamp((percent - percentage) / percent.toFloat(), 0f, 1f)
binding.characterCover.scaleX = 1f * cap
binding.characterCover.scaleY = 1f * cap
binding.characterCover.cardElevation = 32f * cap
binding.characterCover.visibility = if (binding.characterCover.scaleX == 0f) View.GONE else View.VISIBLE
if (percentage >= percent && !isCollapsed) {
isCollapsed = true
if (uiSettings.immersiveMode) this.window.statusBarColor = ContextCompat.getColor(this, R.color.nav_bg)
binding.characterAppBar.setBackgroundResource(R.color.nav_bg)
}
if (percentage <= percent && isCollapsed) {
isCollapsed = false
if (uiSettings.immersiveMode) this.window.statusBarColor = ContextCompat.getColor(this, R.color.status)
binding.characterAppBar.setBackgroundResource(R.color.bg)
}
}
}

View file

@ -0,0 +1,42 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
import android.app.Activity
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.currActivity
import ani.dantotsu.databinding.ItemCharacterDetailsBinding
import ani.dantotsu.others.SpoilerPlugin
import io.noties.markwon.Markwon
import io.noties.markwon.SoftBreakAddsNewLinePlugin
class CharacterDetailsAdapter(private val character: Character, private val activity: Activity) :
RecyclerView.Adapter<CharacterDetailsAdapter.GenreViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GenreViewHolder {
val binding = ItemCharacterDetailsBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return GenreViewHolder(binding)
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: GenreViewHolder, position: Int) {
val binding = holder.binding
val desc =
(if (character.age != "null") currActivity()!!.getString(R.string.age) + " " + character.age else "") +
(if (character.dateOfBirth.toString() != "") currActivity()!!.getString(R.string.birthday) + " " + character.dateOfBirth.toString() else "") +
(if (character.gender != "null") currActivity()!!.getString(R.string.gender) + " " + when(character.gender){
"Male" -> currActivity()!!.getString(R.string.male)
"Female" -> currActivity()!!.getString(R.string.female)
else -> character.gender
} else "") + "\n" + character.description
binding.characterDesc.isTextSelectable
val markWon = Markwon.builder(activity).usePlugin(SoftBreakAddsNewLinePlugin.create()).usePlugin(SpoilerPlugin()).build()
markWon.setMarkdown(binding.characterDesc, desc)
}
override fun getItemCount(): Int = 1
inner class GenreViewHolder(val binding: ItemCharacterDetailsBinding) : RecyclerView.ViewHolder(binding.root)
}

View file

@ -0,0 +1,60 @@
package ani.dantotsu.media
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.GenresViewModel
import ani.dantotsu.databinding.ActivityGenreBinding
import ani.dantotsu.initActivity
import ani.dantotsu.loadData
import ani.dantotsu.navBarHeight
import ani.dantotsu.statusBarHeight
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
class GenreActivity : AppCompatActivity() {
private lateinit var binding: ActivityGenreBinding
val model: GenresViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityGenreBinding.inflate(layoutInflater)
setContentView(binding.root)
initActivity(this)
binding.genreContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight;bottomMargin += navBarHeight }
val screenWidth = resources.displayMetrics.run { widthPixels / density }
val type = intent.getStringExtra("type")
if (type != null) {
val adapter = GenreAdapter(type, true)
model.doneListener = {
MainScope().launch {
binding.mediaInfoGenresProgressBar.visibility = View.GONE
}
}
if (model.genres != null) {
adapter.genres = model.genres!!
adapter.pos = ArrayList(model.genres!!.keys)
if (model.done)
model.doneListener?.invoke()
}
binding.mediaInfoGenresRecyclerView.adapter = adapter
binding.mediaInfoGenresRecyclerView.layoutManager = GridLayoutManager(this, (screenWidth / 156f).toInt())
lifecycleScope.launch(Dispatchers.IO) {
model.loadGenres(Anilist.genres ?: loadData("genres_list") ?: arrayListOf()) {
MainScope().launch {
adapter.addGenre(it)
}
}
}
}
}
}

View file

@ -0,0 +1,71 @@
package ani.dantotsu.media
import android.content.Intent
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.view.updateLayoutParams
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.currActivity
import ani.dantotsu.databinding.ItemGenreBinding
import ani.dantotsu.loadImage
import ani.dantotsu.px
class GenreAdapter(
private val type: String,
private val big: Boolean = false
) : RecyclerView.Adapter<GenreAdapter.GenreViewHolder>() {
var genres = mutableMapOf<String, String>()
var pos = arrayListOf<String>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GenreViewHolder {
val binding = ItemGenreBinding.inflate(LayoutInflater.from(parent.context), parent, false)
if (big) binding.genreCard.updateLayoutParams { height = 72f.px }
return GenreViewHolder(binding)
}
override fun onBindViewHolder(holder: GenreViewHolder, position: Int) {
val binding = holder.binding
if (pos.size > position) {
val genre = genres[pos[position]]
binding.genreTitle.text = pos[position]
binding.genreImage.loadImage(genre)
}
}
override fun getItemCount(): Int = genres.size
inner class GenreViewHolder(val binding: ItemGenreBinding) : RecyclerView.ViewHolder(binding.root) {
init {
itemView.setOnClickListener {
ContextCompat.startActivity(
itemView.context,
Intent(itemView.context, SearchActivity::class.java)
.putExtra("type", type)
.putExtra("genre", pos[bindingAdapterPosition])
.putExtra("sortBy", Anilist.sortBy[2])
.putExtra("search", true)
.also {
if (pos[bindingAdapterPosition].lowercase() == "hentai") {
if (!Anilist.adult) Toast.makeText(
itemView.context,
currActivity()?.getString(R.string.content_18),
Toast.LENGTH_SHORT
).show()
it.putExtra("hentai", true)
}
},
null
)
}
}
}
fun addGenre(genre: Pair<String, String>) {
genres[genre.first] = genre.second
pos.add(genre.first)
notifyItemInserted(pos.size - 1)
}
}

View file

@ -0,0 +1,118 @@
package ani.dantotsu.media
import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.anilist.api.MediaEdge
import ani.dantotsu.connections.anilist.api.MediaList
import ani.dantotsu.connections.anilist.api.MediaType
import ani.dantotsu.media.anime.Anime
import ani.dantotsu.media.manga.Manga
import java.io.Serializable
import ani.dantotsu.connections.anilist.api.Media as ApiMedia
data class Media(
val anime: Anime? = null,
val manga: Manga? = null,
val id: Int,
var idMAL: Int? = null,
var typeMAL: String? = null,
val name: String?,
val nameRomaji: String,
val userPreferredName: String,
var cover: String? = null,
val banner: String? = null,
var relation: String? = null,
var popularity: Int? = null,
var isAdult: Boolean,
var isFav: Boolean = false,
var notify: Boolean = false,
var userListId: Int? = null,
var isListPrivate: Boolean = false,
var notes: String? = null,
var userProgress: Int? = null,
var userStatus: String? = null,
var userScore: Int = 0,
var userRepeat: Int = 0,
var userUpdatedAt: Long? = null,
var userStartedAt: FuzzyDate = FuzzyDate(),
var userCompletedAt: FuzzyDate = FuzzyDate(),
var inCustomListsOf: MutableMap<String, Boolean>?= null,
var userFavOrder: Int? = null,
val status: String? = null,
var format: String? = null,
var source: String? = null,
var countryOfOrigin: String? = null,
val meanScore: Int? = null,
var genres: ArrayList<String> = arrayListOf(),
var tags: ArrayList<String> = arrayListOf(),
var description: String? = null,
var synonyms: ArrayList<String> = arrayListOf(),
var trailer: String? = null,
var startDate: FuzzyDate? = null,
var endDate: FuzzyDate? = null,
var characters: ArrayList<Character>? = null,
var prequel: Media? = null,
var sequel: Media? = null,
var relations: ArrayList<Media>? = null,
var recommendations: ArrayList<Media>? = null,
var vrvId: String? = null,
var crunchySlug: String? = null,
var nameMAL: String? = null,
var shareLink: String? = null,
var selected: Selected? = null,
var idKitsu: String?=null,
var cameFromContinue: Boolean = false
) : Serializable {
constructor(apiMedia: ApiMedia) : this(
id = apiMedia.id,
idMAL = apiMedia.idMal,
popularity = apiMedia.popularity,
name = apiMedia.title!!.english,
nameRomaji = apiMedia.title!!.romaji,
userPreferredName = apiMedia.title!!.userPreferred,
cover = apiMedia.coverImage?.large,
banner = apiMedia.bannerImage,
status = apiMedia.status.toString(),
isFav = apiMedia.isFavourite!!,
isAdult = apiMedia.isAdult ?: false,
isListPrivate = apiMedia.mediaListEntry?.private ?: false,
userProgress = apiMedia.mediaListEntry?.progress,
userScore = apiMedia.mediaListEntry?.score?.toInt() ?: 0,
userStatus = apiMedia.mediaListEntry?.status?.toString(),
meanScore = apiMedia.meanScore,
startDate = apiMedia.startDate,
endDate = apiMedia.endDate,
anime = if (apiMedia.type == MediaType.ANIME) Anime(
totalEpisodes = apiMedia.episodes,
nextAiringEpisode = apiMedia.nextAiringEpisode?.episode?.minus(1)
) else null,
manga = if (apiMedia.type == MediaType.MANGA) Manga(totalChapters = apiMedia.chapters) else null,
format = apiMedia.format?.toString(),
)
constructor(mediaList: MediaList) : this(mediaList.media!!) {
this.userProgress = mediaList.progress
this.isListPrivate = mediaList.private ?: false
this.userScore = mediaList.score?.toInt() ?: 0
this.userStatus = mediaList.status?.toString()
this.userUpdatedAt = mediaList.updatedAt?.toLong()
}
constructor(mediaEdge: MediaEdge) : this(mediaEdge.node!!) {
this.relation = mediaEdge.relationType?.toString()
}
fun mainName() = nameMAL ?: name ?: nameRomaji
fun mangaName() = if (countryOfOrigin != "JP") mainName() else nameRomaji
}

View file

@ -0,0 +1,305 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.FragmentActivity
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.*
import ani.dantotsu.databinding.ItemMediaCompactBinding
import ani.dantotsu.databinding.ItemMediaLargeBinding
import ani.dantotsu.databinding.ItemMediaPageBinding
import ani.dantotsu.databinding.ItemMediaPageSmallBinding
import ani.dantotsu.settings.UserInterfaceSettings
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.request.RequestOptions
import com.flaviofaria.kenburnsview.RandomTransitionGenerator
import jp.wasabeef.glide.transformations.BlurTransformation
import java.io.Serializable
class MediaAdaptor(
var type: Int,
private val mediaList: MutableList<Media>?,
private val activity: FragmentActivity,
private val matchParent: Boolean = false,
private val viewPager: ViewPager2? = null,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (type) {
0 -> MediaViewHolder(ItemMediaCompactBinding.inflate(LayoutInflater.from(parent.context), parent, false))
1 -> MediaLargeViewHolder(ItemMediaLargeBinding.inflate(LayoutInflater.from(parent.context), parent, false))
2 -> MediaPageViewHolder(ItemMediaPageBinding.inflate(LayoutInflater.from(parent.context), parent, false))
3 -> MediaPageSmallViewHolder(
ItemMediaPageSmallBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
else -> throw IllegalArgumentException()
}
}
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (type) {
0 -> {
val b = (holder as MediaViewHolder).binding
setAnimation(activity, b.root, uiSettings)
val media = mediaList?.getOrNull(position)
if (media != null) {
b.itemCompactImage.loadImage(media.cover)
b.itemCompactOngoing.visibility = if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
b.itemCompactTitle.text = media.userPreferredName
b.itemCompactScore.text =
((if (media.userScore == 0) (media.meanScore ?: 0) else media.userScore) / 10.0).toString()
b.itemCompactScoreBG.background = ContextCompat.getDrawable(
b.root.context,
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
)
b.itemCompactUserProgress.text = (media.userProgress ?: "~").toString()
if (media.relation != null) {
b.itemCompactRelation.text = "${media.relation} "
b.itemCompactType.visibility = View.VISIBLE
} else {
b.itemCompactType.visibility = View.GONE
}
if (media.anime != null) {
if (media.relation != null) b.itemCompactTypeImage.setImageDrawable(
AppCompatResources.getDrawable(
activity,
R.drawable.ic_round_movie_filter_24
)
)
b.itemCompactTotal.text =
" | ${if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " | " + (media.anime.totalEpisodes ?: "~").toString()) else (media.anime.totalEpisodes ?: "~").toString()}"
} else if (media.manga != null) {
if (media.relation != null) b.itemCompactTypeImage.setImageDrawable(
AppCompatResources.getDrawable(
activity,
R.drawable.ic_round_import_contacts_24
)
)
b.itemCompactTotal.text = " | ${media.manga.totalChapters ?: "~"}"
}
}
}
1 -> {
val b = (holder as MediaLargeViewHolder).binding
setAnimation(activity, b.root, uiSettings)
val media = mediaList?.get(position)
if (media != null) {
b.itemCompactImage.loadImage(media.cover)
b.itemCompactBanner.loadImage(media.banner ?: media.cover, 400)
b.itemCompactOngoing.visibility = if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
b.itemCompactTitle.text = media.userPreferredName
b.itemCompactScore.text =
((if (media.userScore == 0) (media.meanScore ?: 0) else media.userScore) / 10.0).toString()
b.itemCompactScoreBG.background = ContextCompat.getDrawable(
b.root.context,
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
)
if (media.anime != null) {
b.itemTotal.text = " " + if ((media.anime.totalEpisodes ?: 0) != 1) currActivity()!!.getString(R.string.episode_plural)
else currActivity()!!.getString(R.string.episode_singular)
b.itemCompactTotal.text =
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " / " + (media.anime.totalEpisodes
?: "??").toString()) else (media.anime.totalEpisodes ?: "??").toString()
} else if (media.manga != null) {
b.itemTotal.text = " " + if ((media.manga.totalChapters ?: 0) != 1) currActivity()!!.getString(R.string.chapter_plural)
else currActivity()!!.getString(R.string.chapter_singular)
b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}"
}
@SuppressLint("NotifyDataSetChanged")
if (position == mediaList!!.size - 2 && viewPager != null) viewPager.post {
mediaList.addAll(mediaList)
notifyDataSetChanged()
}
}
}
2 -> {
val b = (holder as MediaPageViewHolder).binding
val media = mediaList?.get(position)
if (media != null) {
b.itemCompactImage.loadImage(media.cover)
if (uiSettings.bannerAnimations)
b.itemCompactBanner.setTransitionGenerator(
RandomTransitionGenerator(
(10000 + 15000 * (uiSettings.animationSpeed)).toLong(),
AccelerateDecelerateInterpolator()
)
)
val banner = if (uiSettings.bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen
val context = b.itemCompactBanner.context
if (!(context as Activity).isDestroyed)
Glide.with(context as Context)
.load(GlideUrl(media.banner ?: media.cover))
.diskCacheStrategy(DiskCacheStrategy.ALL).override(400)
.apply(RequestOptions.bitmapTransform(BlurTransformation(2, 3)))
.into(banner)
b.itemCompactOngoing.visibility = if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
b.itemCompactTitle.text = media.userPreferredName
b.itemCompactScore.text =
((if (media.userScore == 0) (media.meanScore ?: 0) else media.userScore) / 10.0).toString()
b.itemCompactScoreBG.background = ContextCompat.getDrawable(
b.root.context,
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
)
if (media.anime != null) {
b.itemTotal.text = " " + if ((media.anime.totalEpisodes ?: 0) != 1) currActivity()!!.getString(R.string.episode_plural)
else currActivity()!!.getString(R.string.episode_singular)
b.itemCompactTotal.text =
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " / " + (media.anime.totalEpisodes
?: "??").toString()) else (media.anime.totalEpisodes ?: "??").toString()
} else if (media.manga != null) {
b.itemTotal.text =" " + if ((media.manga.totalChapters ?: 0) != 1) currActivity()!!.getString(R.string.chapter_plural)
else currActivity()!!.getString(R.string.chapter_singular)
b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}"
}
@SuppressLint("NotifyDataSetChanged")
if (position == mediaList!!.size - 2 && viewPager != null) viewPager.post {
val size = mediaList.size
mediaList.addAll(mediaList)
notifyItemRangeInserted(size - 1, mediaList.size)
}
}
}
3 -> {
val b = (holder as MediaPageSmallViewHolder).binding
val media = mediaList?.get(position)
if (media != null) {
b.itemCompactImage.loadImage(media.cover)
if (uiSettings.bannerAnimations)
b.itemCompactBanner.setTransitionGenerator(
RandomTransitionGenerator(
(10000 + 15000 * (uiSettings.animationSpeed)).toLong(),
AccelerateDecelerateInterpolator()
)
)
val banner = if (uiSettings.bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen
val context = b.itemCompactBanner.context
if (!(context as Activity).isDestroyed)
Glide.with(context as Context)
.load(GlideUrl(media.banner ?: media.cover))
.diskCacheStrategy(DiskCacheStrategy.ALL).override(400)
.apply(RequestOptions.bitmapTransform(BlurTransformation(2, 3)))
.into(banner)
b.itemCompactOngoing.visibility = if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
b.itemCompactTitle.text = media.userPreferredName
b.itemCompactScore.text =
((if (media.userScore == 0) (media.meanScore ?: 0) else media.userScore) / 10.0).toString()
b.itemCompactScoreBG.background = ContextCompat.getDrawable(
b.root.context,
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
)
media.genres.apply {
if (isNotEmpty()) {
var genres = ""
forEach { genres += "$it" }
genres = genres.removeSuffix("")
b.itemCompactGenres.text = genres
}
}
b.itemCompactStatus.text = media.status ?: ""
if (media.anime != null) {
b.itemTotal.text = " " + if ((media.anime.totalEpisodes ?: 0) != 1) currActivity()!!.getString(R.string.episode_plural)
else currActivity()!!.getString(R.string.episode_singular)
b.itemCompactTotal.text =
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " / " + (media.anime.totalEpisodes
?: "??").toString()) else (media.anime.totalEpisodes ?: "??").toString()
} else if (media.manga != null) {
b.itemTotal.text = " " + if ((media.manga.totalChapters ?: 0) != 1) currActivity()!!.getString(R.string.chapter_plural)
else currActivity()!!.getString(R.string.chapter_singular)
b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}"
}
@SuppressLint("NotifyDataSetChanged")
if (position == mediaList!!.size - 2 && viewPager != null) viewPager.post {
val size = mediaList.size
mediaList.addAll(mediaList)
notifyItemRangeInserted(size - 1, mediaList.size)
}
}
}
}
}
override fun getItemCount() = mediaList!!.size
override fun getItemViewType(position: Int): Int {
return type
}
inner class MediaViewHolder(val binding: ItemMediaCompactBinding) : RecyclerView.ViewHolder(binding.root) {
init {
if (matchParent) itemView.updateLayoutParams { width = -1 }
itemView.setSafeOnClickListener { clicked(bindingAdapterPosition) }
itemView.setOnLongClickListener { longClicked(bindingAdapterPosition) }
}
}
inner class MediaLargeViewHolder(val binding: ItemMediaLargeBinding) : RecyclerView.ViewHolder(binding.root) {
init {
itemView.setSafeOnClickListener { clicked(bindingAdapterPosition) }
itemView.setOnLongClickListener { longClicked(bindingAdapterPosition) }
}
}
@SuppressLint("ClickableViewAccessibility")
inner class MediaPageViewHolder(val binding: ItemMediaPageBinding) : RecyclerView.ViewHolder(binding.root) {
init {
binding.itemCompactImage.setSafeOnClickListener { clicked(bindingAdapterPosition) }
itemView.setOnTouchListener { _, _ -> true }
binding.itemCompactImage.setOnLongClickListener { longClicked(bindingAdapterPosition) }
}
}
@SuppressLint("ClickableViewAccessibility")
inner class MediaPageSmallViewHolder(val binding: ItemMediaPageSmallBinding) : RecyclerView.ViewHolder(binding.root) {
init {
binding.itemCompactImage.setSafeOnClickListener { clicked(bindingAdapterPosition) }
binding.itemCompactTitleContainer.setSafeOnClickListener { clicked(bindingAdapterPosition) }
itemView.setOnTouchListener { _, _ -> true }
binding.itemCompactImage.setOnLongClickListener { longClicked(bindingAdapterPosition) }
}
}
fun clicked(position: Int) {
if ((mediaList?.size ?: 0) > position && position != -1) {
val media = mediaList?.get(position)
ContextCompat.startActivity(
activity,
Intent(activity, MediaDetailsActivity::class.java).putExtra(
"media",
media as Serializable
), null
)
}
}
fun longClicked(position: Int): Boolean {
if ((mediaList?.size ?: 0) > position && position != -1) {
val media = mediaList?.get(position) ?: return false
if (activity.supportFragmentManager.findFragmentByTag("list") == null) {
MediaListDialogSmallFragment.newInstance(media).show(activity.supportFragmentManager, "list")
return true
}
}
return false
}
}

View file

@ -0,0 +1,456 @@
package ani.dantotsu.media
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.text.SpannableStringBuilder
import android.util.TypedValue
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.ImageView
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import androidx.core.text.bold
import androidx.core.text.color
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.lifecycleScope
import androidx.viewpager2.adapter.FragmentStateAdapter
import ani.dantotsu.CustomBottomNavBar
import ani.dantotsu.GesturesListener
import ani.dantotsu.R
import ani.dantotsu.Refresh
import ani.dantotsu.ZoomOutPageTransformer
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.media.anime.AnimeWatchFragment
import ani.dantotsu.copyToClipboard
import ani.dantotsu.databinding.ActivityMediaBinding
import ani.dantotsu.initActivity
import ani.dantotsu.loadData
import ani.dantotsu.loadImage
import ani.dantotsu.media.manga.MangaReadFragment
import ani.dantotsu.navBarHeight
import ani.dantotsu.media.novel.NovelReadFragment
import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.others.getSerialized
import ani.dantotsu.saveData
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight
import com.flaviofaria.kenburnsview.RandomTransitionGenerator
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.navigation.NavigationBarView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.math.abs
class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener {
private lateinit var binding: ActivityMediaBinding
private val scope = lifecycleScope
private val model: MediaDetailsViewModel by viewModels()
private lateinit var tabLayout: NavigationBarView
private lateinit var uiSettings: UserInterfaceSettings
var selected = 0
var anime = true
private var adult = false
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMediaBinding.inflate(layoutInflater)
setContentView(binding.root)
screenWidth = resources.displayMetrics.widthPixels.toFloat()
//Ui init
initActivity(this)
uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
if (!uiSettings.immersiveMode) this.window.statusBarColor = ContextCompat.getColor(this, R.color.nav_bg_inv)
binding.mediaBanner.updateLayoutParams { height += statusBarHeight }
binding.mediaBannerNoKen.updateLayoutParams { height += statusBarHeight }
binding.mediaClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
binding.mediaCollapsing.minimumHeight = statusBarHeight
if (binding.mediaTab is CustomBottomNavBar) binding.mediaTab.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
}
binding.mediaTitle.isSelected = true
mMaxScrollSize = binding.mediaAppBar.totalScrollRange
binding.mediaAppBar.addOnOffsetChangedListener(this)
binding.mediaClose.setOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
if (uiSettings.bannerAnimations) {
val adi = AccelerateDecelerateInterpolator()
val generator = RandomTransitionGenerator((10000 + 15000 * (uiSettings.animationSpeed)).toLong(), adi)
binding.mediaBanner.setTransitionGenerator(generator)
}
val banner = if (uiSettings.bannerAnimations) binding.mediaBanner else binding.mediaBannerNoKen
val viewPager = binding.mediaViewPager
tabLayout = binding.mediaTab as NavigationBarView
viewPager.isUserInputEnabled = false
viewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings))
var media: Media = intent.getSerialized("media") ?: return
media.selected = model.loadSelected(media)
binding.mediaCoverImage.loadImage(media.cover)
binding.mediaCoverImage.setOnLongClickListener {
ImageViewDialog.newInstance(
this,
media.userPreferredName + "[Cover]",
media.cover
)
}
banner.loadImage(media.banner ?: media.cover, 400)
val gestureDetector = GestureDetector(this, object : GesturesListener() {
override fun onDoubleClick(event: MotionEvent) {
if (!uiSettings.bannerAnimations)
snackString(getString(R.string.enable_banner_animations))
else {
binding.mediaBanner.restart()
binding.mediaBanner.performClick()
}
}
override fun onLongClick(event: MotionEvent) {
ImageViewDialog.newInstance(
this@MediaDetailsActivity,
media.userPreferredName + "[Banner]",
media.banner ?: media.cover
)
banner.performClick()
}
})
banner.setOnTouchListener { _, motionEvent -> gestureDetector.onTouchEvent(motionEvent);true }
binding.mediaTitle.text = media.userPreferredName
binding.mediaTitle.setOnLongClickListener {
copyToClipboard(media.userPreferredName)
true
}
binding.mediaTitleCollapse.text = media.userPreferredName
binding.mediaTitleCollapse.setOnLongClickListener {
copyToClipboard(media.userPreferredName)
true
}
binding.mediaStatus.text = media.status ?: ""
//Fav Button
val favButton = if (Anilist.userid != null) {
if (media.isFav) binding.mediaFav.setImageDrawable(
AppCompatResources.getDrawable(
this,
R.drawable.ic_round_favorite_24
)
)
PopImageButton(
scope,
binding.mediaFav,
R.drawable.ic_round_favorite_24,
R.drawable.ic_round_favorite_border_24,
R.color.nav_tab,
R.color.fav,
media.isFav
) {
media.isFav = it
Anilist.mutation.toggleFav(media.anime != null, media.id)
Refresh.all()
}
} else {
binding.mediaFav.visibility = View.GONE
null
}
fun total() {
val text = SpannableStringBuilder().apply {
val white = ContextCompat.getColor(this@MediaDetailsActivity, R.color.bg_opp)
if (media.userStatus != null) {
append(if (media.anime != null) getString(R.string.watched_num) else getString(R.string.read_num))
val typedValue = TypedValue()
theme.resolveAttribute(com.google.android.material.R.attr.colorSecondary, typedValue, true)
bold { color(typedValue.data) { append("${media.userProgress}") } }
append(if (media.anime != null) getString(R.string.episodes_out_of) else getString(R.string.chapters_out_of))
} else {
append(if (media.anime != null) getString(R.string.episodes_total_of) else getString(R.string.chapters_total_of))
}
if (media.anime != null) {
if (media.anime!!.nextAiringEpisode != null) {
bold { color(white) { append("${media.anime!!.nextAiringEpisode}") } }
append(" / ")
}
bold { color(white) { append("${media.anime!!.totalEpisodes ?: "??"}") } }
} else
bold { color(white) { append("${media.manga!!.totalChapters ?: "??"}") } }
}
binding.mediaTotal.text = text
}
fun progress() {
val statuses: Array<String> = resources.getStringArray(R.array.status)
val statusStrings = if (media.manga==null) resources.getStringArray(R.array.status_anime) else resources.getStringArray(R.array.status_manga)
val userStatus = if(media.userStatus != null) statusStrings[statuses.indexOf(media.userStatus)] else statusStrings[0]
if (media.userStatus != null) {
binding.mediaTotal.visibility = View.VISIBLE
binding.mediaAddToList.text = userStatus
} else {
binding.mediaAddToList.setText(R.string.add)
}
total()
binding.mediaAddToList.setOnClickListener {
if (Anilist.userid != null) {
if (supportFragmentManager.findFragmentByTag("dialog") == null)
MediaListDialogFragment().show(supportFragmentManager, "dialog")
} else snackString(getString(R.string.please_login_anilist))
}
binding.mediaAddToList.setOnLongClickListener {
saveData("${media.id}_progressDialog", true)
snackString(getString(R.string.auto_update_reset))
true
}
}
progress()
model.getMedia().observe(this) {
if (it != null) {
media = it
scope.launch {
if(media.isFav!=favButton?.clicked) favButton?.clicked()
}
binding.mediaNotify.setOnClickListener {
val i = Intent(Intent.ACTION_SEND)
i.type = "text/plain"
i.putExtra(Intent.EXTRA_TEXT, media.shareLink)
startActivity(Intent.createChooser(i, media.userPreferredName))
}
binding.mediaNotify.setOnLongClickListener {
openLinkInBrowser(media.shareLink)
true
}
binding.mediaCover.setOnClickListener {
openLinkInBrowser(media.shareLink)
}
progress()
}
}
adult = media.isAdult
tabLayout.menu.clear()
if (media.anime != null) {
viewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle, SupportedMedia.ANIME)
tabLayout.inflateMenu(R.menu.anime_menu_detail)
} else if (media.manga != null) {
viewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle, if(media.format=="NOVEL") SupportedMedia.NOVEL else SupportedMedia.MANGA)
tabLayout.inflateMenu(R.menu.manga_menu_detail)
anime = false
}
selected = media.selected!!.window
binding.mediaTitle.translationX = -screenWidth
tabLayout.visibility = View.VISIBLE
tabLayout.setOnItemSelectedListener { item ->
selectFromID(item.itemId)
viewPager.setCurrentItem(selected, false)
val sel = model.loadSelected(media)
sel.window = selected
model.saveSelected(media.id, sel, this)
true
}
tabLayout.selectedItemId = idFromSelect()
viewPager.setCurrentItem(selected, false)
if (model.continueMedia == null && media.cameFromContinue) {
model.continueMedia = loadData("continue_media") ?: true
selected = 1
}
val live = Refresh.activity.getOrPut(this.hashCode()) { MutableLiveData(true) }
live.observe(this) {
if (it) {
scope.launch(Dispatchers.IO) {
model.loadMedia(media)
live.postValue(false)
}
}
}
}
private fun selectFromID(id: Int) {
when (id) {
R.id.info -> {
selected = 0
}
R.id.watch, R.id.read -> {
selected = 1
}
}
}
private fun idFromSelect(): Int {
if (anime) when (selected) {
0 -> return R.id.info
1 -> return R.id.watch
}
else when (selected) {
0 -> return R.id.info
1 -> return R.id.read
}
return R.id.info
}
override fun onResume() {
tabLayout.selectedItemId = idFromSelect()
super.onResume()
}
private enum class SupportedMedia{
ANIME, MANGA, NOVEL
}
//ViewPager
private class ViewPagerAdapter(
fragmentManager: FragmentManager,
lifecycle: Lifecycle,
private val media: SupportedMedia
) :
FragmentStateAdapter(fragmentManager, lifecycle) {
override fun getItemCount(): Int = 2
override fun createFragment(position: Int): Fragment = when (position){
0 -> MediaInfoFragment()
1 -> when(media){
SupportedMedia.ANIME -> AnimeWatchFragment()
SupportedMedia.MANGA -> MangaReadFragment()
SupportedMedia.NOVEL -> NovelReadFragment()
}
else -> MediaInfoFragment()
}
}
//Collapsing UI Stuff
private var isCollapsed = false
private val percent = 45
private var mMaxScrollSize = 0
private var screenWidth: Float = 0f
override fun onOffsetChanged(appBar: AppBarLayout, i: Int) {
if (mMaxScrollSize == 0) mMaxScrollSize = appBar.totalScrollRange
val percentage = abs(i) * 100 / mMaxScrollSize
binding.mediaCover.visibility = if (binding.mediaCover.scaleX == 0f) View.GONE else View.VISIBLE
val duration = (200 * uiSettings.animationSpeed).toLong()
if (percentage >= percent && !isCollapsed) {
isCollapsed = true
ObjectAnimator.ofFloat(binding.mediaTitle, "translationX", 0f).setDuration(duration).start()
ObjectAnimator.ofFloat(binding.mediaAccessContainer, "translationX", screenWidth).setDuration(duration).start()
ObjectAnimator.ofFloat(binding.mediaCover, "translationX", screenWidth).setDuration(duration).start()
ObjectAnimator.ofFloat(binding.mediaCollapseContainer, "translationX", screenWidth).setDuration(duration).start()
binding.mediaBanner.pause()
if (!uiSettings.immersiveMode) this.window.statusBarColor = ContextCompat.getColor(this, R.color.nav_bg)
}
if (percentage <= percent && isCollapsed) {
isCollapsed = false
ObjectAnimator.ofFloat(binding.mediaTitle, "translationX", -screenWidth).setDuration(duration).start()
ObjectAnimator.ofFloat(binding.mediaAccessContainer, "translationX", 0f).setDuration(duration).start()
ObjectAnimator.ofFloat(binding.mediaCover, "translationX", 0f).setDuration(duration).start()
ObjectAnimator.ofFloat(binding.mediaCollapseContainer, "translationX", 0f).setDuration(duration).start()
if (uiSettings.bannerAnimations) binding.mediaBanner.resume()
if (!uiSettings.immersiveMode) this.window.statusBarColor = ContextCompat.getColor(this, R.color.nav_bg_inv)
}
if (percentage == 1 && model.scrolledToTop.value != false) model.scrolledToTop.postValue(false)
if (percentage == 0 && model.scrolledToTop.value != true) model.scrolledToTop.postValue(true)
}
class PopImageButton(
private val scope: CoroutineScope,
private val image: ImageView,
private val d1: Int,
private val d2: Int,
private val c1: Int,
private val c2: Int,
var clicked: Boolean,
callback: suspend (Boolean) -> (Unit)
) {
private var disabled = false
private val context = image.context
private var pressable = true
init {
enabled(true)
scope.launch {
clicked()
}
image.setOnClickListener {
if (pressable && !disabled) {
pressable = false
clicked = !clicked
scope.launch {
launch(Dispatchers.IO) {
callback.invoke(clicked)
}
clicked()
pressable = true
}
}
}
}
suspend fun clicked() {
ObjectAnimator.ofFloat(image, "scaleX", 1f, 0f).setDuration(69).start()
ObjectAnimator.ofFloat(image, "scaleY", 1f, 0f).setDuration(100).start()
delay(100)
if (clicked) {
ObjectAnimator.ofArgb(image,
"ColorFilter",
ContextCompat.getColor(context, c1),
ContextCompat.getColor(context, c2)
).setDuration(120).start()
image.setImageDrawable(AppCompatResources.getDrawable(context, d1))
} else image.setImageDrawable(AppCompatResources.getDrawable(context, d2))
ObjectAnimator.ofFloat(image, "scaleX", 0f, 1.5f).setDuration(120).start()
ObjectAnimator.ofFloat(image, "scaleY", 0f, 1.5f).setDuration(100).start()
delay(120)
ObjectAnimator.ofFloat(image, "scaleX", 1.5f, 1f).setDuration(100).start()
ObjectAnimator.ofFloat(image, "scaleY", 1.5f, 1f).setDuration(100).start()
delay(200)
if (clicked) ObjectAnimator.ofArgb(
image,
"ColorFilter",
ContextCompat.getColor(context, c2),
ContextCompat.getColor(context, c1)
).setDuration(200).start()
}
fun enabled(enabled: Boolean) {
disabled = !enabled
image.alpha = if(disabled) 0.33f else 1f
}
}
}

View file

@ -0,0 +1,279 @@
package ani.dantotsu.media
import android.app.Activity
import android.os.Handler
import android.os.Looper
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.media.anime.Episode
import ani.dantotsu.media.anime.SelectorDialogFragment
import ani.dantotsu.loadData
import ani.dantotsu.logger
import ani.dantotsu.media.manga.MangaChapter
import ani.dantotsu.others.AniSkip
import ani.dantotsu.others.Jikan
import ani.dantotsu.others.Kitsu
import ani.dantotsu.parsers.Book
import ani.dantotsu.parsers.MangaImage
import ani.dantotsu.parsers.MangaReadSources
import ani.dantotsu.parsers.NovelSources
import ani.dantotsu.parsers.ShowResponse
import ani.dantotsu.parsers.VideoExtractor
import ani.dantotsu.parsers.WatchSources
import ani.dantotsu.saveData
import ani.dantotsu.snackString
import ani.dantotsu.tryWithSuspend
import ani.dantotsu.currContext
import ani.dantotsu.R
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
class MediaDetailsViewModel : ViewModel() {
val scrolledToTop = MutableLiveData(true)
fun saveSelected(id: Int, data: Selected, activity: Activity? = null) {
saveData("$id-select", data, activity)
}
fun loadSelected(media: Media): Selected {
return loadData<Selected>("${media.id}-select") ?: Selected().let {
it.source = if (media.isAdult) 0 else when (media.anime != null) {
true -> loadData("settings_def_anime_source") ?: 0
else -> loadData("settings_def_manga_source") ?: 0
}
it.preferDub = loadData("settings_prefer_dub") ?: false
saveSelected(media.id, it)
it
}
}
var continueMedia: Boolean? = null
private var loading = false
private val media: MutableLiveData<Media> = MutableLiveData<Media>(null)
fun getMedia(): LiveData<Media> = media
fun loadMedia(m: Media) {
if (!loading) {
loading = true
media.postValue(Anilist.query.mediaDetails(m))
}
loading = false
}
fun setMedia(m: Media) {
media.postValue(m)
}
val responses = MutableLiveData<List<ShowResponse>?>(null)
//Anime
private val kitsuEpisodes: MutableLiveData<Map<String, Episode>> = MutableLiveData<Map<String, Episode>>(null)
fun getKitsuEpisodes(): LiveData<Map<String, Episode>> = kitsuEpisodes
suspend fun loadKitsuEpisodes(s: Media) {
tryWithSuspend {
if (kitsuEpisodes.value == null) kitsuEpisodes.postValue(Kitsu.getKitsuEpisodesDetails(s))
}
}
private val fillerEpisodes: MutableLiveData<Map<String, Episode>> = MutableLiveData<Map<String, Episode>>(null)
fun getFillerEpisodes(): LiveData<Map<String, Episode>> = fillerEpisodes
suspend fun loadFillerEpisodes(s: Media) {
tryWithSuspend {
if (fillerEpisodes.value == null) fillerEpisodes.postValue(
Jikan.getEpisodes(
s.idMAL ?: return@tryWithSuspend
)
)
}
}
var watchSources: WatchSources? = null
private val episodes = MutableLiveData<MutableMap<Int, MutableMap<String, Episode>>>(null)
private val epsLoaded = mutableMapOf<Int, MutableMap<String, Episode>>()
fun getEpisodes(): LiveData<MutableMap<Int, MutableMap<String, Episode>>> = episodes
suspend fun loadEpisodes(media: Media, i: Int) {
if (!epsLoaded.containsKey(i)) {
epsLoaded[i] = watchSources?.loadEpisodesFromMedia(i, media) ?: return
}
episodes.postValue(epsLoaded)
}
suspend fun forceLoadEpisode(media: Media, i: Int) {
epsLoaded[i] = watchSources?.loadEpisodesFromMedia(i, media) ?: return
episodes.postValue(epsLoaded)
}
suspend fun overrideEpisodes(i: Int, source: ShowResponse, id: Int) {
watchSources?.saveResponse(i, id, source)
epsLoaded[i] = watchSources?.loadEpisodes(i, source.link, source.extra, source.sAnime) ?: return
episodes.postValue(epsLoaded)
}
private var episode = MutableLiveData<Episode?>(null)
fun getEpisode(): LiveData<Episode?> = episode
suspend fun loadEpisodeVideos(ep: Episode, i: Int, post: Boolean = true) {
val link = ep.link ?: return
if (!ep.allStreams || ep.extractors.isNullOrEmpty()) {
val list = mutableListOf<VideoExtractor>()
ep.extractors = list
watchSources?.get(i)?.apply {
if (!post && !allowsPreloading) return@apply
ep.sEpisode?.let {
loadByVideoServers(link, ep.extra, it) {
if (it.videos.isNotEmpty()) {
list.add(it)
ep.extractorCallback?.invoke(it)
}
}
}
ep.extractorCallback = null
if (list.isNotEmpty())
ep.allStreams = true
}
}
if (post) {
episode.postValue(ep)
MainScope().launch(Dispatchers.Main) {
episode.value = null
}
}
}
val timeStamps = MutableLiveData<List<AniSkip.Stamp>?>()
private val timeStampsMap: MutableMap<Int, List<AniSkip.Stamp>?> = mutableMapOf()
suspend fun loadTimeStamps(malId: Int?, episodeNum: Int?, duration: Long, useProxyForTimeStamps: Boolean) {
malId ?: return
episodeNum ?: return
if (timeStampsMap.containsKey(episodeNum))
return timeStamps.postValue(timeStampsMap[episodeNum])
val result = AniSkip.getResult(malId, episodeNum, duration, useProxyForTimeStamps)
timeStampsMap[episodeNum] = result
timeStamps.postValue(result)
}
suspend fun loadEpisodeSingleVideo(ep: Episode, selected: Selected, post: Boolean = true): Boolean {
if (ep.extractors.isNullOrEmpty()) {
val server = selected.server ?: return false
val link = ep.link ?: return false
ep.extractors = mutableListOf(watchSources?.get(selected.source)?.let {
if (!post && !it.allowsPreloading) null
else ep.sEpisode?.let { it1 ->
it.loadSingleVideoServer(server, link, ep.extra,
it1, post)
}
} ?: return false)
ep.allStreams = false
}
if (post) {
episode.postValue(ep)
MainScope().launch(Dispatchers.Main) {
episode.value = null
}
}
return true
}
fun setEpisode(ep: Episode?, who: String) {
logger("set episode ${ep?.number} - $who", false)
episode.postValue(ep)
MainScope().launch(Dispatchers.Main) {
episode.value = null
}
}
val epChanged = MutableLiveData(true)
fun onEpisodeClick(media: Media, i: String, manager: FragmentManager, launch: Boolean = true, prevEp: String? = null) {
Handler(Looper.getMainLooper()).post {
if (manager.findFragmentByTag("dialog") == null && !manager.isDestroyed) {
if (media.anime?.episodes?.get(i) != null) {
media.anime.selectedEpisode = i
} else {
snackString(currContext()?.getString(R.string.episode_not_found, i))
return@post
}
media.selected = this.loadSelected(media)
val selector = SelectorDialogFragment.newInstance(media.selected!!.server, launch, prevEp)
selector.show(manager, "dialog")
}
}
}
//Manga
var mangaReadSources: MangaReadSources? = null
private val mangaChapters = MutableLiveData<MutableMap<Int, MutableMap<String, MangaChapter>>>(null)
private val mangaLoaded = mutableMapOf<Int, MutableMap<String, MangaChapter>>()
fun getMangaChapters(): LiveData<MutableMap<Int, MutableMap<String, MangaChapter>>> = mangaChapters
suspend fun loadMangaChapters(media: Media, i: Int) {
logger("Loading Manga Chapters : $mangaLoaded")
if (!mangaLoaded.containsKey(i)) tryWithSuspend {
mangaLoaded[i] = mangaReadSources?.loadChaptersFromMedia(i, media) ?: return@tryWithSuspend
}
mangaChapters.postValue(mangaLoaded)
}
suspend fun overrideMangaChapters(i: Int, source: ShowResponse, id: Int) {
mangaReadSources?.saveResponse(i, id, source)
tryWithSuspend {
mangaLoaded[i] = mangaReadSources?.loadChapters(i, source) ?: return@tryWithSuspend
}
mangaChapters.postValue(mangaLoaded)
}
private val mangaChapter = MutableLiveData<MangaChapter?>(null)
fun getMangaChapter(): LiveData<MangaChapter?> = mangaChapter
suspend fun loadMangaChapterImages(chapter: MangaChapter, selected: Selected, post: Boolean = true): Boolean {
return tryWithSuspend(true) {
chapter.addImages(
mangaReadSources?.get(selected.source)?.loadImages(chapter.link) ?: return@tryWithSuspend false
)
if (post) mangaChapter.postValue(chapter)
true
} ?: false
}
fun loadTransformation(mangaImage: MangaImage, source: Int): BitmapTransformation? {
return if (mangaImage.useTransformation) mangaReadSources?.get(source)?.getTransformation() else null
}
val novelSources = NovelSources
val novelResponses = MutableLiveData<List<ShowResponse>>(null)
suspend fun searchNovels(query: String, i: Int) {
val source = novelSources[i]
tryWithSuspend(post = true) {
if (source != null) {
novelResponses.postValue(source.search(query))
}
}
}
suspend fun autoSearchNovels(media: Media) {
val source = novelSources[media.selected?.source ?: 0]
tryWithSuspend(post = true) {
if (source != null) {
novelResponses.postValue(source.sortedSearch(media))
}
}
}
val book: MutableLiveData<Book> = MutableLiveData(null)
suspend fun loadBook(novel: ShowResponse, i: Int) {
tryWithSuspend {
book.postValue(novelSources[i]?.loadBook(novel.link, novel.extra) ?: return@tryWithSuspend)
}
}
}

View file

@ -0,0 +1,490 @@
package ani.dantotsu.media
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.CountDownTimer
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.WebChromeClient
import android.widget.FrameLayout
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.*
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.GenresViewModel
import ani.dantotsu.databinding.*
import io.noties.markwon.Markwon
import io.noties.markwon.SoftBreakAddsNewLinePlugin
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import java.io.Serializable
import java.net.URLEncoder
@SuppressLint("SetTextI18n")
class MediaInfoFragment : Fragment() {
private var _binding: FragmentMediaInfoBinding? = null
private val binding get() = _binding!!
private var timer: CountDownTimer? = null
private var loaded = false
private var type = "ANIME"
private val genreModel: GenresViewModel by activityViewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentMediaInfoBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView();_binding = null
}
@SuppressLint("SetJavaScriptEnabled")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val model: MediaDetailsViewModel by activityViewModels()
binding.mediaInfoProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE
binding.mediaInfoContainer.visibility = if (loaded) View.VISIBLE else View.GONE
binding.mediaInfoContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += 128f.px + navBarHeight }
model.scrolledToTop.observe(viewLifecycleOwner){
if(it) binding.mediaInfoScroll.scrollTo(0,0)
}
model.getMedia().observe(viewLifecycleOwner) { media ->
if (media != null && !loaded) {
loaded = true
binding.mediaInfoProgressBar.visibility = View.GONE
binding.mediaInfoContainer.visibility = View.VISIBLE
binding.mediaInfoName.text = "\t\t\t" + (media.name?:media.nameRomaji)
binding.mediaInfoName.setOnLongClickListener {
copyToClipboard(media.name?:media.nameRomaji)
true
}
if (media.name != null) binding.mediaInfoNameRomajiContainer.visibility = View.VISIBLE
binding.mediaInfoNameRomaji.text = "\t\t\t" + media.nameRomaji
binding.mediaInfoNameRomaji.setOnLongClickListener {
copyToClipboard(media.nameRomaji)
true
}
binding.mediaInfoMeanScore.text = if (media.meanScore != null) (media.meanScore / 10.0).toString() else "??"
binding.mediaInfoStatus.text = media.status
binding.mediaInfoFormat.text = media.format
binding.mediaInfoSource.text = media.source
binding.mediaInfoStart.text = media.startDate?.toString() ?: "??"
binding.mediaInfoEnd.text =media.endDate?.toString() ?: "??"
if (media.anime != null) {
binding.mediaInfoDuration.text =
if (media.anime.episodeDuration != null) media.anime.episodeDuration.toString() else "??"
binding.mediaInfoDurationContainer.visibility = View.VISIBLE
binding.mediaInfoSeasonContainer.visibility = View.VISIBLE
binding.mediaInfoSeason.text =
(media.anime.season ?: "??")+ " " + (media.anime.seasonYear ?: "??")
if (media.anime.mainStudio != null) {
binding.mediaInfoStudioContainer.visibility = View.VISIBLE
binding.mediaInfoStudio.text = media.anime.mainStudio!!.name
binding.mediaInfoStudioContainer.setOnClickListener {
ContextCompat.startActivity(
requireActivity(),
Intent(activity, StudioActivity::class.java).putExtra(
"studio",
media.anime.mainStudio!! as Serializable
),
null
)
}
}
if (media.anime.author != null) {
binding.mediaInfoAuthorContainer.visibility = View.VISIBLE
binding.mediaInfoAuthor.text = media.anime.author!!.name
binding.mediaInfoAuthorContainer.setOnClickListener {
ContextCompat.startActivity(
requireActivity(),
Intent(activity, AuthorActivity::class.java).putExtra(
"author",
media.anime.author!! as Serializable
),
null
)
}
}
binding.mediaInfoTotalTitle.setText(R.string.total_eps)
binding.mediaInfoTotal.text =
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " | " + (media.anime.totalEpisodes
?: "~").toString()) else (media.anime.totalEpisodes ?: "~").toString()
} else if (media.manga != null) {
type = "MANGA"
binding.mediaInfoTotalTitle.setText(R.string.total_chaps)
binding.mediaInfoTotal.text = (media.manga.totalChapters ?: "~").toString()
if (media.manga.author != null) {
binding.mediaInfoAuthorContainer.visibility = View.VISIBLE
binding.mediaInfoAuthor.text = media.manga.author!!.name
binding.mediaInfoAuthorContainer.setOnClickListener {
ContextCompat.startActivity(
requireActivity(),
Intent(activity, AuthorActivity::class.java).putExtra(
"author",
media.manga.author!! as Serializable
),
null
)
}
}
}
val desc = HtmlCompat.fromHtml(
(media.description ?: "null").replace("\\n", "<br>").replace("\\\"", "\""),
HtmlCompat.FROM_HTML_MODE_LEGACY
)
binding.mediaInfoDescription.text =
"\t\t\t" + if (desc.toString() != "null") desc else getString(R.string.no_description_available)
binding.mediaInfoDescription.setOnClickListener {
if (binding.mediaInfoDescription.maxLines == 5) {
ObjectAnimator.ofInt(binding.mediaInfoDescription, "maxLines", 100)
.setDuration(950).start()
} else {
ObjectAnimator.ofInt(binding.mediaInfoDescription, "maxLines", 5)
.setDuration(400).start()
}
}
countDown(media, binding.mediaInfoContainer)
val parent = _binding?.mediaInfoContainer!!
val screenWidth = resources.displayMetrics.run { widthPixels / density }
if (media.synonyms.isNotEmpty()) {
val bind = ItemTitleChipgroupBinding.inflate(
LayoutInflater.from(context),
parent,
false
)
for (position in media.synonyms.indices) {
val chip = ItemChipBinding.inflate(
LayoutInflater.from(context),
bind.itemChipGroup,
false
).root
chip.text = media.synonyms[position]
chip.setOnLongClickListener { copyToClipboard(media.synonyms[position]);true }
bind.itemChipGroup.addView(chip)
}
parent.addView(bind.root)
}
if (media.trailer != null) {
@Suppress("DEPRECATION")
class MyChrome : WebChromeClient() {
private var mCustomView: View? = null
private var mCustomViewCallback: CustomViewCallback? = null
private var mOriginalSystemUiVisibility = 0
override fun onHideCustomView() {
(requireActivity().window.decorView as FrameLayout).removeView(
mCustomView
)
mCustomView = null
requireActivity().window.decorView.systemUiVisibility =
mOriginalSystemUiVisibility
mCustomViewCallback!!.onCustomViewHidden()
mCustomViewCallback = null
}
override fun onShowCustomView(
paramView: View,
paramCustomViewCallback: CustomViewCallback
) {
if (mCustomView != null) {
onHideCustomView()
return
}
mCustomView = paramView
mOriginalSystemUiVisibility =
requireActivity().window.decorView.systemUiVisibility
mCustomViewCallback = paramCustomViewCallback
(requireActivity().window.decorView as FrameLayout).addView(
mCustomView,
FrameLayout.LayoutParams(-1, -1)
)
requireActivity().window.decorView.systemUiVisibility =
3846 or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
}
}
val bind = ItemTitleTrailerBinding.inflate(
LayoutInflater.from(context),
parent,
false
)
bind.mediaInfoTrailer.apply {
visibility = View.VISIBLE
settings.javaScriptEnabled = true
isSoundEffectsEnabled = true
webChromeClient = MyChrome()
loadUrl(media.trailer!!)
}
parent.addView(bind.root)
}
if (media.anime != null && (media.anime.op.isNotEmpty() || media.anime.ed.isNotEmpty())) {
val markWon = Markwon.builder(requireContext())
.usePlugin(SoftBreakAddsNewLinePlugin.create()).build()
fun makeLink(a: String): String {
val first = a.indexOf('"').let { if (it != -1) it else return a } + 1
val end = a.indexOf('"', first).let { if (it != -1) it else return a }
val name = a.subSequence(first, end).toString()
return "${a.subSequence(0, first)}" +
"[$name](https://www.youtube.com/results?search_query=${URLEncoder.encode(name, "utf-8")})" +
"${a.subSequence(end, a.length)}"
}
fun makeText(textView: TextView, arr: ArrayList<String>) {
var op = ""
arr.forEach {
op += "\n"
op += makeLink(it)
}
op = op.removePrefix("\n")
textView.setOnClickListener {
if (textView.maxLines == 4) {
ObjectAnimator.ofInt(textView, "maxLines", 100)
.setDuration(950).start()
} else {
ObjectAnimator.ofInt(textView, "maxLines", 4)
.setDuration(400).start()
}
}
markWon.setMarkdown(textView, op)
}
if (media.anime.op.isNotEmpty()) {
val bind = ItemTitleTextBinding.inflate(LayoutInflater.from(context), parent, false)
bind.itemTitle.setText(R.string.opening)
makeText(bind.itemText, media.anime.op)
parent.addView(bind.root)
}
if (media.anime.ed.isNotEmpty()) {
val bind = ItemTitleTextBinding.inflate(LayoutInflater.from(context), parent, false)
bind.itemTitle.setText(R.string.ending)
makeText(bind.itemText, media.anime.ed)
parent.addView(bind.root)
}
}
if (media.genres.isNotEmpty()) {
val bind = ActivityGenreBinding.inflate(
LayoutInflater.from(context),
parent,
false
)
val adapter = GenreAdapter(type)
genreModel.doneListener = {
MainScope().launch {
bind.mediaInfoGenresProgressBar.visibility = View.GONE
}
}
if (genreModel.genres != null) {
adapter.genres = genreModel.genres!!
adapter.pos = ArrayList(genreModel.genres!!.keys)
if (genreModel.done) genreModel.doneListener?.invoke()
}
bind.mediaInfoGenresRecyclerView.adapter = adapter
bind.mediaInfoGenresRecyclerView.layoutManager =
GridLayoutManager(requireActivity(), (screenWidth / 156f).toInt())
lifecycleScope.launch(Dispatchers.IO) {
genreModel.loadGenres(media.genres) {
MainScope().launch {
adapter.addGenre(it)
}
}
}
parent.addView(bind.root)
}
if (media.tags.isNotEmpty()) {
val bind = ItemTitleChipgroupBinding.inflate(
LayoutInflater.from(context),
parent,
false
)
bind.itemTitle.setText(R.string.tags)
for (position in media.tags.indices) {
val chip = ItemChipBinding.inflate(
LayoutInflater.from(context),
bind.itemChipGroup,
false
).root
chip.text = media.tags[position]
chip.setSafeOnClickListener {
ContextCompat.startActivity(
chip.context,
Intent(chip.context, SearchActivity::class.java)
.putExtra("type", type)
.putExtra("sortBy", Anilist.sortBy[2])
.putExtra("tag", media.tags[position].substringBefore(" :"))
.putExtra("search", true)
.also {
if (media.isAdult) {
if (!Anilist.adult) Toast.makeText(
chip.context,
currActivity()?.getString(R.string.content_18),
Toast.LENGTH_SHORT
).show()
it.putExtra("hentai", true)
}
},
null
)
}
chip.setOnLongClickListener { copyToClipboard(media.tags[position]);true }
bind.itemChipGroup.addView(chip)
}
parent.addView(bind.root)
}
if (!media.characters.isNullOrEmpty()) {
val bind = ItemTitleRecyclerBinding.inflate(
LayoutInflater.from(context),
parent,
false
)
bind.itemTitle.setText(R.string.characters)
bind.itemRecycler.adapter =
CharacterAdapter(media.characters!!)
bind.itemRecycler.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
parent.addView(bind.root)
}
if (!media.relations.isNullOrEmpty()) {
if (media.sequel != null || media.prequel != null) {
val bind = ItemQuelsBinding.inflate(
LayoutInflater.from(context),
parent,
false
)
if (media.sequel != null) {
bind.mediaInfoSequel.visibility = View.VISIBLE
bind.mediaInfoSequelImage.loadImage(
media.sequel!!.banner ?: media.sequel!!.cover
)
bind.mediaInfoSequel.setSafeOnClickListener {
ContextCompat.startActivity(
requireContext(),
Intent(
requireContext(),
MediaDetailsActivity::class.java
).putExtra(
"media",
media.sequel as Serializable
), null
)
}
}
if (media.prequel != null) {
bind.mediaInfoPrequel.visibility = View.VISIBLE
bind.mediaInfoPrequelImage.loadImage(
media.prequel!!.banner ?: media.prequel!!.cover
)
bind.mediaInfoPrequel.setSafeOnClickListener {
ContextCompat.startActivity(
requireContext(),
Intent(
requireContext(),
MediaDetailsActivity::class.java
).putExtra(
"media",
media.prequel as Serializable
), null
)
}
}
parent.addView(bind.root)
}
val bindi = ItemTitleRecyclerBinding.inflate(
LayoutInflater.from(context),
parent,
false
)
bindi.itemRecycler.adapter =
MediaAdaptor(0, media.relations!!, requireActivity())
bindi.itemRecycler.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
parent.addView(bindi.root)
}
if (!media.recommendations.isNullOrEmpty()) {
val bind = ItemTitleRecyclerBinding.inflate(
LayoutInflater.from(context),
parent,
false
)
bind.itemTitle.setText(R.string.recommended)
bind.itemRecycler.adapter =
MediaAdaptor(0, media.recommendations!!, requireActivity())
bind.itemRecycler.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
parent.addView(bind.root)
}
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val cornerTop = ObjectAnimator.ofFloat(binding.root, "radius", 0f, 32f).setDuration(200)
val cornerNotTop = ObjectAnimator.ofFloat(binding.root, "radius", 32f, 0f).setDuration(200)
var cornered = true
cornerTop.start()
binding.mediaInfoScroll.setOnScrollChangeListener { v, _, _, _, _ ->
if (!v.canScrollVertically(-1)) {
if (!cornered) {
cornered = true
cornerTop.start()
}
} else {
if (cornered) {
cornered = false
cornerNotTop.start()
}
}
}
}
super.onViewCreated(view, null)
}
override fun onResume() {
binding.mediaInfoProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE
super.onResume()
}
override fun onDestroy() {
timer?.cancel()
super.onDestroy()
}
}

View file

@ -0,0 +1,267 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
import android.os.Bundle
import android.text.InputFilter.LengthFilter
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import ani.dantotsu.*
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.databinding.BottomSheetMediaListBinding
import ani.dantotsu.connections.mal.MAL
import com.google.android.material.switchmaterial.SwitchMaterial
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class MediaListDialogFragment : BottomSheetDialogFragment() {
private var _binding: BottomSheetMediaListBinding? = null
private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = BottomSheetMediaListBinding.inflate(inflater, container, false)
return binding.root
}
@SuppressLint("SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.mediaListContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight }
var media: Media?
val model: MediaDetailsViewModel by activityViewModels()
val scope = viewLifecycleOwner.lifecycleScope
model.getMedia().observe(this) { it ->
media = it
if (media != null) {
binding.mediaListProgressBar.visibility = View.GONE
binding.mediaListLayout.visibility = View.VISIBLE
val statuses: Array<String> = resources.getStringArray(R.array.status)
val statusStrings = if (media?.manga==null) resources.getStringArray(R.array.status_anime) else resources.getStringArray(R.array.status_manga)
val userStatus = if(media!!.userStatus != null) statusStrings[statuses.indexOf(media!!.userStatus)] else statusStrings[0]
binding.mediaListStatus.setText(userStatus)
binding.mediaListStatus.setAdapter(
ArrayAdapter(
requireContext(),
R.layout.item_dropdown,
statusStrings
)
)
var total: Int? = null
binding.mediaListProgress.setText(if (media!!.userProgress != null) media!!.userProgress.toString() else "")
if (media!!.anime != null) if (media!!.anime!!.totalEpisodes != null) {
total = media!!.anime!!.totalEpisodes!!;binding.mediaListProgress.filters =
arrayOf(
InputFilterMinMax(0.0, total.toDouble(), binding.mediaListStatus),
LengthFilter(total.toString().length)
)
} else if (media!!.manga != null) if (media!!.manga!!.totalChapters != null) {
total = media!!.manga!!.totalChapters!!;binding.mediaListProgress.filters =
arrayOf(
InputFilterMinMax(0.0, total.toDouble(), binding.mediaListStatus),
LengthFilter(total.toString().length)
)
}
binding.mediaListProgressLayout.suffixText = " / ${total ?: '?'}"
binding.mediaListProgressLayout.suffixTextView.updateLayoutParams {
height = ViewGroup.LayoutParams.MATCH_PARENT
}
binding.mediaListProgressLayout.suffixTextView.gravity = Gravity.CENTER
binding.mediaListScore.setText(
if (media!!.userScore != 0) media!!.userScore.div(
10.0
).toString() else ""
)
binding.mediaListScore.filters =
arrayOf(InputFilterMinMax(1.0, 10.0), LengthFilter(10.0.toString().length))
binding.mediaListScoreLayout.suffixTextView.updateLayoutParams {
height = ViewGroup.LayoutParams.MATCH_PARENT
}
binding.mediaListScoreLayout.suffixTextView.gravity = Gravity.CENTER
val start = DatePickerFragment(requireActivity(), media!!.userStartedAt)
val end = DatePickerFragment(requireActivity(), media!!.userCompletedAt)
binding.mediaListStart.setText(media!!.userStartedAt.toStringOrEmpty())
binding.mediaListStart.setOnClickListener {
tryWith(false) {
if (!start.dialog.isShowing) start.dialog.show()
}
}
binding.mediaListStart.setOnFocusChangeListener { _, b ->
tryWith(false) {
if (b && !start.dialog.isShowing) start.dialog.show()
}
}
binding.mediaListEnd.setText(media!!.userCompletedAt.toStringOrEmpty())
binding.mediaListEnd.setOnClickListener {
tryWith(false) {
if (!end.dialog.isShowing) end.dialog.show()
}
}
binding.mediaListEnd.setOnFocusChangeListener { _, b ->
tryWith(false) {
if (b && !end.dialog.isShowing) end.dialog.show()
}
}
start.dialog.setOnDismissListener { _binding?.mediaListStart?.setText(start.date.toStringOrEmpty()) }
end.dialog.setOnDismissListener { _binding?.mediaListEnd?.setText(end.date.toStringOrEmpty()) }
fun onComplete() {
binding.mediaListProgress.setText(total.toString())
if (start.date.year == null) {
start.date = FuzzyDate().getToday()
binding.mediaListStart.setText(start.date.toString())
}
end.date = FuzzyDate().getToday()
binding.mediaListEnd.setText(end.date.toString())
}
var startBackupDate: FuzzyDate? = null
var endBackupDate: FuzzyDate? = null
var progressBackup: String? = null
binding.mediaListStatus.setOnItemClickListener { _, _, i, _ ->
if (i == 2 && total != null) {
startBackupDate = start.date
endBackupDate = end.date
progressBackup = binding.mediaListProgress.text.toString()
onComplete()
} else {
if (progressBackup != null) binding.mediaListProgress.setText(progressBackup)
if (startBackupDate != null) {
binding.mediaListStart.setText(startBackupDate.toString())
start.date = startBackupDate!!
}
if (endBackupDate != null) {
binding.mediaListEnd.setText(endBackupDate.toString())
end.date = endBackupDate!!
}
}
}
binding.mediaListIncrement.setOnClickListener {
if (binding.mediaListStatus.text.toString() == statusStrings[0]) binding.mediaListStatus.setText(
statusStrings[1],
false
)
val init =
if (binding.mediaListProgress.text.toString() != "") binding.mediaListProgress.text.toString()
.toInt() else 0
if (init < (total ?: 5000)) binding.mediaListProgress.setText((init + 1).toString())
if (init + 1 == (total ?: 5000)) {
binding.mediaListStatus.setText(statusStrings[2], false)
onComplete()
}
}
binding.mediaListPrivate.isChecked = media?.isListPrivate ?: false
binding.mediaListPrivate.setOnCheckedChangeListener { _, checked ->
media?.isListPrivate = checked
}
media?.userRepeat?.apply {
binding.mediaListRewatch.setText(this.toString())
}
media?.notes?.apply {
binding.mediaListNotes.setText(this)
}
if (media?.inCustomListsOf?.isEmpty() != false)
binding.mediaListAddCustomList.apply {
(parent as? ViewGroup)?.removeView(this)
}
media?.inCustomListsOf?.forEach {
SwitchMaterial(requireContext()).apply {
isChecked = it.value
text = it.key
setOnCheckedChangeListener { _, isChecked ->
media?.inCustomListsOf?.put(it.key, isChecked)
}
binding.mediaListCustomListContainer.addView(this)
}
}
binding.mediaListSave.setOnClickListener {
scope.launch {
withContext(Dispatchers.IO) {
if (media != null) {
val progress = _binding?.mediaListProgress?.text.toString().toIntOrNull()
val score =
(_binding?.mediaListScore?.text.toString().toDoubleOrNull()?.times(10))?.toInt()
val status = statuses[statusStrings.indexOf(_binding?.mediaListStatus?.text.toString())]
val rewatch = _binding?.mediaListRewatch?.text?.toString()?.toIntOrNull()
val notes = _binding?.mediaListNotes?.text?.toString()
val startD = start.date
val endD = end.date
Anilist.mutation.editList(
media!!.id,
progress,
score,
rewatch,
notes,
status,
media?.isListPrivate ?: false,
startD,
endD,
media?.inCustomListsOf?.mapNotNull { if (it.value) it.key else null }
)
MAL.query.editList(
media!!.idMAL,
media!!.anime != null,
progress,
score,
status,
rewatch,
startD,
endD
)
}
}
Refresh.all()
snackString(getString(R.string.list_updated))
dismissAllowingStateLoss()
}
}
binding.mediaListDelete.setOnClickListener {
val id = media!!.userListId
if (id != null) {
scope.launch {
withContext(Dispatchers.IO) {
Anilist.mutation.deleteList(id)
MAL.query.deleteList(media?.anime!=null,media?.idMAL)
}
Refresh.all()
snackString(getString(R.string.deleted_from_list))
dismissAllowingStateLoss()
}
} else {
snackString(getString(R.string.no_list_id))
Refresh.all()
}
}
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View file

@ -0,0 +1,148 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
import android.os.Bundle
import android.text.InputFilter.LengthFilter
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import ani.dantotsu.*
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.databinding.BottomSheetMediaListSmallBinding
import ani.dantotsu.connections.mal.MAL
import ani.dantotsu.others.getSerialized
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.Serializable
class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
private lateinit var media: Media
companion object {
fun newInstance(m: Media): MediaListDialogSmallFragment =
MediaListDialogSmallFragment().apply {
arguments = Bundle().apply {
putSerializable("media", m as Serializable)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
media = it.getSerialized("media")!!
}
}
private var _binding: BottomSheetMediaListSmallBinding? = null
private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = BottomSheetMediaListSmallBinding.inflate(inflater, container, false)
return binding.root
}
@SuppressLint("SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.mediaListContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight }
val scope = viewLifecycleOwner.lifecycleScope
binding.mediaListProgressBar.visibility = View.GONE
binding.mediaListLayout.visibility = View.VISIBLE
val statuses: Array<String> = resources.getStringArray(R.array.status)
val statusStrings = if (media.manga==null) resources.getStringArray(R.array.status_anime) else resources.getStringArray(R.array.status_manga)
val userStatus = if(media.userStatus != null) statusStrings[statuses.indexOf(media.userStatus)] else statusStrings[0]
binding.mediaListStatus.setText(userStatus)
binding.mediaListStatus.setAdapter(
ArrayAdapter(
requireContext(),
R.layout.item_dropdown,
statusStrings
)
)
var total: Int? = null
binding.mediaListProgress.setText(if (media.userProgress != null) media.userProgress.toString() else "")
if (media.anime != null) if (media.anime!!.totalEpisodes != null) {
total = media.anime!!.totalEpisodes!!;binding.mediaListProgress.filters =
arrayOf(
InputFilterMinMax(0.0, total.toDouble(), binding.mediaListStatus),
LengthFilter(total.toString().length)
)
} else if (media.manga != null) if (media.manga!!.totalChapters != null) {
total = media.manga!!.totalChapters!!;binding.mediaListProgress.filters =
arrayOf(
InputFilterMinMax(0.0, total.toDouble(), binding.mediaListStatus),
LengthFilter(total.toString().length)
)
}
binding.mediaListProgressLayout.suffixText = " / ${total ?: '?'}"
binding.mediaListProgressLayout.suffixTextView.updateLayoutParams {
height = ViewGroup.LayoutParams.MATCH_PARENT
}
binding.mediaListProgressLayout.suffixTextView.gravity = Gravity.CENTER
binding.mediaListScore.setText(
if (media.userScore != 0) media.userScore.div(
10.0
).toString() else ""
)
binding.mediaListScore.filters =
arrayOf(InputFilterMinMax(1.0, 10.0), LengthFilter(10.0.toString().length))
binding.mediaListScoreLayout.suffixTextView.updateLayoutParams {
height = ViewGroup.LayoutParams.MATCH_PARENT
}
binding.mediaListScoreLayout.suffixTextView.gravity = Gravity.CENTER
binding.mediaListIncrement.setOnClickListener {
if (binding.mediaListStatus.text.toString() == statusStrings[0]) binding.mediaListStatus.setText(
statusStrings[1],
false
)
val init =
if (binding.mediaListProgress.text.toString() != "") binding.mediaListProgress.text.toString()
.toInt() else 0
if (init < (total ?: 5000)) binding.mediaListProgress.setText((init + 1).toString())
if (init + 1 == (total ?: 5000)) {
binding.mediaListStatus.setText(statusStrings[2], false)
}
}
binding.mediaListPrivate.isChecked = media.isListPrivate
binding.mediaListPrivate.setOnCheckedChangeListener { _, checked ->
media.isListPrivate = checked
}
binding.mediaListSave.setOnClickListener {
scope.launch {
withContext(Dispatchers.IO) {
withContext(Dispatchers.IO) {
val progress = _binding?.mediaListProgress?.text.toString().toIntOrNull()
val score = (_binding?.mediaListScore?.text.toString().toDoubleOrNull()?.times(10))?.toInt()
val status = statuses[statusStrings.indexOf(_binding?.mediaListStatus?.text.toString())]
Anilist.mutation.editList(media.id, progress, score, null, null, status, media.isListPrivate)
MAL.query.editList(media.idMAL, media.anime != null, progress, score, status)
}
}
Refresh.all()
snackString(getString(R.string.list_updated))
dismissAllowingStateLoss()
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View file

@ -0,0 +1,48 @@
package ani.dantotsu.media
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import ani.dantotsu.connections.anilist.Anilist
import java.text.DateFormat
import java.util.*
class OtherDetailsViewModel : ViewModel() {
private val character: MutableLiveData<Character> = MutableLiveData(null)
fun getCharacter(): LiveData<Character> = character
suspend fun loadCharacter(m: Character) {
if (character.value == null) character.postValue(Anilist.query.getCharacterDetails(m))
}
private val studio: MutableLiveData<Studio> = MutableLiveData(null)
fun getStudio(): LiveData<Studio> = studio
suspend fun loadStudio(m: Studio) {
if (studio.value == null) studio.postValue(Anilist.query.getStudioDetails(m))
}
private val author: MutableLiveData<Author> = MutableLiveData(null)
fun getAuthor(): LiveData<Author> = author
suspend fun loadAuthor(m: Author) {
if (author.value == null) author.postValue(Anilist.query.getAuthorDetails(m))
}
private val calendar: MutableLiveData<Map<String,MutableList<Media>>> = MutableLiveData(null)
fun getCalendar(): LiveData<Map<String,MutableList<Media>>> = calendar
suspend fun loadCalendar() {
val curr = System.currentTimeMillis()/1000
val res = Anilist.query.recentlyUpdated(false,curr-86400,curr+(86400*6))
val df = DateFormat.getDateInstance(DateFormat.FULL)
val map = mutableMapOf<String,MutableList<Media>>()
val idMap = mutableMapOf<String,MutableList<Int>>()
res?.forEach {
val v = it.relation?.split(",")?.map { i-> i.toLong() }!!
val dateInfo = df.format(Date(v[1]*1000))
val list = map.getOrPut(dateInfo) { mutableListOf() }
val idList = idMap.getOrPut(dateInfo) { mutableListOf() }
it.relation = "Episode ${v[0]}"
if(!idList.contains(it.id)) {
idList.add(it.id)
list.add(it)
}
}
calendar.postValue(map)
}
}

View file

@ -0,0 +1,59 @@
package ani.dantotsu.media
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.view.GestureDetector
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.ViewGroup
import android.widget.ProgressBar
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.MutableLiveData
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.GesturesListener
import ani.dantotsu.R
import ani.dantotsu.currContext
import ani.dantotsu.databinding.ItemProgressbarBinding
import ani.dantotsu.snackString
class ProgressAdapter(private val horizontal: Boolean = true, searched: Boolean) :
RecyclerView.Adapter<ProgressAdapter.ProgressViewHolder>() {
val ready = MutableLiveData(searched)
var bar: ProgressBar? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProgressViewHolder {
val binding = ItemProgressbarBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ProgressViewHolder(binding)
}
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
override fun onBindViewHolder(holder: ProgressViewHolder, position: Int) {
val progressBar = holder.binding.root
bar = progressBar
val doubleClickDetector = GestureDetector(progressBar.context, object : GesturesListener() {
override fun onDoubleClick(event: MotionEvent) {
snackString(currContext()?.getString(R.string.cant_wait))
ObjectAnimator.ofFloat(progressBar, "translationX", progressBar.translationX, progressBar.translationX + 100f)
.setDuration(300).start()
}
override fun onScrollYClick(y: Float) {}
override fun onSingleClick(event: MotionEvent) {}
})
progressBar.setOnTouchListener { v, event ->
doubleClickDetector.onTouchEvent(event)
v.performClick()
true
}
if (ready.value == false) {
ready.postValue(true)
}
}
override fun getItemCount(): Int = 1
inner class ProgressViewHolder(val binding: ItemProgressbarBinding) : RecyclerView.ViewHolder(binding.root) {
init {
itemView.updateLayoutParams { if (horizontal) width = -1 else height = -1 }
}
}
}

View file

@ -0,0 +1,195 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
import android.os.Bundle
import android.os.Parcelable
import android.view.View
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.updatePaddingRelative
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.*
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistSearch
import ani.dantotsu.connections.anilist.SearchResults
import ani.dantotsu.databinding.ActivitySearchBinding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.*
class SearchActivity : AppCompatActivity() {
private lateinit var binding: ActivitySearchBinding
private val scope = lifecycleScope
val model: AnilistSearch by viewModels()
var style: Int = 0
private var screenWidth: Float = 0f
private lateinit var mediaAdaptor: MediaAdaptor
private lateinit var progressAdapter: ProgressAdapter
private lateinit var concatAdapter: ConcatAdapter
lateinit var result: SearchResults
lateinit var updateChips: (()->Unit)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySearchBinding.inflate(layoutInflater)
setContentView(binding.root)
initActivity(this)
screenWidth = resources.displayMetrics.run { widthPixels / density }
binding.searchRecyclerView.updatePaddingRelative(
top = statusBarHeight,
bottom = navBarHeight + 80f.px
)
style = loadData<Int>("searchStyle") ?: 0
var listOnly: Boolean? = intent.getBooleanExtra("listOnly", false)
if (!listOnly!!) listOnly = null
val notSet = model.notSet
if (model.notSet) {
model.notSet = false
model.searchResults = SearchResults(
intent.getStringExtra("type") ?: "ANIME",
isAdult = if (Anilist.adult) intent.getBooleanExtra("hentai", false) else false,
onList = listOnly,
genres = intent.getStringExtra("genre")?.let { mutableListOf(it) },
tags = intent.getStringExtra("tag")?.let { mutableListOf(it) },
sort = intent.getStringExtra("sortBy"),
season = intent.getStringExtra("season"),
seasonYear = intent.getStringExtra("seasonYear")?.toIntOrNull(),
results = mutableListOf(),
hasNextPage = false
)
}
result = model.searchResults
progressAdapter = ProgressAdapter(searched = model.searched)
mediaAdaptor = MediaAdaptor(style, model.searchResults.results, this, matchParent = true)
val headerAdaptor = SearchAdapter(this)
val gridSize = (screenWidth / 124f).toInt()
val gridLayoutManager = GridLayoutManager(this, gridSize)
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return when (position) {
0 -> gridSize
concatAdapter.itemCount - 1 -> gridSize
else -> when (style) {
0 -> 1
else -> gridSize
}
}
}
}
concatAdapter = ConcatAdapter(headerAdaptor, mediaAdaptor, progressAdapter)
binding.searchRecyclerView.layoutManager = gridLayoutManager
binding.searchRecyclerView.adapter = concatAdapter
binding.searchRecyclerView.addOnScrollListener(object :
RecyclerView.OnScrollListener() {
override fun onScrolled(v: RecyclerView, dx: Int, dy: Int) {
if (!v.canScrollVertically(1)) {
if (model.searchResults.hasNextPage && model.searchResults.results.isNotEmpty() && !loading) {
scope.launch(Dispatchers.IO) {
model.loadNextPage(model.searchResults)
}
}
}
super.onScrolled(v, dx, dy)
}
})
model.getSearch().observe(this) {
if (it != null) {
model.searchResults.apply {
onList = it.onList
isAdult = it.isAdult
perPage = it.perPage
search = it.search
sort = it.sort
genres = it.genres
excludedGenres = it.excludedGenres
excludedTags = it.excludedTags
tags = it.tags
season = it.season
seasonYear = it.seasonYear
format = it.format
page = it.page
hasNextPage = it.hasNextPage
}
val prev = model.searchResults.results.size
model.searchResults.results.addAll(it.results)
mediaAdaptor.notifyItemRangeInserted(prev, it.results.size)
progressAdapter.bar?.visibility = if (it.hasNextPage) View.VISIBLE else View.GONE
}
}
progressAdapter.ready.observe(this) {
if (it == true) {
if (!notSet) {
if (!model.searched) {
model.searched = true
headerAdaptor.search?.run()
}
} else
headerAdaptor.requestFocus?.run()
if(intent.getBooleanExtra("search",false)) search()
}
}
}
private var searchTimer = Timer()
private var loading = false
fun search() {
val size = model.searchResults.results.size
model.searchResults.results.clear()
runOnUiThread {
mediaAdaptor.notifyItemRangeRemoved(0, size)
}
progressAdapter.bar?.visibility = View.VISIBLE
searchTimer.cancel()
searchTimer.purge()
val timerTask: TimerTask = object : TimerTask() {
override fun run() {
scope.launch(Dispatchers.IO) {
loading = true
model.loadSearch(result)
loading = false
}
}
}
searchTimer = Timer()
searchTimer.schedule(timerTask, 500)
}
@SuppressLint("NotifyDataSetChanged")
fun recycler() {
mediaAdaptor.type = style
mediaAdaptor.notifyDataSetChanged()
}
var state: Parcelable? = null
override fun onPause() {
super.onPause()
state = binding.searchRecyclerView.layoutManager?.onSaveInstanceState()
}
override fun onResume() {
super.onResume()
binding.searchRecyclerView.layoutManager?.onRestoreInstanceState(state)
}
}

View file

@ -0,0 +1,200 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.HORIZONTAL
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.databinding.ItemSearchHeaderBinding
import ani.dantotsu.saveData
import com.google.android.material.checkbox.MaterialCheckBox.*
class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter<SearchAdapter.SearchHeaderViewHolder>() {
private val itemViewType = 6969
var search: Runnable? = null
var requestFocus: Runnable? = null
private var textWatcher: TextWatcher? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchHeaderViewHolder {
val binding = ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return SearchHeaderViewHolder(binding)
}
@SuppressLint("ClickableViewAccessibility")
override fun onBindViewHolder(holder: SearchHeaderViewHolder, position: Int) {
val binding = holder.binding
val imm: InputMethodManager = activity.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager
when (activity.style) {
0 -> {
binding.searchResultGrid.alpha = 1f
binding.searchResultList.alpha = 0.33f
}
1 -> {
binding.searchResultList.alpha = 1f
binding.searchResultGrid.alpha = 0.33f
}
}
binding.searchBar.hint = activity.result.type
var adult = activity.result.isAdult
var listOnly = activity.result.onList
binding.searchBarText.removeTextChangedListener(textWatcher)
binding.searchBarText.setText(activity.result.search)
binding.searchAdultCheck.isChecked = adult
binding.searchList.isChecked = listOnly == true
binding.searchChipRecycler.adapter = SearchChipAdapter(activity).also {
activity.updateChips = { it.update() }
}
binding.searchChipRecycler.layoutManager = LinearLayoutManager(binding.root.context, HORIZONTAL, false)
binding.searchFilter.setOnClickListener {
SearchFilterBottomDialog.newInstance().show(activity.supportFragmentManager, "dialog")
}
fun searchTitle() {
activity.result.apply {
search = if (binding.searchBarText.text.toString() != "") binding.searchBarText.text.toString() else null
onList = listOnly
isAdult = adult
}
activity.search()
}
textWatcher = object : TextWatcher {
override fun afterTextChanged(s: Editable) {}
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
searchTitle()
}
}
binding.searchBarText.addTextChangedListener(textWatcher)
binding.searchBarText.setOnEditorActionListener { _, actionId, _ ->
return@setOnEditorActionListener when (actionId) {
EditorInfo.IME_ACTION_SEARCH -> {
searchTitle()
binding.searchBarText.clearFocus()
imm.hideSoftInputFromWindow(binding.searchBarText.windowToken, 0)
true
}
else -> false
}
}
binding.searchBar.setEndIconOnClickListener { searchTitle() }
binding.searchResultGrid.setOnClickListener {
it.alpha = 1f
binding.searchResultList.alpha = 0.33f
activity.style = 0
saveData("searchStyle", 0)
activity.recycler()
}
binding.searchResultList.setOnClickListener {
it.alpha = 1f
binding.searchResultGrid.alpha = 0.33f
activity.style = 1
saveData("searchStyle", 1)
activity.recycler()
}
if (Anilist.adult) {
binding.searchAdultCheck.visibility = View.VISIBLE
binding.searchAdultCheck.isChecked = adult
binding.searchAdultCheck.setOnCheckedChangeListener { _, b ->
adult = b
searchTitle()
}
} else binding.searchAdultCheck.visibility = View.GONE
binding.searchList.apply {
if (Anilist.userid != null) {
visibility = View.VISIBLE
checkedState = when(listOnly){
null -> STATE_UNCHECKED
true -> STATE_CHECKED
false -> STATE_INDETERMINATE
}
addOnCheckedStateChangedListener { _, state ->
listOnly = when (state) {
STATE_CHECKED -> true
STATE_INDETERMINATE -> false
STATE_UNCHECKED -> null
else -> null
}
}
setOnTouchListener { _, event ->
(event.actionMasked == MotionEvent.ACTION_DOWN).also {
if (it) checkedState = (checkedState + 1) % 3
searchTitle()
}
}
} else visibility = View.GONE
}
search = Runnable { searchTitle() }
requestFocus = Runnable { binding.searchBarText.requestFocus() }
}
override fun getItemCount(): Int = 1
inner class SearchHeaderViewHolder(val binding: ItemSearchHeaderBinding) : RecyclerView.ViewHolder(binding.root)
override fun getItemViewType(position: Int): Int {
return itemViewType
}
class SearchChipAdapter(val activity: SearchActivity) : RecyclerView.Adapter<SearchChipAdapter.SearchChipViewHolder>() {
private var chips = activity.result.toChipList()
inner class SearchChipViewHolder(val binding: ItemChipBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchChipViewHolder {
val binding = ItemChipBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return SearchChipViewHolder(binding)
}
override fun onBindViewHolder(holder: SearchChipViewHolder, position: Int) {
val chip = chips[position]
holder.binding.root.apply {
text = chip.text
setOnClickListener {
activity.result.removeChip(chip)
update()
activity.search()
}
}
}
@SuppressLint("NotifyDataSetChanged")
fun update() {
chips = activity.result.toChipList()
notifyDataSetChanged()
}
override fun getItemCount(): Int = chips.size
}
}

View file

@ -0,0 +1,191 @@
package ani.dantotsu.media
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.ViewGroup
import android.widget.ArrayAdapter
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.HORIZONTAL
import androidx.recyclerview.widget.RecyclerView.VERTICAL
import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.databinding.BottomSheetSearchFilterBinding
import ani.dantotsu.databinding.ItemChipBinding
import com.google.android.material.chip.Chip
class SearchFilterBottomDialog() : BottomSheetDialogFragment() {
private var _binding: BottomSheetSearchFilterBinding? = null
private val binding get() = _binding!!
private lateinit var activity: SearchActivity
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = BottomSheetSearchFilterBinding.inflate(inflater, container, false)
return binding.root
}
private var selectedGenres = mutableListOf<String>()
private var exGenres = mutableListOf<String>()
private var selectedTags = mutableListOf<String>()
private var exTags = mutableListOf<String>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
activity = requireActivity() as SearchActivity
selectedGenres = activity.result.genres ?: mutableListOf()
exGenres = activity.result.excludedGenres ?: mutableListOf()
selectedTags = activity.result.tags ?: mutableListOf()
exTags = activity.result.excludedTags ?: mutableListOf()
binding.searchFilterApply.setOnClickListener {
activity.result.apply {
format = binding.searchFormat.text.toString().ifBlank { null }
sort = binding.searchSortBy.text.toString().ifBlank { null }
?.let { Anilist.sortBy[resources.getStringArray(R.array.sort_by).indexOf(it)] }
season = binding.searchSeason.text.toString().ifBlank { null }
seasonYear = binding.searchYear.text.toString().toIntOrNull()
genres = selectedGenres
tags = selectedTags
excludedGenres = exGenres
excludedTags = exTags
}
activity.updateChips.invoke()
activity.search()
dismiss()
}
binding.searchFilterCancel.setOnClickListener {
dismiss()
}
binding.searchSortBy.setText(activity.result.sort?.let {
resources.getStringArray(R.array.sort_by)[Anilist.sortBy.indexOf(it)]
})
binding.searchSortBy.setAdapter(
ArrayAdapter(
binding.root.context,
R.layout.item_dropdown,
resources.getStringArray(R.array.sort_by)
)
)
binding.searchFormat.setText(activity.result.format)
binding.searchFormat.setAdapter(
ArrayAdapter(
binding.root.context,
R.layout.item_dropdown,
(if (activity.result.type == "ANIME") Anilist.anime_formats else Anilist.manga_formats).toTypedArray()
)
)
if (activity.result.type == "MANGA") binding.searchSeasonYearCont.visibility = GONE
else {
binding.searchSeason.setText(activity.result.season)
binding.searchSeason.setAdapter(
ArrayAdapter(
binding.root.context,
R.layout.item_dropdown,
Anilist.seasons.toTypedArray()
)
)
binding.searchYear.setText(activity.result.seasonYear?.toString())
binding.searchYear.setAdapter(
ArrayAdapter(
binding.root.context,
R.layout.item_dropdown,
(1970 until 2024).map { it.toString() }.reversed().toTypedArray()
)
)
}
binding.searchFilterGenres.adapter = FilterChipAdapter(Anilist.genres ?: listOf()) { chip ->
val genre = chip.text.toString()
chip.isChecked = selectedGenres.contains(genre)
chip.isCloseIconVisible = exGenres.contains(genre)
chip.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
chip.isCloseIconVisible = false
exGenres.remove(genre)
selectedGenres.add(genre)
} else
selectedGenres.remove(genre)
}
chip.setOnLongClickListener {
chip.isChecked = false
chip.isCloseIconVisible = true
exGenres.add(genre)
}
}
binding.searchGenresGrid.setOnCheckedChangeListener { _, isChecked ->
binding.searchFilterGenres.layoutManager =
if (!isChecked) LinearLayoutManager(binding.root.context, HORIZONTAL, false)
else GridLayoutManager(binding.root.context, 2, VERTICAL, false)
}
binding.searchGenresGrid.isChecked = false
binding.searchFilterTags.adapter = FilterChipAdapter(Anilist.tags?.get(activity.result.isAdult) ?: listOf()) { chip ->
val tag = chip.text.toString()
chip.isChecked = selectedTags.contains(tag)
chip.isCloseIconVisible = exTags.contains(tag)
chip.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
chip.isCloseIconVisible = false
exTags.remove(tag)
selectedTags.add(tag)
} else
selectedTags.remove(tag)
}
chip.setOnLongClickListener {
chip.isChecked = false
chip.isCloseIconVisible = true
exTags.add(tag)
}
}
binding.searchTagsGrid.setOnCheckedChangeListener { _, isChecked ->
binding.searchFilterTags.layoutManager =
if (!isChecked) LinearLayoutManager(binding.root.context, HORIZONTAL, false)
else GridLayoutManager(binding.root.context, 2, VERTICAL, false)
}
binding.searchTagsGrid.isChecked = false
}
class FilterChipAdapter(val list: List<String>, private val perform: ((Chip) -> Unit)) :
RecyclerView.Adapter<FilterChipAdapter.SearchChipViewHolder>() {
inner class SearchChipViewHolder(val binding: ItemChipBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchChipViewHolder {
val binding = ItemChipBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return SearchChipViewHolder(binding)
}
override fun onBindViewHolder(holder: SearchChipViewHolder, position: Int) {
val title = list[position]
holder.setIsRecyclable(false)
holder.binding.root.apply {
text = title
isCheckable = true
perform.invoke(this)
}
}
override fun getItemCount(): Int = list.size
}
override fun onDestroy() {
_binding = null
super.onDestroy()
}
companion object {
fun newInstance() = SearchFilterBottomDialog()
}
}

View file

@ -0,0 +1,15 @@
package ani.dantotsu.media
import java.io.Serializable
data class Selected(
var window: Int = 0,
var recyclerStyle: Int? = null,
var recyclerReversed: Boolean = false,
var chip: Int = 0,
var source: Int = 0,
var preferDub: Boolean = false,
var server: String? = null,
var video: Int = 0,
var latest: Float = 0f,
) : Serializable

View file

@ -0,0 +1,10 @@
package ani.dantotsu.media
import java.io.Serializable
data class Source(
val link: String,
val name: String,
val cover: String,
val headers: MutableMap<String, String>? = null
) : Serializable

View file

@ -0,0 +1,51 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.databinding.ItemCharacterBinding
import ani.dantotsu.loadImage
import ani.dantotsu.parsers.ShowResponse
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
abstract class SourceAdapter(
private val sources: List<ShowResponse>,
private val dialogFragment: SourceSearchDialogFragment,
private val scope: CoroutineScope
) : RecyclerView.Adapter<SourceAdapter.SourceViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SourceViewHolder {
val binding = ItemCharacterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return SourceViewHolder(binding)
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: SourceViewHolder, position: Int) {
val binding = holder.binding
val character = sources[position]
binding.itemCompactImage.loadImage(character.coverUrl, 200)
binding.itemCompactTitle.isSelected = true
binding.itemCompactTitle.text = character.name
}
override fun getItemCount(): Int = sources.size
abstract suspend fun onItemClick(source: ShowResponse)
inner class SourceViewHolder(val binding: ItemCharacterBinding) : RecyclerView.ViewHolder(binding.root) {
init {
itemView.setOnClickListener {
dialogFragment.dismiss()
scope.launch(Dispatchers.IO) { onItemClick(sources[bindingAdapterPosition]) }
}
var a = true
itemView.setOnLongClickListener {
a = !a
binding.itemCompactTitle.isSingleLine = a
true
}
}
}
}

View file

@ -0,0 +1,121 @@
package ani.dantotsu.media
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import androidx.core.math.MathUtils.clamp
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.media.anime.AnimeSourceAdapter
import ani.dantotsu.databinding.BottomSheetSourceSearchBinding
import ani.dantotsu.media.manga.MangaSourceAdapter
import ani.dantotsu.navBarHeight
import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.HAnimeSources
import ani.dantotsu.parsers.HMangaSources
import ani.dantotsu.parsers.MangaSources
import ani.dantotsu.px
import ani.dantotsu.tryWithSuspend
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class SourceSearchDialogFragment : BottomSheetDialogFragment() {
private var _binding: BottomSheetSourceSearchBinding? = null
private val binding get() = _binding!!
val model: MediaDetailsViewModel by activityViewModels()
private var searched = false
var anime = true
var i: Int? = null
var id: Int? = null
var media: Media? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = BottomSheetSourceSearchBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.mediaListContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight }
val scope = requireActivity().lifecycleScope
val imm = requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
model.getMedia().observe(viewLifecycleOwner) {
media = it
if (media != null) {
binding.mediaListProgressBar.visibility = View.GONE
binding.mediaListLayout.visibility = View.VISIBLE
binding.searchRecyclerView.visibility = View.GONE
binding.searchProgress.visibility = View.VISIBLE
i = media!!.selected!!.source
val source = if (media!!.anime != null) {
(if (!media!!.isAdult) AnimeSources else HAnimeSources)[i!!]
} else {
anime = false
(if (media!!.isAdult) HMangaSources else MangaSources)[i!!]
}
fun search() {
binding.searchBarText.clearFocus()
imm.hideSoftInputFromWindow(binding.searchBarText.windowToken, 0)
scope.launch {
model.responses.postValue(
withContext(Dispatchers.IO) {
tryWithSuspend {
source.search(binding.searchBarText.text.toString())
}
}
)
}
}
binding.searchSourceTitle.text = source.name
binding.searchBarText.setText(media!!.mangaName())
binding.searchBarText.setOnEditorActionListener { _, actionId, _ ->
return@setOnEditorActionListener when (actionId) {
EditorInfo.IME_ACTION_SEARCH -> {
search()
true
}
else -> false
}
}
binding.searchBar.setEndIconOnClickListener { search() }
if (!searched) search()
searched = true
model.responses.observe(viewLifecycleOwner) { j ->
if (j != null) {
binding.searchRecyclerView.visibility = View.VISIBLE
binding.searchProgress.visibility = View.GONE
binding.searchRecyclerView.adapter =
if (anime) AnimeSourceAdapter(j, model, i!!, media!!.id, this, scope)
else MangaSourceAdapter(j, model, i!!, media!!.id, this, scope)
binding.searchRecyclerView.layoutManager = GridLayoutManager(
requireActivity(),
clamp(requireActivity().resources.displayMetrics.widthPixels / 124f.px, 1, 4)
)
}
}
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun dismiss() {
model.responses.value = null
super.dismiss()
}
}

View file

@ -0,0 +1,9 @@
package ani.dantotsu.media
import java.io.Serializable
data class Studio(
val id: String,
val name: String,
var yearMedia: MutableMap<String, ArrayList<Media>>? = null
) : Serializable

View file

@ -0,0 +1,111 @@
package ani.dantotsu.media
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager
import ani.dantotsu.*
import ani.dantotsu.databinding.ActivityStudioBinding
import ani.dantotsu.others.getSerialized
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class StudioActivity : AppCompatActivity() {
private lateinit var binding: ActivityStudioBinding
private val scope = lifecycleScope
private val model: OtherDetailsViewModel by viewModels()
private var studio: Studio? = null
private var loaded = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityStudioBinding.inflate(layoutInflater)
setContentView(binding.root)
initActivity(this)
this.window.statusBarColor = ContextCompat.getColor(this, R.color.nav_bg)
val screenWidth = resources.displayMetrics.run { widthPixels / density }
binding.root.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
binding.studioRecycler.updatePadding(bottom = 64f.px + navBarHeight)
binding.studioTitle.isSelected = true
studio = intent.getSerialized("studio")
binding.studioTitle.text = studio?.name
binding.studioClose.setOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
model.getStudio().observe(this) {
if (it != null) {
studio = it
loaded = true
binding.studioProgressBar.visibility = View.GONE
binding.studioRecycler.visibility = View.VISIBLE
val titlePosition = arrayListOf<Int>()
val concatAdapter = ConcatAdapter()
val map = studio!!.yearMedia ?: return@observe
val keys = map.keys.toTypedArray()
var pos = 0
val gridSize = (screenWidth / 124f).toInt()
val gridLayoutManager = GridLayoutManager(this, gridSize)
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return when (position in titlePosition) {
true -> gridSize
else -> 1
}
}
}
for (i in keys.indices) {
val medias = map[keys[i]]!!
val empty = if (medias.size >= 4) medias.size % 4 else 4 - medias.size
titlePosition.add(pos)
pos += (empty + medias.size + 1)
concatAdapter.addAdapter(TitleAdapter("${keys[i]} (${medias.size})"))
concatAdapter.addAdapter(MediaAdaptor(0, medias, this, true))
concatAdapter.addAdapter(EmptyAdapter(empty))
}
binding.studioRecycler.adapter = concatAdapter
binding.studioRecycler.layoutManager = gridLayoutManager
}
}
val live = Refresh.activity.getOrPut(this.hashCode()) { MutableLiveData(true) }
live.observe(this) {
if (it) {
scope.launch {
if (studio != null)
withContext(Dispatchers.IO) { model.loadStudio(studio!!) }
live.postValue(false)
}
}
}
}
override fun onDestroy() {
if (Refresh.activity.containsKey(this.hashCode())) {
Refresh.activity.remove(this.hashCode())
}
super.onDestroy()
}
override fun onResume() {
binding.studioProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE
super.onResume()
}
}

View file

@ -0,0 +1,21 @@
package ani.dantotsu.media
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.databinding.ItemTitleBinding
class TitleAdapter(private val text: String) : RecyclerView.Adapter<TitleAdapter.TitleViewHolder>() {
inner class TitleViewHolder(val binding: ItemTitleBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TitleViewHolder {
val binding = ItemTitleBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return TitleViewHolder(binding)
}
override fun onBindViewHolder(holder: TitleViewHolder, position: Int) {
holder.binding.itemTitle.text = text
}
override fun getItemCount(): Int = 1
}

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

View file

@ -0,0 +1,12 @@
package ani.dantotsu.media.manga
import ani.dantotsu.media.Author
import java.io.Serializable
data class Manga(
var totalChapters: Int? = null,
var selectedChapter: String? = null,
var chapters: MutableMap<String, MangaChapter>? = null,
var slug: String? = null,
var author: Author?=null,
) : Serializable

View file

@ -0,0 +1,30 @@
package ani.dantotsu.media.manga
import ani.dantotsu.parsers.MangaChapter
import ani.dantotsu.parsers.MangaImage
import java.io.Serializable
import kotlin.math.floor
data class MangaChapter(
val number: String,
var link: String,
var title: String? = null,
var description: String? = null,
) : Serializable {
constructor(chapter: MangaChapter) : this(chapter.number, chapter.link, chapter.title, chapter.description)
private val images = mutableListOf<MangaImage>()
fun images(): List<MangaImage> = images
fun addImages(image: List<MangaImage>) {
if (images.isNotEmpty()) return
image.forEach { images.add(it) }
(0..floor((images.size.toFloat() - 1f) / 2).toInt()).forEach {
val i = it * 2
dualPages.add(images[i] to images.getOrNull(i + 1))
}
}
private val dualPages = mutableListOf<Pair<MangaImage, MangaImage?>>()
fun dualPages(): List<Pair<MangaImage, MangaImage?>> = dualPages
}

View file

@ -0,0 +1,116 @@
package ani.dantotsu.media.manga
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.databinding.ItemChapterListBinding
import ani.dantotsu.databinding.ItemEpisodeCompactBinding
import ani.dantotsu.media.Media
import ani.dantotsu.setAnimation
import ani.dantotsu.connections.updateProgress
class MangaChapterAdapter(
private var type: Int,
private val media: Media,
private val fragment: MangaReadFragment,
var arr: ArrayList<MangaChapter> = arrayListOf(),
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
1 -> ChapterCompactViewHolder(
ItemEpisodeCompactBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
0 -> ChapterListViewHolder(ItemChapterListBinding.inflate(LayoutInflater.from(parent.context), parent, false))
else -> throw IllegalArgumentException()
}
}
override fun getItemViewType(position: Int): Int {
return type
}
override fun getItemCount(): Int = arr.size
inner class ChapterCompactViewHolder(val binding: ItemEpisodeCompactBinding) : RecyclerView.ViewHolder(binding.root) {
init {
itemView.setOnClickListener {
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size)
fragment.onMangaChapterClick(arr[bindingAdapterPosition].number)
}
}
}
inner class ChapterListViewHolder(val binding: ItemChapterListBinding) : RecyclerView.ViewHolder(binding.root) {
init {
itemView.setOnClickListener {
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size)
fragment.onMangaChapterClick(arr[bindingAdapterPosition].number)
}
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is ChapterCompactViewHolder -> {
val binding = holder.binding
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
val ep = arr[position]
binding.itemEpisodeNumber.text = ep.number
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
}
}
}
}
is ChapterListViewHolder -> {
val binding = holder.binding
val ep = arr[position]
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
binding.itemChapterNumber.text = ep.number
if (!ep.title.isNullOrEmpty()) {
binding.itemChapterTitle.text = ep.title
binding.itemChapterTitle.setOnLongClickListener {
binding.itemChapterTitle.maxLines.apply {
binding.itemChapterTitle.maxLines = if (this == 1) 3 else 1
}
true
}
binding.itemChapterTitle.visibility = View.VISIBLE
} else binding.itemChapterTitle.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.root.setOnLongClickListener {
updateProgress(media, ep.number)
true
}
}
} else {
binding.itemEpisodeViewedCover.visibility = View.GONE
binding.itemEpisodeViewed.visibility = View.GONE
}
}
}
}
fun updateType(t: Int) {
type = t
}
}

View file

@ -0,0 +1,226 @@
package ani.dantotsu.media.manga
import android.annotation.SuppressLint
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.media.anime.handleProgress
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.MangaReadSources
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 MangaReadAdapter(
private val media: Media,
private val fragment: MangaReadFragment,
private val mangaReadSources: MangaReadSources
) : RecyclerView.Adapter<MangaReadAdapter.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
binding.sourceTitle.setText(R.string.chaps)
//Wrong Title
binding.animeSourceSearch.setOnClickListener {
SourceSearchDialogFragment().show(fragment.requireActivity().supportFragmentManager, null)
}
//Source Selection
val source = media.selected!!.source.let { if (it >= mangaReadSources.names.size) 0 else it }
if (mangaReadSources.names.isNotEmpty() && source in 0 until mangaReadSources.names.size) {
binding.animeSource.setText(mangaReadSources.names[source])
mangaReadSources[source].apply {
binding.animeSourceTitle.text = showUserText
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
}
}
binding.animeSource.setAdapter(ArrayAdapter(fragment.requireContext(), R.layout.item_dropdown, mangaReadSources.names))
binding.animeSourceTitle.isSelected = true
binding.animeSource.setOnItemClickListener { _, _, i, _ ->
fragment.onSourceChange(i).apply {
binding.animeSourceTitle.text = showUserText
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
}
subscribeButton(false)
fragment.loadChapters(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
binding.animeSourceGrid.visibility = View.GONE
var reversed = media.selected!!.recyclerReversed
var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.mangaDefaultView
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.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.animeSourceCompact.setOnClickListener {
selected(it as ImageView)
style = 1
fragment.onIconPressed(style, reversed)
}
//Chapter Handling
handleChapters()
}
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 handleChapters() {
val binding = _binding
if (binding != null) {
if (media.manga?.chapters != null) {
val chapters = media.manga.chapters!!.keys.toTypedArray()
val anilistEp = (media.userProgress ?: 0).plus(1)
val appEp = loadData<String>("${media.id}_current_chp")?.toIntOrNull() ?: 1
var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString()
if (chapters.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 > 0.8f) {
val e = chapters.indexOf(continueEp)
if (e != -1 && e + 1 < chapters.size) {
continueEp = chapters[e + 1]
}
}
val ep = media.manga.chapters!![continueEp]!!
binding.itemEpisodeImage.loadImage(media.banner ?: media.cover)
binding.animeSourceContinueText.text =
currActivity()!!.getString(R.string.continue_chapter) + "${ep.number}${if (!ep.title.isNullOrEmpty()) "\n${ep.title}" else ""}"
binding.animeSourceContinue.setOnClickListener {
fragment.onMangaChapterClick(continueEp)
}
if (fragment.continueEp) {
if ((binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams).weight < 0.8f) {
binding.animeSourceContinue.performClick()
fragment.continueEp = false
}
}
} else {
binding.animeSourceContinue.visibility = View.GONE
}
binding.animeSourceProgressBar.visibility = View.GONE
if (media.manga.chapters!!.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)
}

View file

@ -0,0 +1,278 @@
package ani.dantotsu.media.manga
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.clamp
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.manga.mangareader.ChapterLoaderDialog
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.parsers.HMangaSources
import ani.dantotsu.parsers.MangaParser
import ani.dantotsu.parsers.MangaSources
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.subcriptions.Notifications
import ani.dantotsu.subcriptions.Notifications.Group.MANGA_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.launch
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.roundToInt
open class MangaReadFragment : 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: MangaReadAdapter
private lateinit var chapterAdapter: MangaChapterAdapter
var screenWidth = 0f
private var progress = View.VISIBLE
var continueEp: Boolean = false
var loaded = false
val uiSettings = loadData("ui_settings", toast = false) ?: UserInterfaceSettings().apply { saveData("ui_settings", this) }
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))
val gridLayoutManager = GridLayoutManager(requireContext(), maxGridSize)
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
val style = chapterAdapter.getItemViewType(position)
return when (position) {
0 -> maxGridSize
else -> when (style) {
0 -> maxGridSize
1 -> 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
progress = View.GONE
binding.mediaInfoProgressBar.visibility = progress
if (media.format == "MANGA" || media.format == "ONE SHOT") {
media.selected = model.loadSelected(media)
subscribed = SubscriptionHelper.getSubscriptions(requireContext()).containsKey(media.id)
style = media.selected!!.recyclerStyle
reverse = media.selected!!.recyclerReversed
if (!loaded) {
model.mangaReadSources = if (media.isAdult) HMangaSources else MangaSources
headerAdapter = MangaReadAdapter(it, this, model.mangaReadSources!!)
chapterAdapter = MangaChapterAdapter(style ?: uiSettings.mangaDefaultView, media, this)
binding.animeSourceRecycler.adapter = ConcatAdapter(headerAdapter, chapterAdapter)
lifecycleScope.launch(Dispatchers.IO) {
model.loadMangaChapters(media, media.selected!!.source)
}
loaded = true
} else {
reload()
}
} else {
binding.animeNotSupported.visibility = View.VISIBLE
binding.animeNotSupported.text = getString(R.string.not_supported, media.format ?: "")
}
}
}
model.getMangaChapters().observe(viewLifecycleOwner) { loadedChapters ->
if (loadedChapters != null) {
val chapters = loadedChapters[media.selected!!.source]
if (chapters != null) {
media.manga?.chapters = chapters
//CHIP GROUP
val total = chapters.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 = chapters.keys.toTypedArray()
val stored = ceil((total).toDouble() / limit).toInt()
val position = 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()
}
}
}
}
fun onSourceChange(i: Int): MangaParser {
media.manga?.chapters = null
reload()
val selected = model.loadSelected(media)
model.mangaReadSources?.get(selected.source)?.showUserTextListener = null
selected.source = i
selected.server = null
model.saveSelected(media.id, selected, requireActivity())
media.selected = selected
return model.mangaReadSources?.get(i)!!
}
fun loadChapters(i: Int) {
lifecycleScope.launch(Dispatchers.IO) { model.loadMangaChapters(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(),
MANGA_GROUP,
getChannelId(true, media.id),
media.userPreferredName
)
snackString(
if (subscribed) getString(R.string.subscribed_notification, source)
else getString(R.string.unsubscribed_notification)
)
}
fun onMangaChapterClick(i: String) {
model.continueMedia = false
media.manga?.chapters?.get(i)?.let {
media.manga?.selectedChapter = i
model.saveSelected(media.id, media.selected!!, requireActivity())
ChapterLoaderDialog.newInstance(it, true).show(requireActivity().supportFragmentManager, "dialog")
}
}
@SuppressLint("NotifyDataSetChanged")
private fun reload() {
val selected = model.loadSelected(media)
//Find latest chapter for subscription
selected.latest = media.manga?.chapters?.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.handleChapters()
chapterAdapter.notifyItemRangeRemoved(0, chapterAdapter.arr.size)
var arr: ArrayList<MangaChapter> = arrayListOf()
if (media.manga!!.chapters != null) {
val end = if (end != null && end!! < media.manga!!.chapters!!.size) end else null
arr.addAll(
media.manga!!.chapters!!.values.toList().slice(start..(end ?: (media.manga!!.chapters!!.size - 1)))
)
if (reverse)
arr = (arr.reversed() as? ArrayList<MangaChapter>) ?: arr
}
chapterAdapter.arr = arr
chapterAdapter.updateType(style ?: uiSettings.mangaDefaultView)
chapterAdapter.notifyItemRangeInserted(0, arr.size)
}
override fun onDestroy() {
model.mangaReadSources?.flushText()
super.onDestroy()
}
private 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,20 @@
package ani.dantotsu.media.manga
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 MangaSourceAdapter(
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.overrideMangaChapters(i, source, id)
}
}

View file

@ -0,0 +1,136 @@
package ani.dantotsu.media.manga.mangareader
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.core.view.GestureDetectorCompat
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.*
import ani.dantotsu.media.manga.MangaChapter
import ani.dantotsu.settings.CurrentReaderSettings
import com.alexvasilkov.gestures.views.GestureFrameLayout
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
abstract class BaseImageAdapter(
val activity: MangaReaderActivity,
chapter: MangaChapter
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
val settings = activity.settings.default
val uiSettings = activity.uiSettings
val images = chapter.images()
@SuppressLint("ClickableViewAccessibility")
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val view = holder.itemView as GestureFrameLayout
view.controller.also {
if (settings.layout == CurrentReaderSettings.Layouts.PAGED) {
it.settings.enableGestures()
}
it.settings.isRotationEnabled = settings.rotation
}
if (settings.layout != CurrentReaderSettings.Layouts.PAGED) {
if (settings.padding) {
when (settings.direction) {
CurrentReaderSettings.Directions.TOP_TO_BOTTOM -> view.setPadding(0, 0, 0, 16f.px)
CurrentReaderSettings.Directions.LEFT_TO_RIGHT -> view.setPadding(0, 0, 16f.px, 0)
CurrentReaderSettings.Directions.BOTTOM_TO_TOP -> view.setPadding(0, 16f.px, 0, 0)
CurrentReaderSettings.Directions.RIGHT_TO_LEFT -> view.setPadding(16f.px, 0, 0, 0)
}
}
view.updateLayoutParams {
if (settings.direction != CurrentReaderSettings.Directions.LEFT_TO_RIGHT && settings.direction != CurrentReaderSettings.Directions.RIGHT_TO_LEFT) {
width = ViewGroup.LayoutParams.MATCH_PARENT
height = 480f.px
} else {
width = 480f.px
height = ViewGroup.LayoutParams.MATCH_PARENT
}
}
} else {
val detector = GestureDetectorCompat(view.context, object : GesturesListener() {
override fun onSingleClick(event: MotionEvent) = activity.handleController()
})
view.findViewById<View>(R.id.imgProgCover).apply {
setOnTouchListener { _, event ->
detector.onTouchEvent(event)
false
}
setOnLongClickListener {
val pos = holder.bindingAdapterPosition
val image = images.getOrNull(pos) ?: return@setOnLongClickListener false
activity.onImageLongClicked(pos, image, null) { dialog ->
activity.lifecycleScope.launch {
loadImage(pos, view)
}
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
dialog.dismiss()
}
}
}
}
activity.lifecycleScope.launch { loadImage(holder.bindingAdapterPosition, view) }
}
abstract suspend fun loadImage(position: Int, parent: View): Boolean
companion object {
suspend fun Context.loadBitmap(link: FileUrl, transforms: List<BitmapTransformation>): Bitmap? {
return tryWithSuspend {
withContext(Dispatchers.IO) {
Glide.with(this@loadBitmap)
.asBitmap()
.let {
if (link.url.startsWith("file://")) {
it.load(link.url)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
} else {
it.load(GlideUrl(link.url) { link.headers })
}
}
.let {
if (transforms.isNotEmpty()) {
it.transform(*transforms.toTypedArray())
}
else {
it
}
}
.submit()
.get()
}
}
}
fun mergeBitmap(bitmap1: Bitmap, bitmap2: Bitmap, scale: Boolean = false): Bitmap {
val height = if (bitmap1.height > bitmap2.height) bitmap1.height else bitmap2.height
val (bit1, bit2) = if (!scale) bitmap1 to bitmap2 else {
val width1 = bitmap1.width * height * 1f / bitmap1.height
val width2 = bitmap2.width * height * 1f / bitmap2.height
(Bitmap.createScaledBitmap(bitmap1, width1.toInt(), height, false)
to
Bitmap.createScaledBitmap(bitmap2, width2.toInt(), height, false))
}
val width = bit1.width + bit2.width
val newBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(newBitmap)
canvas.drawBitmap(bit1, 0f, (height * 1f - bit1.height) / 2, null)
canvas.drawBitmap(bit2, bit1.width.toFloat(), (height * 1f - bit2.height) / 2, null)
return newBitmap
}
}
}

View file

@ -0,0 +1,77 @@
package ani.dantotsu.media.manga.mangareader
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.R
import ani.dantotsu.currActivity
import ani.dantotsu.databinding.BottomSheetSelectorBinding
import ani.dantotsu.media.manga.MangaChapter
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.others.getSerialized
import ani.dantotsu.tryWith
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.Serializable
class ChapterLoaderDialog : BottomSheetDialogFragment() {
private var _binding: BottomSheetSelectorBinding? = null
private val binding get() = _binding!!
val model: MediaDetailsViewModel by activityViewModels()
private val launch : Boolean by lazy { arguments?.getBoolean("launch", false) ?: false }
private val chp : MangaChapter by lazy { arguments?.getSerialized("next")!! }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
var loaded = false
binding.selectorAutoListContainer.visibility = View.VISIBLE
binding.selectorListContainer.visibility = View.GONE
binding.selectorTitle.text = getString(R.string.loading_next_chap)
binding.selectorCancel.setOnClickListener {
dismiss()
}
model.getMedia().observe(viewLifecycleOwner) { m ->
if (m != null && !loaded) {
loaded = true
binding.selectorAutoText.text = chp.title
lifecycleScope.launch(Dispatchers.IO) {
if(model.loadMangaChapterImages(chp, m.selected!!)) {
val activity = currActivity()
activity?.runOnUiThread {
tryWith { dismiss() }
if(launch) {
val intent = Intent(activity, MangaReaderActivity::class.java).apply { putExtra("media", m) }
activity.startActivity(intent)
}
}
}
}
}
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = BottomSheetSelectorBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroy() {
_binding = null
super.onDestroy()
}
companion object {
fun newInstance(next: MangaChapter, launch: Boolean = false) = ChapterLoaderDialog().apply {
arguments = bundleOf("next" to next as Serializable, "launch" to launch)
}
}
}

View file

@ -0,0 +1,54 @@
package ani.dantotsu.media.manga.mangareader
import android.graphics.Bitmap
import android.view.View
import ani.dantotsu.media.manga.MangaChapter
import ani.dantotsu.settings.CurrentReaderSettings.Directions.LEFT_TO_RIGHT
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
class DualPageAdapter(
activity: MangaReaderActivity,
val chapter: MangaChapter
) : ImageAdapter(activity, chapter) {
private val pages = chapter.dualPages()
override suspend fun loadBitmap(position: Int, parent: View): Bitmap? {
val img1 = pages[position].first
val link1 = img1.url
if (link1.url.isEmpty()) return null
val img2 = pages[position].second
val link2 = img2?.url
if (link2?.url?.isEmpty() == true) return null
val transforms1 = mutableListOf<BitmapTransformation>()
val parserTransformation1 = activity.getTransformation(img1)
if (parserTransformation1 != null) transforms1.add(parserTransformation1)
val transforms2 = mutableListOf<BitmapTransformation>()
if (img2 != null) {
val parserTransformation2 = activity.getTransformation(img2)
if (parserTransformation2 != null) transforms2.add(parserTransformation2)
}
if (settings.cropBorders) {
transforms1.add(RemoveBordersTransformation(true, settings.cropBorderThreshold))
transforms1.add(RemoveBordersTransformation(false, settings.cropBorderThreshold))
if (img2 != null) {
transforms2.add(RemoveBordersTransformation(true, settings.cropBorderThreshold))
transforms2.add(RemoveBordersTransformation(false, settings.cropBorderThreshold))
}
}
val bitmap1 = activity.loadBitmap(link1, transforms1) ?: return null
val bitmap2 = link2?.let { activity.loadBitmap(it, transforms2) ?: return null }
return if (bitmap2 != null) {
if (settings.direction != LEFT_TO_RIGHT)
mergeBitmap(bitmap2, bitmap1)
else mergeBitmap(bitmap1, bitmap2)
} else bitmap1
}
override fun getItemCount(): Int = pages.size
}

View file

@ -0,0 +1,90 @@
package ani.dantotsu.media.manga.mangareader
import android.animation.ObjectAnimator
import android.content.res.Resources.getSystem
import android.graphics.Bitmap
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.updateLayoutParams
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.databinding.ItemImageBinding
import ani.dantotsu.media.manga.MangaChapter
import ani.dantotsu.settings.CurrentReaderSettings.Directions.LEFT_TO_RIGHT
import ani.dantotsu.settings.CurrentReaderSettings.Directions.RIGHT_TO_LEFT
import ani.dantotsu.settings.CurrentReaderSettings.Layouts.PAGED
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
open class ImageAdapter(
activity: MangaReaderActivity,
chapter: MangaChapter
) : BaseImageAdapter(activity, chapter) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageViewHolder {
val binding = ItemImageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ImageViewHolder(binding)
}
inner class ImageViewHolder(binding: ItemImageBinding) : RecyclerView.ViewHolder(binding.root)
open suspend fun loadBitmap(position: Int, parent: View) : Bitmap? {
val link = images.getOrNull(position)?.url ?: return null
if (link.url.isEmpty()) return null
val transforms = mutableListOf<BitmapTransformation>()
val parserTransformation = activity.getTransformation(images[position])
if(parserTransformation!=null) transforms.add(parserTransformation)
if(settings.cropBorders) {
transforms.add(RemoveBordersTransformation(true, settings.cropBorderThreshold))
transforms.add(RemoveBordersTransformation(false, settings.cropBorderThreshold))
}
return activity.loadBitmap(link, transforms)
}
override suspend fun loadImage(position: Int, parent: View): Boolean {
val imageView = parent.findViewById<SubsamplingScaleImageView>(R.id.imgProgImageNoGestures) ?: return false
val progress = parent.findViewById<View>(R.id.imgProgProgress) ?: return false
imageView.recycle()
imageView.visibility = View.GONE
val bitmap = loadBitmap(position, parent) ?: return false
var sWidth = getSystem().displayMetrics.widthPixels
var sHeight = getSystem().displayMetrics.heightPixels
if (settings.layout != PAGED)
parent.updateLayoutParams {
if (settings.direction != LEFT_TO_RIGHT && settings.direction != RIGHT_TO_LEFT) {
sHeight = if (settings.wrapImages) bitmap.height else (sWidth * bitmap.height * 1f / bitmap.width).toInt()
height = sHeight
} else {
sWidth = if (settings.wrapImages) bitmap.width else (sHeight * bitmap.width * 1f / bitmap.height).toInt()
width = sWidth
}
}
imageView.visibility = View.VISIBLE
imageView.setImage(ImageSource.cachedBitmap(bitmap))
val parentArea = sWidth * sHeight * 1f
val bitmapArea = bitmap.width * bitmap.height * 1f
val scale = if (parentArea < bitmapArea) (bitmapArea / parentArea) else (parentArea / bitmapArea)
imageView.maxScale = scale * 1.1f
imageView.minScale = scale
ObjectAnimator.ofFloat(parent, "alpha", 0f, 1f)
.setDuration((400 * uiSettings.animationSpeed).toLong())
.start()
progress.visibility = View.GONE
return true
}
override fun getItemCount(): Int = images.size
}

View file

@ -0,0 +1,733 @@
package ani.dantotsu.media.manga.mangareader
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.res.Configuration
import android.graphics.Bitmap
import android.os.Build
import android.os.Bundle
import android.view.*
import android.view.KeyEvent.*
import android.view.animation.OvershootInterpolator
import android.widget.AdapterView
import androidx.activity.addCallback
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.math.MathUtils.clamp
import androidx.core.view.GestureDetectorCompat
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.PagerSnapHelper
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.*
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.discord.Discord
import ani.dantotsu.connections.discord.RPC
import ani.dantotsu.connections.updateProgress
import ani.dantotsu.databinding.ActivityMangaReaderBinding
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.media.manga.MangaChapter
import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.others.getSerialized
import ani.dantotsu.parsers.HMangaSources
import ani.dantotsu.parsers.MangaImage
import ani.dantotsu.parsers.MangaSources
import ani.dantotsu.settings.CurrentReaderSettings.Companion.applyWebtoon
import ani.dantotsu.settings.CurrentReaderSettings.Directions.*
import ani.dantotsu.settings.CurrentReaderSettings.DualPageModes.*
import ani.dantotsu.settings.CurrentReaderSettings.Layouts.*
import ani.dantotsu.settings.ReaderSettings
import ani.dantotsu.settings.UserInterfaceSettings
import com.alexvasilkov.gestures.views.GestureFrameLayout
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.*
import kotlin.math.min
import kotlin.properties.Delegates
@SuppressLint("SetTextI18n")
class MangaReaderActivity : AppCompatActivity() {
private lateinit var binding: ActivityMangaReaderBinding
private val model: MediaDetailsViewModel by viewModels()
private val scope = lifecycleScope
private lateinit var media: Media
private lateinit var chapter: MangaChapter
private lateinit var chapters: MutableMap<String, MangaChapter>
private lateinit var chaptersArr: List<String>
private lateinit var chaptersTitleArr: ArrayList<String>
private var currentChapterIndex = 0
private var isContVisible = false
private var showProgressDialog = true
private var progressDialog: AlertDialog.Builder? = null
private var maxChapterPage = 0L
private var currentChapterPage = 0L
lateinit var settings: ReaderSettings
lateinit var uiSettings: UserInterfaceSettings
private var notchHeight: Int? = null
private var imageAdapter: BaseImageAdapter? = null
var sliding = false
var isAnimating = false
private var rpc : RPC? = null
override fun onAttachedToWindow() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && !settings.showSystemBars) {
val displayCutout = window.decorView.rootWindowInsets.displayCutout
if (displayCutout != null) {
if (displayCutout.boundingRects.size > 0) {
notchHeight = min(displayCutout.boundingRects[0].width(), displayCutout.boundingRects[0].height())
checkNotch()
}
}
}
super.onAttachedToWindow()
}
private fun checkNotch() {
binding.mangaReaderTopLayout.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = notchHeight ?: return
}
}
private fun hideBars() {
if (!settings.showSystemBars) hideSystemBars()
}
override fun onDestroy() {
rpc?.close()
super.onDestroy()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMangaReaderBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.mangaReaderBack.setOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
onBackPressedDispatcher.addCallback(this) {
progress { finish() }
}
settings = loadData("reader_settings", this) ?: ReaderSettings().apply { saveData("reader_settings", this) }
uiSettings = loadData("ui_settings", this) ?: UserInterfaceSettings().apply { saveData("ui_settings", this) }
controllerDuration = (uiSettings.animationSpeed * 200).toLong()
hideBars()
var pageSliderTimer = Timer()
fun pageSliderHide() {
pageSliderTimer.cancel()
pageSliderTimer.purge()
val timerTask: TimerTask = object : TimerTask() {
override fun run() {
binding.mangaReaderCont.post {
sliding = false
handleController(false)
}
}
}
pageSliderTimer = Timer()
pageSliderTimer.schedule(timerTask, 3000)
}
binding.mangaReaderSlider.addOnChangeListener { _, value, fromUser ->
if (fromUser) {
sliding = true
if (settings.default.layout != PAGED)
binding.mangaReaderRecycler.scrollToPosition((value.toInt() - 1) / (dualPage { 2 } ?: 1))
else
binding.mangaReaderPager.currentItem = (value.toInt() - 1) / (dualPage { 2 } ?: 1)
pageSliderHide()
}
}
media = if (model.getMedia().value == null)
try {
(intent.getSerialized("media")) ?: return
} catch (e: Exception) {
logError(e)
return
}
else model.getMedia().value ?: return
model.setMedia(media)
if (settings.autoDetectWebtoon && media.countryOfOrigin != "JP") applyWebtoon(settings.default)
settings.default = loadData("${media.id}_current_settings") ?: settings.default
chapters = media.manga?.chapters ?: return
chapter = chapters[media.manga!!.selectedChapter] ?: return
model.mangaReadSources = if (media.isAdult) HMangaSources else MangaSources
binding.mangaReaderSource.visibility = if (settings.showSource) View.VISIBLE else View.GONE
binding.mangaReaderSource.text = model.mangaReadSources!!.names[media.selected!!.source]
binding.mangaReaderTitle.text = media.userPreferredName
chaptersArr = chapters.keys.toList()
currentChapterIndex = chaptersArr.indexOf(media.manga!!.selectedChapter)
chaptersTitleArr = arrayListOf()
chapters.forEach {
val chapter = it.value
chaptersTitleArr.add("${if (!chapter.title.isNullOrEmpty() && chapter.title != "null") "" else "Chapter "}${chapter.number}${if (!chapter.title.isNullOrEmpty() && chapter.title != "null") " : " + chapter.title else ""}")
}
showProgressDialog = if (settings.askIndividual) loadData<Boolean>("${media.id}_progressDialog") != true else false
progressDialog =
if (showProgressDialog && Anilist.userid != null && if (media.isAdult) settings.updateForH else true)
AlertDialog.Builder(this, R.style.DialogTheme).setTitle(getString(R.string.title_update_progress)).apply {
setMultiChoiceItems(
arrayOf(getString(R.string.dont_ask_again, media.userPreferredName)),
booleanArrayOf(false)
) { _, _, isChecked ->
if (isChecked) progressDialog = null
saveData("${media.id}_progressDialog", isChecked)
showProgressDialog = isChecked
}
setOnCancelListener { hideBars() }
}
else null
//Chapter Change
fun change(index: Int) {
saveData("${media.id}_${chaptersArr[currentChapterIndex]}", currentChapterPage, this)
ChapterLoaderDialog.newInstance(chapters[chaptersArr[index]]!!).show(supportFragmentManager, "dialog")
}
//ChapterSelector
binding.mangaReaderChapterSelect.adapter = NoPaddingArrayAdapter(this, R.layout.item_dropdown, chaptersTitleArr)
binding.mangaReaderChapterSelect.setSelection(currentChapterIndex)
binding.mangaReaderChapterSelect.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(p0: AdapterView<*>?, p1: View?, position: Int, p3: Long) {
if (position != currentChapterIndex) change(position)
}
override fun onNothingSelected(parent: AdapterView<*>) {}
}
binding.mangaReaderSettings.setSafeOnClickListener {
ReaderSettingsDialogFragment.newInstance().show(supportFragmentManager, "settings")
}
//Next Chapter
binding.mangaReaderNextChap.setOnClickListener {
binding.mangaReaderNextChapter.performClick()
}
binding.mangaReaderNextChapter.setOnClickListener {
if (chaptersArr.size > currentChapterIndex + 1) progress { change(currentChapterIndex + 1) }
else snackString(getString(R.string.next_chapter_not_found))
}
//Prev Chapter
binding.mangaReaderPrevChap.setOnClickListener {
binding.mangaReaderPreviousChapter.performClick()
}
binding.mangaReaderPreviousChapter.setOnClickListener {
if (currentChapterIndex > 0) change(currentChapterIndex - 1)
else snackString(getString(R.string.first_chapter))
}
model.getMangaChapter().observe(this) { chap ->
if (chap != null) {
chapter = chap
media.manga!!.selectedChapter = chapter.number
media.selected = model.loadSelected(media)
saveData("${media.id}_current_chp", chap.number, this)
currentChapterIndex = chaptersArr.indexOf(chap.number)
binding.mangaReaderChapterSelect.setSelection(currentChapterIndex)
binding.mangaReaderNextChap.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: ""
binding.mangaReaderPrevChap.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: ""
applySettings()
rpc?.close()
rpc = Discord.defaultRPC()
rpc?.send {
type = RPC.Type.WATCHING
activityName = media.userPreferredName
details = chap.title?.takeIf { it.isNotEmpty() } ?: getString(R.string.chapter_num, chap.number)
state = "Chapter : ${chap.number}/${media.manga?.totalChapters ?: "??"}"
media.cover?.let { cover ->
largeImage = RPC.Link(media.userPreferredName, cover)
}
media.shareLink?.let { link ->
buttons.add(0, RPC.Link(getString(R.string.view_manga), link))
}
}
}
}
scope.launch(Dispatchers.IO) { model.loadMangaChapterImages(chapter, media.selected!!) }
}
private val snapHelper = PagerSnapHelper()
fun <T> dualPage(callback: () -> T): T? {
return when (settings.default.dualPageMode) {
No -> null
Automatic -> {
val orientation = resources.configuration.orientation
if (orientation == Configuration.ORIENTATION_LANDSCAPE) callback.invoke()
else null
}
Force -> callback.invoke()
}
}
@SuppressLint("ClickableViewAccessibility")
fun applySettings() {
saveData("${media.id}_current_settings", settings.default)
hideBars()
//true colors
SubsamplingScaleImageView.setPreferredBitmapConfig(
if (settings.default.trueColors) Bitmap.Config.ARGB_8888
else Bitmap.Config.RGB_565
)
//keep screen On
if (settings.default.keepScreenOn) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
else window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
binding.mangaReaderPager.unregisterOnPageChangeCallback(pageChangeCallback)
currentChapterPage = loadData("${media.id}_${chapter.number}", this) ?: 1
val chapImages = chapter.images()
maxChapterPage = 0
if (chapImages.isNotEmpty()) {
maxChapterPage = chapImages.size.toLong()
saveData("${media.id}_${chapter.number}_max", maxChapterPage)
imageAdapter = dualPage { DualPageAdapter(this, chapter) } ?: ImageAdapter(this, chapter)
if (chapImages.size > 1) {
binding.mangaReaderSlider.apply {
visibility = View.VISIBLE
valueTo = maxChapterPage.toFloat()
value = clamp(currentChapterPage.toFloat(), 1f, valueTo)
}
} else {
binding.mangaReaderSlider.visibility = View.GONE
}
binding.mangaReaderPageNumber.text =
if (settings.default.hidePageNumbers) "" else "${currentChapterPage}/$maxChapterPage"
}
val currentPage = currentChapterPage.toInt()
if ((settings.default.direction == TOP_TO_BOTTOM || settings.default.direction == BOTTOM_TO_TOP)) {
binding.mangaReaderSwipy.vertical = true
if (settings.default.direction == TOP_TO_BOTTOM) {
binding.BottomSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: getString(R.string.no_chapter)
binding.TopSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: getString(R.string.no_chapter)
binding.mangaReaderSwipy.onTopSwiped = {
binding.mangaReaderPreviousChapter.performClick()
}
binding.mangaReaderSwipy.onBottomSwiped = {
binding.mangaReaderNextChapter.performClick()
}
} else {
binding.BottomSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: getString(R.string.no_chapter)
binding.TopSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: getString(R.string.no_chapter)
binding.mangaReaderSwipy.onTopSwiped = {
binding.mangaReaderNextChapter.performClick()
}
binding.mangaReaderSwipy.onBottomSwiped = {
binding.mangaReaderPreviousChapter.performClick()
}
}
binding.mangaReaderSwipy.topBeingSwiped = { value ->
binding.TopSwipeContainer.apply {
alpha = value
translationY = -height.dp * (1 - min(value, 1f))
}
}
binding.mangaReaderSwipy.bottomBeingSwiped = { value ->
binding.BottomSwipeContainer.apply {
alpha = value
translationY = height.dp * (1 - min(value, 1f))
}
}
} else {
binding.mangaReaderSwipy.vertical = false
if (settings.default.direction == RIGHT_TO_LEFT) {
binding.LeftSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: getString(R.string.no_chapter)
binding.RightSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: getString(R.string.no_chapter)
binding.mangaReaderSwipy.onLeftSwiped = {
binding.mangaReaderNextChapter.performClick()
}
binding.mangaReaderSwipy.onRightSwiped = {
binding.mangaReaderPreviousChapter.performClick()
}
} else {
binding.LeftSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: getString(R.string.no_chapter)
binding.RightSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: getString(R.string.no_chapter)
binding.mangaReaderSwipy.onLeftSwiped = {
binding.mangaReaderPreviousChapter.performClick()
}
binding.mangaReaderSwipy.onRightSwiped = {
binding.mangaReaderNextChapter.performClick()
}
}
binding.mangaReaderSwipy.leftBeingSwiped = { value ->
binding.LeftSwipeContainer.apply {
alpha = value
translationX = -width.dp * (1 - min(value, 1f))
}
}
binding.mangaReaderSwipy.rightBeingSwiped = { value ->
binding.RightSwipeContainer.apply {
alpha = value
translationX = width.dp * (1 - min(value, 1f))
}
}
}
if (settings.default.layout != PAGED) {
binding.mangaReaderRecyclerContainer.visibility = View.VISIBLE
binding.mangaReaderRecyclerContainer.controller.settings.isRotationEnabled = settings.default.rotation
val detector = GestureDetectorCompat(this, object : GesturesListener() {
override fun onLongPress(e: MotionEvent) {
if (binding.mangaReaderRecycler.findChildViewUnder(e.x, e.y).let { child ->
child ?: return@let false
val pos = binding.mangaReaderRecycler.getChildAdapterPosition(child)
val callback: (ImageViewDialog) -> Unit = { dialog ->
lifecycleScope.launch { imageAdapter?.loadImage(pos, child as GestureFrameLayout) }
binding.mangaReaderRecycler.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
dialog.dismiss()
}
dualPage {
val page = chapter.dualPages().getOrNull(pos) ?: return@dualPage false
val nextPage = page.second
if (settings.default.direction != LEFT_TO_RIGHT && nextPage != null)
onImageLongClicked(pos * 2, nextPage, page.first, callback)
else
onImageLongClicked(pos * 2, page.first, nextPage, callback)
} ?: onImageLongClicked(pos, chapImages.getOrNull(pos) ?: return@let false, null, callback)
}
) binding.mangaReaderRecycler.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
super.onLongPress(e)
}
override fun onSingleClick(event: MotionEvent) {
handleController()
}
})
val manager = PreloadLinearLayoutManager(
this,
if (settings.default.direction == TOP_TO_BOTTOM || settings.default.direction == BOTTOM_TO_TOP)
RecyclerView.VERTICAL
else
RecyclerView.HORIZONTAL,
!(settings.default.direction == TOP_TO_BOTTOM || settings.default.direction == LEFT_TO_RIGHT)
)
manager.preloadItemCount = 5
binding.mangaReaderPager.visibility = View.GONE
binding.mangaReaderRecycler.apply {
clearOnScrollListeners()
binding.mangaReaderSwipy.child = this
adapter = imageAdapter
layoutManager = manager
setOnTouchListener { _, event ->
if (event != null)
tryWith { detector.onTouchEvent(event) } ?: false
else false
}
addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(v: RecyclerView, dx: Int, dy: Int) {
settings.default.apply {
if (
((direction == TOP_TO_BOTTOM || direction == BOTTOM_TO_TOP)
&& (!v.canScrollVertically(-1) || !v.canScrollVertically(1)))
||
((direction == LEFT_TO_RIGHT || direction == RIGHT_TO_LEFT)
&& (!v.canScrollHorizontally(-1) || !v.canScrollHorizontally(1)))
) {
handleController(true)
} else handleController(false)
}
updatePageNumber(manager.findLastVisibleItemPosition().toLong() * (dualPage { 2 } ?: 1) + 1)
super.onScrolled(v, dx, dy)
}
})
if ((settings.default.direction == TOP_TO_BOTTOM || settings.default.direction == BOTTOM_TO_TOP))
updatePadding(0, 128f.px, 0, 128f.px)
else
updatePadding(128f.px, 0, 128f.px, 0)
snapHelper.attachToRecyclerView(
if (settings.default.layout == CONTINUOUS_PAGED) this
else null
)
onVolumeUp = {
if ((settings.default.direction == TOP_TO_BOTTOM || settings.default.direction == BOTTOM_TO_TOP))
smoothScrollBy(0, -500)
else
smoothScrollBy(-500, 0)
}
onVolumeDown = {
if ((settings.default.direction == TOP_TO_BOTTOM || settings.default.direction == BOTTOM_TO_TOP))
smoothScrollBy(0, 500)
else
smoothScrollBy(500, 0)
}
scrollToPosition(currentPage / (dualPage { 2 } ?: 1) - 1)
}
} else {
binding.mangaReaderRecyclerContainer.visibility = View.GONE
binding.mangaReaderPager.apply {
binding.mangaReaderSwipy.child = this
visibility = View.VISIBLE
adapter = imageAdapter
layoutDirection =
if (settings.default.direction == BOTTOM_TO_TOP || settings.default.direction == RIGHT_TO_LEFT)
View.LAYOUT_DIRECTION_RTL
else View.LAYOUT_DIRECTION_LTR
orientation =
if (settings.default.direction == LEFT_TO_RIGHT || settings.default.direction == RIGHT_TO_LEFT)
ViewPager2.ORIENTATION_HORIZONTAL
else ViewPager2.ORIENTATION_VERTICAL
registerOnPageChangeCallback(pageChangeCallback)
offscreenPageLimit = 5
setCurrentItem(currentPage / (dualPage { 2 } ?: 1) - 1, false)
}
onVolumeUp = {
binding.mangaReaderPager.currentItem -= 1
}
onVolumeDown = {
binding.mangaReaderPager.currentItem += 1
}
}
}
private var onVolumeUp: (() -> Unit)? = null
private var onVolumeDown: (() -> Unit)? = null
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
return when (event.keyCode) {
KEYCODE_VOLUME_UP, KEYCODE_DPAD_UP, KEYCODE_PAGE_UP -> {
if (event.keyCode == KEYCODE_VOLUME_UP)
if (!settings.default.volumeButtons)
return false
if (event.action == ACTION_DOWN) {
onVolumeUp?.invoke()
true
} else false
}
KEYCODE_VOLUME_DOWN, KEYCODE_DPAD_DOWN, KEYCODE_PAGE_DOWN -> {
if (event.keyCode == KEYCODE_VOLUME_DOWN)
if (!settings.default.volumeButtons)
return false
if (event.action == ACTION_DOWN) {
onVolumeDown?.invoke()
true
} else false
}
else -> {
super.dispatchKeyEvent(event)
}
}
}
private val pageChangeCallback = object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
updatePageNumber(position.toLong() * (dualPage { 2 } ?: 1) + 1)
handleController(position == 0 || position + 1 >= maxChapterPage)
super.onPageSelected(position)
}
}
private val overshoot = OvershootInterpolator(1.4f)
private var controllerDuration by Delegates.notNull<Long>()
private var goneTimer = Timer()
fun gone() {
goneTimer.cancel()
goneTimer.purge()
val timerTask: TimerTask = object : TimerTask() {
override fun run() {
if (!isContVisible) binding.mangaReaderCont.post {
binding.mangaReaderCont.visibility = View.GONE
isAnimating = false
}
}
}
goneTimer = Timer()
goneTimer.schedule(timerTask, controllerDuration)
}
fun handleController(shouldShow: Boolean? = null) {
if (!sliding) {
if (!settings.showSystemBars) {
hideBars()
checkNotch()
}
//horizontal scrollbar
if (settings.default.horizontalScrollBar) {
binding.mangaReaderSliderContainer.updateLayoutParams {
height = ViewGroup.LayoutParams.WRAP_CONTENT
width = ViewGroup.LayoutParams.WRAP_CONTENT
}
binding.mangaReaderSlider.apply {
updateLayoutParams<ViewGroup.MarginLayoutParams> {
width = ViewGroup.LayoutParams.MATCH_PARENT
}
rotation = 0f
}
} else {
binding.mangaReaderSliderContainer.updateLayoutParams {
height = ViewGroup.LayoutParams.MATCH_PARENT
width = 48f.px
}
binding.mangaReaderSlider.apply {
updateLayoutParams {
width = binding.mangaReaderSliderContainer.height - 16f.px
}
rotation = 90f
}
}
binding.mangaReaderSlider.layoutDirection =
if (settings.default.direction == RIGHT_TO_LEFT || settings.default.direction == BOTTOM_TO_TOP)
View.LAYOUT_DIRECTION_RTL
else View.LAYOUT_DIRECTION_LTR
shouldShow?.apply { isContVisible = !this }
if (isContVisible) {
isContVisible = false
if (!isAnimating) {
isAnimating = true
ObjectAnimator.ofFloat(binding.mangaReaderCont, "alpha", 1f, 0f).setDuration(controllerDuration).start()
ObjectAnimator.ofFloat(binding.mangaReaderBottomLayout, "translationY", 0f, 128f)
.apply { interpolator = overshoot;duration = controllerDuration;start() }
ObjectAnimator.ofFloat(binding.mangaReaderTopLayout, "translationY", 0f, -128f)
.apply { interpolator = overshoot;duration = controllerDuration;start() }
}
gone()
} else {
isContVisible = true
binding.mangaReaderCont.visibility = View.VISIBLE
ObjectAnimator.ofFloat(binding.mangaReaderCont, "alpha", 0f, 1f).setDuration(controllerDuration).start()
ObjectAnimator.ofFloat(binding.mangaReaderTopLayout, "translationY", -128f, 0f)
.apply { interpolator = overshoot;duration = controllerDuration;start() }
ObjectAnimator.ofFloat(binding.mangaReaderBottomLayout, "translationY", 128f, 0f)
.apply { interpolator = overshoot;duration = controllerDuration;start() }
}
}
}
private var loading = false
fun updatePageNumber(page: Long) {
if (currentChapterPage != page) {
currentChapterPage = page
saveData("${media.id}_${chapter.number}", page, this)
binding.mangaReaderPageNumber.text =
if (settings.default.hidePageNumbers) "" else "${currentChapterPage}/$maxChapterPage"
if (!sliding) binding.mangaReaderSlider.apply {
value = clamp(currentChapterPage.toFloat(), 1f, valueTo)
}
}
if (maxChapterPage - currentChapterPage <= 1 && !loading)
scope.launch(Dispatchers.IO) {
loading = true
model.loadMangaChapterImages(
chapters[chaptersArr.getOrNull(currentChapterIndex + 1) ?: return@launch]!!,
media.selected!!,
false
)
loading = false
}
}
private fun progress(runnable: Runnable) {
if (maxChapterPage - currentChapterPage <= 1 && Anilist.userid != null) {
if (showProgressDialog && progressDialog != null) {
progressDialog?.setCancelable(false)
?.setPositiveButton(getString(R.string.yes)) { dialog, _ ->
saveData("${media.id}_save_progress", true)
updateProgress(media, media.manga!!.selectedChapter!!)
dialog.dismiss()
runnable.run()
}
?.setNegativeButton(getString(R.string.no)) { dialog, _ ->
saveData("${media.id}_save_progress", false)
dialog.dismiss()
runnable.run()
}
progressDialog?.show()
} else {
if (loadData<Boolean>("${media.id}_save_progress") != false && if (media.isAdult) settings.updateForH else true)
updateProgress(media, media.manga!!.selectedChapter!!)
runnable.run()
}
} else {
runnable.run()
}
}
fun getTransformation(mangaImage: MangaImage): BitmapTransformation? {
return model.loadTransformation(mangaImage, media.selected!!.source)
}
fun onImageLongClicked(
pos: Int,
img1: MangaImage,
img2: MangaImage?,
callback: ((ImageViewDialog) -> Unit)? = null
): Boolean {
if (!settings.default.longClickImage) return false
val title = "(Page ${pos + 1}${if (img2 != null) "-${pos + 2}" else ""}) ${
chaptersTitleArr.getOrNull(currentChapterIndex)?.replace(" : ", " - ") ?: ""
} [${media.userPreferredName}]"
ImageViewDialog.newInstance(title, img1.url, true, img2?.url).apply {
val transforms1 = mutableListOf<BitmapTransformation>()
val parserTransformation1 = getTransformation(img1)
if (parserTransformation1 != null) transforms1.add(parserTransformation1)
val transforms2 = mutableListOf<BitmapTransformation>()
if (img2 != null) {
val parserTransformation2 = getTransformation(img2)
if (parserTransformation2 != null) transforms2.add(parserTransformation2)
}
val threshold = settings.default.cropBorderThreshold
if (settings.default.cropBorders) {
transforms1.add(RemoveBordersTransformation(true, threshold))
transforms1.add(RemoveBordersTransformation(false, threshold))
if (img2 != null) {
transforms2.add(RemoveBordersTransformation(true, threshold))
transforms2.add(RemoveBordersTransformation(false, threshold))
}
}
trans1 = transforms1.ifEmpty { null }
trans2 = transforms2.ifEmpty { null }
onReloadPressed = callback
show(supportFragmentManager, "image")
}
return true
}
}

View file

@ -0,0 +1,52 @@
package ani.dantotsu.media.manga.mangareader
import android.content.Context
import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.OrientationHelper
import androidx.recyclerview.widget.RecyclerView
import kotlin.math.max
class PreloadLinearLayoutManager(context: Context, orientation: Int, reverseLayout: Boolean) :
LinearLayoutManager(context, orientation, reverseLayout) {
private val mOrientationHelper: OrientationHelper = OrientationHelper.createOrientationHelper(this, orientation)
/**
* As [LinearLayoutManager.collectAdjacentPrefetchPositions] will prefetch one view for us,
* we only need to prefetch additional ones.
*/
var preloadItemCount = 1
set(count){
require(count >= 1) { "preloadItemCount must not be smaller than 1!" }
field = count - 1
}
override fun collectAdjacentPrefetchPositions(
dx: Int, dy: Int, state: RecyclerView.State,
layoutPrefetchRegistry: LayoutPrefetchRegistry
) {
super.collectAdjacentPrefetchPositions(dx, dy, state, layoutPrefetchRegistry)
val delta = if (orientation == HORIZONTAL) dx else dy
if (childCount == 0 || delta == 0) {
return
}
val layoutDirection = if (delta > 0) 1 else -1
val child = getChildClosest(layoutDirection)
val currentPosition: Int = getPosition(child ?: return) + layoutDirection
if (layoutDirection == 1) {
val scrollingOffset = (mOrientationHelper.getDecoratedEnd(child) - mOrientationHelper.endAfterPadding)
((currentPosition + 1) until (currentPosition + preloadItemCount + 1)).forEach {
if (it >= 0 && it < state.itemCount) {
layoutPrefetchRegistry.addPosition(it, max(0, scrollingOffset))
}
}
}
}
private fun getChildClosest(layoutDirection: Int): View? {
return getChildAt(if (layoutDirection == -1) 0 else childCount - 1)
}
}

View file

@ -0,0 +1,158 @@
package ani.dantotsu.media.manga.mangareader
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.R
import ani.dantotsu.databinding.BottomSheetCurrentReaderSettingsBinding
import ani.dantotsu.settings.CurrentReaderSettings
import ani.dantotsu.settings.CurrentReaderSettings.Directions
class ReaderSettingsDialogFragment : BottomSheetDialogFragment() {
private var _binding: BottomSheetCurrentReaderSettingsBinding? = null
private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = BottomSheetCurrentReaderSettingsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val activity = requireActivity() as MangaReaderActivity
val settings = activity.settings.default
binding.readerDirectionText.text = resources.getStringArray(R.array.manga_directions)[settings.direction.ordinal]
binding.readerDirection.rotation = 90f * (settings.direction.ordinal)
binding.readerDirection.setOnClickListener {
settings.direction = Directions[settings.direction.ordinal + 1] ?: Directions.TOP_TO_BOTTOM
binding.readerDirectionText.text = resources.getStringArray(R.array.manga_directions)[settings.direction.ordinal]
binding.readerDirection.rotation = 90f * (settings.direction.ordinal)
activity.applySettings()
}
val list = listOf(
binding.readerPaged,
binding.readerContinuousPaged,
binding.readerContinuous
)
binding.readerPadding.isEnabled = settings.layout.ordinal!=0
fun paddingAvailable(enable:Boolean){
binding.readerPadding.isEnabled = enable
}
binding.readerPadding.isChecked = settings.padding
binding.readerPadding.setOnCheckedChangeListener { _,isChecked ->
settings.padding = isChecked
activity.applySettings()
}
binding.readerCropBorders.isChecked = settings.cropBorders
binding.readerCropBorders.setOnCheckedChangeListener { _,isChecked ->
settings.cropBorders = isChecked
activity.applySettings()
}
binding.readerLayoutText.text = resources.getStringArray(R.array.manga_layouts)[settings.layout.ordinal]
var selected = list[settings.layout.ordinal]
selected.alpha = 1f
list.forEachIndexed { index , imageButton ->
imageButton.setOnClickListener {
selected.alpha = 0.33f
selected = imageButton
selected.alpha = 1f
settings.layout = CurrentReaderSettings.Layouts[index]?:CurrentReaderSettings.Layouts.CONTINUOUS
binding.readerLayoutText.text = resources.getStringArray(R.array.manga_layouts)[settings.layout.ordinal]
activity.applySettings()
paddingAvailable(settings.layout.ordinal!=0)
}
}
val dualList = listOf(
binding.readerDualNo,
binding.readerDualAuto,
binding.readerDualForce
)
binding.readerDualPageText.text = settings.dualPageMode.toString()
var selectedDual = dualList[settings.dualPageMode.ordinal]
selectedDual.alpha = 1f
dualList.forEachIndexed { index, imageButton ->
imageButton.setOnClickListener {
selectedDual.alpha = 0.33f
selectedDual = imageButton
selectedDual.alpha = 1f
settings.dualPageMode = CurrentReaderSettings.DualPageModes[index] ?: CurrentReaderSettings.DualPageModes.Automatic
binding.readerDualPageText.text = settings.dualPageMode.toString()
activity.applySettings()
}
}
binding.readerTrueColors.isChecked = settings.trueColors
binding.readerTrueColors.setOnCheckedChangeListener { _, isChecked ->
settings.trueColors = isChecked
activity.applySettings()
}
binding.readerImageRotation.isChecked = settings.rotation
binding.readerImageRotation.setOnCheckedChangeListener { _, isChecked ->
settings.rotation = isChecked
activity.applySettings()
}
binding.readerHorizontalScrollBar.isChecked = settings.horizontalScrollBar
binding.readerHorizontalScrollBar.setOnCheckedChangeListener { _, isChecked ->
settings.horizontalScrollBar = isChecked
activity.applySettings()
}
binding.readerKeepScreenOn.isChecked = settings.keepScreenOn
binding.readerKeepScreenOn.setOnCheckedChangeListener { _,isChecked ->
settings.keepScreenOn = isChecked
activity.applySettings()
}
binding.readerHidePageNumbers.isChecked = settings.hidePageNumbers
binding.readerHidePageNumbers.setOnCheckedChangeListener { _,isChecked ->
settings.hidePageNumbers = isChecked
activity.applySettings()
}
binding.readerOverscroll.isChecked = settings.overScrollMode
binding.readerOverscroll.setOnCheckedChangeListener { _,isChecked ->
settings.overScrollMode = isChecked
activity.applySettings()
}
binding.readerVolumeButton.isChecked = settings.volumeButtons
binding.readerVolumeButton.setOnCheckedChangeListener { _,isChecked ->
settings.volumeButtons = isChecked
activity.applySettings()
}
binding.readerWrapImage.isChecked = settings.wrapImages
binding.readerWrapImage.setOnCheckedChangeListener { _,isChecked ->
settings.wrapImages = isChecked
activity.applySettings()
}
binding.readerLongClickImage.isChecked = settings.longClickImage
binding.readerLongClickImage.setOnCheckedChangeListener { _,isChecked ->
settings.longClickImage = isChecked
activity.applySettings()
}
}
override fun onDestroy() {
_binding = null
super.onDestroy()
}
companion object{
fun newInstance() = ReaderSettingsDialogFragment()
}
}

View file

@ -0,0 +1,100 @@
package ani.dantotsu.media.manga.mangareader
import android.graphics.Bitmap
import android.graphics.Color
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import java.security.MessageDigest
class RemoveBordersTransformation(private val white:Boolean, private val threshHold:Int) : BitmapTransformation() {
override fun transform(
pool: BitmapPool,
toTransform: Bitmap,
outWidth: Int,
outHeight: Int
): Bitmap {
// Get the dimensions of the input bitmap
val width = toTransform.width
val height = toTransform.height
// Find the non-white area by scanning from the edges
var left = 0
var top = 0
var right = width - 1
var bottom = height - 1
// Scan from the left edge
for (x in 0 until width) {
var stop = false
for (y in 0 until height) {
if (isPixelNotWhite(toTransform.getPixel(x, y))) {
left = x
stop = true
break
}
}
if (stop) break
}
// Scan from the right edge
for (x in width - 1 downTo left) {
var stop = false
for (y in 0 until height) {
if (isPixelNotWhite(toTransform.getPixel(x, y))) {
right = x
stop = true
break
}
}
if (stop) break
}
// Scan from the top edge
for (y in 0 until height) {
var stop = false
for (x in 0 until width) {
if (isPixelNotWhite(toTransform.getPixel(x, y))) {
top = y
stop = true
break
}
}
if (stop) break
}
// Scan from the bottom edge
for (y in height - 1 downTo top) {
var stop = false
for (x in 0 until width) {
if (isPixelNotWhite(toTransform.getPixel(x, y))) {
bottom = y
stop = true
break
}
}
if (stop) break
}
// Crop the bitmap to the non-white area
// Return the cropped bitmap
return Bitmap.createBitmap(
toTransform,
left,
top,
right - left + 1,
bottom - top + 1
)
}
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
messageDigest.update(
"RemoveBordersTransformation(${white}_$threshHold)".toByteArray()
)
}
private fun isPixelNotWhite(pixel: Int): Boolean {
val brightness = Color.red(pixel) + Color.green(pixel) + Color.blue(pixel)
return if(white) brightness < (255-threshHold) else brightness > threshHold
}
}

View file

@ -0,0 +1,255 @@
package ani.dantotsu.media.manga.mangareader
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Resources
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import android.widget.FrameLayout
class Swipy @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : FrameLayout(context, attrs) {
var dragDivider : Int = 5
var vertical = true
//public, in case a different sub child needs to be considered
var child: View? = getChildAt(0)
var topBeingSwiped: ((Float) -> Unit) = {}
var onTopSwiped: (() -> Unit) = {}
var onBottomSwiped: (() -> Unit) = {}
var bottomBeingSwiped: ((Float) -> Unit) = {}
var onLeftSwiped: (() -> Unit) = {}
var leftBeingSwiped: ((Float) -> Unit) = {}
var onRightSwiped: (() -> Unit) = {}
var rightBeingSwiped: ((Float) -> Unit) = {}
companion object {
private const val DRAG_RATE = .5f
private const val INVALID_POINTER = -1
}
private var touchSlop = ViewConfiguration.get(context).scaledTouchSlop
private var activePointerId = INVALID_POINTER
private var isBeingDragged = false
private var initialDown = 0f
private var initialMotion = 0f
enum class VerticalPosition {
Top,
None,
Bottom
}
enum class HorizontalPosition {
Left,
None,
Right
}
private var horizontalPos = HorizontalPosition.None
private var verticalPos = VerticalPosition.None
private fun setChildPosition() {
child?.apply {
if (vertical) {
verticalPos = VerticalPosition.None
if (!canScrollVertically(1)) {
verticalPos = VerticalPosition.Bottom
}
if (!canScrollVertically(-1)) {
verticalPos = VerticalPosition.Top
}
} else {
horizontalPos = HorizontalPosition.None
if (!canScrollHorizontally(1)) {
horizontalPos = HorizontalPosition.Right
}
if (!canScrollHorizontally(-1)) {
horizontalPos = HorizontalPosition.Left
}
}
}
}
private fun canChildScroll(): Boolean {
setChildPosition()
return if (vertical) verticalPos == VerticalPosition.None
else horizontalPos == HorizontalPosition.None
}
private fun onSecondaryPointerUp(ev: MotionEvent) {
val pointerIndex = ev.actionIndex
val pointerId = ev.getPointerId(pointerIndex)
if (pointerId == activePointerId) {
val newPointerIndex = if (pointerIndex == 0) 1 else 0
activePointerId = ev.getPointerId(newPointerIndex)
}
}
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
val action = ev.actionMasked
val pointerIndex: Int
if (!isEnabled || canChildScroll()) {
return false
}
when (action) {
MotionEvent.ACTION_DOWN -> {
activePointerId = ev.getPointerId(0)
isBeingDragged = false
pointerIndex = ev.findPointerIndex(activePointerId)
if (pointerIndex < 0) {
return false
}
initialDown = if (vertical) ev.getY(pointerIndex) else ev.getX(pointerIndex)
}
MotionEvent.ACTION_MOVE -> {
if (activePointerId == INVALID_POINTER) {
//("Got ACTION_MOVE event but don't have an active pointer id.")
return false
}
pointerIndex = ev.findPointerIndex(activePointerId)
if (pointerIndex < 0) {
return false
}
val pos = if (vertical) ev.getY(pointerIndex) else ev.getX(pointerIndex)
startDragging(pos)
}
MotionEvent.ACTION_POINTER_UP -> onSecondaryPointerUp(ev)
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
isBeingDragged = false
activePointerId = INVALID_POINTER
}
}
return isBeingDragged
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(ev: MotionEvent): Boolean {
val action = ev.actionMasked
val pointerIndex: Int
if (!isEnabled || canChildScroll()) {
return false
}
when (action) {
MotionEvent.ACTION_DOWN -> {
activePointerId = ev.getPointerId(0)
isBeingDragged = false
}
MotionEvent.ACTION_MOVE -> {
pointerIndex = ev.findPointerIndex(activePointerId)
if (pointerIndex < 0) {
//("Got ACTION_MOVE event but have an invalid active pointer id.")
return false
}
val pos = if (vertical) ev.getY(pointerIndex) else ev.getX(pointerIndex)
startDragging(pos)
if (isBeingDragged) {
val overscroll = (
if (vertical)
if (verticalPos == VerticalPosition.Top) pos - initialMotion else initialMotion - pos
else
if (horizontalPos == HorizontalPosition.Left) pos - initialMotion else initialMotion - pos
) * DRAG_RATE
if (overscroll > 0) {
parent.requestDisallowInterceptTouchEvent(true)
if (vertical){
val totalDragDistance = Resources.getSystem().displayMetrics.heightPixels / dragDivider
if (verticalPos == VerticalPosition.Top)
topBeingSwiped.invoke(overscroll / totalDragDistance)
else
bottomBeingSwiped.invoke(overscroll / totalDragDistance)
}
else {
val totalDragDistance = Resources.getSystem().displayMetrics.widthPixels / dragDivider
if (horizontalPos == HorizontalPosition.Left)
leftBeingSwiped.invoke(overscroll / totalDragDistance)
else
rightBeingSwiped.invoke(overscroll / totalDragDistance)
}
} else {
return false
}
}
}
MotionEvent.ACTION_POINTER_DOWN -> {
pointerIndex = ev.actionIndex
if (pointerIndex < 0) {
//("Got ACTION_POINTER_DOWN event but have an invalid action index.")
return false
}
activePointerId = ev.getPointerId(pointerIndex)
}
MotionEvent.ACTION_POINTER_UP -> onSecondaryPointerUp(ev)
MotionEvent.ACTION_UP -> {
if (vertical) {
topBeingSwiped.invoke(0f)
bottomBeingSwiped.invoke(0f)
} else {
rightBeingSwiped.invoke(0f)
leftBeingSwiped.invoke(0f)
}
pointerIndex = ev.findPointerIndex(activePointerId)
if (pointerIndex < 0) {
//("Got ACTION_UP event but don't have an active pointer id.")
return false
}
if (isBeingDragged) {
val pos = if (vertical) ev.getY(pointerIndex) else ev.getX(pointerIndex)
val overscroll = (
if (vertical)
if (verticalPos == VerticalPosition.Top) pos - initialMotion else initialMotion - pos
else
if (horizontalPos == HorizontalPosition.Left) pos - initialMotion else initialMotion - pos
) * DRAG_RATE
isBeingDragged = false
finishSpinner(overscroll)
}
activePointerId = INVALID_POINTER
return false
}
MotionEvent.ACTION_CANCEL -> return false
}
return true
}
private fun startDragging(pos: Float) {
val posDiff =
if ((vertical && verticalPos == VerticalPosition.Top) || (!vertical && horizontalPos == HorizontalPosition.Left))
pos - initialDown
else
initialDown - pos
if (posDiff > touchSlop && !isBeingDragged) {
initialMotion = initialDown + touchSlop
isBeingDragged = true
}
}
private fun finishSpinner(overscrollDistance: Float) {
if (vertical) {
val totalDragDistance = Resources.getSystem().displayMetrics.heightPixels / dragDivider
if (overscrollDistance > totalDragDistance)
if (verticalPos == VerticalPosition.Top)
onTopSwiped.invoke()
else
onBottomSwiped.invoke()
}
else {
val totalDragDistance = Resources.getSystem().displayMetrics.widthPixels / dragDivider
if (overscrollDistance > totalDragDistance)
if (horizontalPos == HorizontalPosition.Left)
onLeftSwiped.invoke()
else
onRightSwiped.invoke()
}
}
}

View file

@ -0,0 +1,77 @@
package ani.dantotsu.media.novel
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.databinding.BottomSheetBookBinding
import ani.dantotsu.loadImage
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.others.getSerialized
import ani.dantotsu.parsers.ShowResponse
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class BookDialog : BottomSheetDialogFragment() {
private var _binding: BottomSheetBookBinding? = null
private val binding get() = _binding!!
private val viewList = mutableListOf<View>()
private val viewModel by activityViewModels<MediaDetailsViewModel>()
private lateinit var novelName:String
private lateinit var novel: ShowResponse
private var source:Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
arguments?.let {
novelName = it.getString("novelName")!!
novel = it.getSerialized("novel")!!
source = it.getInt("source")
}
super.onCreate(savedInstanceState)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = BottomSheetBookBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.bookRecyclerView.layoutManager = LinearLayoutManager(requireContext())
viewModel.book.observe(viewLifecycleOwner) {
if(it!=null){
binding.itemBookTitle.text = it.name
binding.itemBookDesc.text = it.description
binding.itemBookImage.loadImage(it.img)
binding.bookRecyclerView.adapter = UrlAdapter(it.links, it, novelName)
}
}
lifecycleScope.launch(Dispatchers.IO) {
viewModel.loadBook(novel, source)
}
}
override fun onDestroy() {
_binding = null
super.onDestroy()
}
companion object {
fun newInstance(novelName:String, novel:ShowResponse, source: Int) : BookDialog{
val bundle = Bundle().apply {
putString("novelName", novelName)
putInt("source", source)
putSerializable("novel", novel)
}
return BookDialog().apply {
arguments = bundle
}
}
}
}

View file

@ -0,0 +1,70 @@
package ani.dantotsu.media.novel
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo.IME_ACTION_SEARCH
import android.view.inputmethod.InputMethodManager
import android.widget.ArrayAdapter
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.databinding.ItemNovelHeaderBinding
import ani.dantotsu.media.Media
import ani.dantotsu.parsers.NovelReadSources
class NovelReadAdapter(
private val media: Media,
private val fragment: NovelReadFragment,
private val novelReadSources: NovelReadSources
) : RecyclerView.Adapter<NovelReadAdapter.ViewHolder>() {
var progress: View? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NovelReadAdapter.ViewHolder {
val binding = ItemNovelHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
progress = binding.progress.root
return ViewHolder(binding)
}
private val imm = fragment.requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val binding = holder.binding
progress = binding.progress.root
fun search(): Boolean {
val query = binding.searchBarText.text.toString()
val source = media.selected!!.source.let { if (it >= novelReadSources.names.size) 0 else it }
fragment.source = source
binding.searchBarText.clearFocus()
imm.hideSoftInputFromWindow(binding.searchBarText.windowToken, 0)
fragment.search(query, source, true)
return true
}
val source = media.selected!!.source.let { if (it >= novelReadSources.names.size) 0 else it }
if (novelReadSources.names.isNotEmpty() && source in 0 until novelReadSources.names.size) {
binding.animeSource.setText(novelReadSources.names[source], false)
}
binding.animeSource.setAdapter(ArrayAdapter(fragment.requireContext(), R.layout.item_dropdown, novelReadSources.names))
binding.animeSource.setOnItemClickListener { _, _, i, _ ->
fragment.onSourceChange(i)
search()
}
binding.searchBarText.setText(fragment.searchQuery)
binding.searchBarText.setOnEditorActionListener { _, actionId, _ ->
return@setOnEditorActionListener when (actionId) {
IME_ACTION_SEARCH -> search()
else -> false
}
}
binding.searchBar.setEndIconOnClickListener { search() }
}
override fun getItemCount(): Int = 0
inner class ViewHolder(val binding: ItemNovelHeaderBinding) : RecyclerView.ViewHolder(binding.root)
}

View file

@ -0,0 +1,138 @@
package ani.dantotsu.media.novel
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
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.LinearLayoutManager
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.loadData
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.navBarHeight
import ani.dantotsu.saveData
import ani.dantotsu.settings.UserInterfaceSettings
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class NovelReadFragment : Fragment() {
private var _binding: FragmentAnimeWatchBinding? = null
private val binding get() = _binding!!
private val model: MediaDetailsViewModel by activityViewModels()
private lateinit var media: Media
var source = 0
lateinit var novelName: String
private lateinit var headerAdapter: NovelReadAdapter
private lateinit var novelResponseAdapter: NovelResponseAdapter
private var progress = View.VISIBLE
private var continueEp: Boolean = false
var loaded = false
val uiSettings = loadData("ui_settings", toast = false) ?: UserInterfaceSettings().apply { saveData("ui_settings", this) }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight)
binding.animeSourceRecycler.layoutManager = LinearLayoutManager(requireContext())
model.scrolledToTop.observe(viewLifecycleOwner) {
if (it) binding.animeSourceRecycler.scrollToPosition(0)
}
continueEp = model.continueMedia ?: false
model.getMedia().observe(viewLifecycleOwner) {
if (it != null) {
media = it
novelName = media.userPreferredName
progress = View.GONE
binding.mediaInfoProgressBar.visibility = progress
if (!loaded) {
val sel = media.selected
searchQuery = sel?.server ?: media.name ?: media.nameRomaji
headerAdapter = NovelReadAdapter(media, this, model.novelSources)
novelResponseAdapter = NovelResponseAdapter(this)
binding.animeSourceRecycler.adapter = ConcatAdapter(headerAdapter, novelResponseAdapter)
loaded = true
Handler(Looper.getMainLooper()).postDelayed({
search(searchQuery, sel?.source ?: 0, auto = sel?.server == null)
}, 100)
}
}
}
model.novelResponses.observe(viewLifecycleOwner) {
if (it != null) {
searching = false
novelResponseAdapter.submitList(it)
headerAdapter.progress?.visibility = View.GONE
}
}
}
lateinit var searchQuery: String
private var searching = false
fun search(query: String, source: Int, save: Boolean = false, auto: Boolean = false) {
if (!searching) {
novelResponseAdapter.clear()
searchQuery = query
headerAdapter.progress?.visibility = View.VISIBLE
lifecycleScope.launch(Dispatchers.IO) {
if (auto || query=="") model.autoSearchNovels(media)
else model.searchNovels(query, source)
}
searching = true
if (save) {
val selected = model.loadSelected(media)
selected.server = query
model.saveSelected(media.id, selected, requireActivity())
}
}
}
fun onSourceChange(i: Int) {
val selected = model.loadSelected(media)
selected.source = i
source = i
selected.server = null
model.saveSelected(media.id, selected, requireActivity())
media.selected = selected
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentAnimeWatchBinding.inflate(inflater, container, false)
return _binding?.root
}
override fun onDestroy() {
model.mangaReadSources?.flushText()
super.onDestroy()
}
private 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,57 @@
package ani.dantotsu.media.novel
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.databinding.ItemNovelResponseBinding
import ani.dantotsu.parsers.ShowResponse
import ani.dantotsu.setAnimation
import com.bumptech.glide.Glide
import com.bumptech.glide.load.model.GlideUrl
class NovelResponseAdapter(val fragment: NovelReadFragment) : RecyclerView.Adapter<NovelResponseAdapter.ViewHolder>() {
val list: MutableList<ShowResponse> = mutableListOf()
inner class ViewHolder(val binding: ItemNovelResponseBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val bind = ItemNovelResponseBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(bind)
}
override fun getItemCount(): Int = list.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val binding = holder.binding
val novel = list[position]
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
val cover = GlideUrl(novel.coverUrl.url){ novel.coverUrl.headers }
Glide.with(binding.itemEpisodeImage).load(cover).override(400,0).into(binding.itemEpisodeImage)
binding.itemEpisodeTitle.text = novel.name
binding.itemEpisodeFiller.text = novel.extra?.get("0") ?: ""
binding.itemEpisodeDesc2.text = novel.extra?.get("1") ?: ""
val desc = novel.extra?.get("2")
binding.itemEpisodeDesc.visibility = if (desc != null && desc.trim(' ') != "") View.VISIBLE else View.GONE
binding.itemEpisodeDesc.text = desc ?: ""
binding.root.setOnClickListener {
BookDialog.newInstance(fragment.novelName, novel, fragment.source)
.show(fragment.parentFragmentManager, "dialog")
}
}
fun submitList(it: List<ShowResponse>) {
val old = list.size
list.addAll(it)
notifyItemRangeInserted(old, it.size)
}
fun clear() {
val size = list.size
list.clear()
notifyItemRangeRemoved(0, size)
}
}

View file

@ -0,0 +1,54 @@
package ani.dantotsu.media.novel
import android.annotation.SuppressLint
import android.view.HapticFeedbackConstants
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.FileUrl
import ani.dantotsu.copyToClipboard
import ani.dantotsu.databinding.ItemUrlBinding
import ani.dantotsu.others.Download.download
import ani.dantotsu.parsers.Book
import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.tryWith
class UrlAdapter(private val urls: List<FileUrl>, val book: Book, val novel: String) :
RecyclerView.Adapter<UrlAdapter.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 url = urls[position]
binding.urlQuality.text = url.url
binding.urlDownload.visibility = View.VISIBLE
}
override fun getItemCount(): Int = urls.size
inner class UrlViewHolder(val binding: ItemUrlBinding) : RecyclerView.ViewHolder(binding.root) {
init {
itemView.setSafeOnClickListener {
tryWith(true) {
binding.urlDownload.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
download(
itemView.context,
book,
bindingAdapterPosition,
novel
)
}
}
itemView.setOnLongClickListener {
val file = urls[bindingAdapterPosition]
copyToClipboard(file.url, true)
true
}
}
}
}

View file

@ -0,0 +1,435 @@
package ani.dantotsu.media.novel.novelreader
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.content.pm.ActivityInfo
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.util.Base64
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.view.animation.OvershootInterpolator
import android.widget.AdapterView
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.core.net.toUri
import androidx.core.view.GestureDetectorCompat
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import ani.dantotsu.GesturesListener
import ani.dantotsu.NoPaddingArrayAdapter
import ani.dantotsu.R
import ani.dantotsu.databinding.ActivityNovelReaderBinding
import ani.dantotsu.hideSystemBars
import ani.dantotsu.loadData
import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.saveData
import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.settings.CurrentNovelReaderSettings
import ani.dantotsu.settings.CurrentReaderSettings
import ani.dantotsu.settings.NovelReaderSettings
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.snackString
import ani.dantotsu.tryWith
import com.google.android.material.slider.Slider
import com.vipulog.ebookreader.Book
import com.vipulog.ebookreader.EbookReaderEventListener
import com.vipulog.ebookreader.ReaderError
import com.vipulog.ebookreader.ReaderFlow
import com.vipulog.ebookreader.ReaderTheme
import com.vipulog.ebookreader.RelocationInfo
import com.vipulog.ebookreader.TocItem
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileOutputStream
import java.util.*
import kotlin.math.min
import kotlin.properties.Delegates
class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
private lateinit var binding: ActivityNovelReaderBinding
private val scope = lifecycleScope
lateinit var settings: NovelReaderSettings
private lateinit var uiSettings: UserInterfaceSettings
private var notchHeight: Int? = null
var loaded = false
private lateinit var book: Book
private lateinit var sanitizedBookId: String
private lateinit var toc: List<TocItem>
private var currentTheme: ReaderTheme? = null
private var currentCfi: String? = null
val themes = ArrayList<ReaderTheme>()
init {
val forestTheme = ReaderTheme(
name = "Forest",
lightFg = Color.parseColor("#000000"),
lightBg = Color.parseColor("#E7F6E7"),
lightLink = Color.parseColor("#008000"),
darkFg = Color.parseColor("#FFFFFF"),
darkBg = Color.parseColor("#084D08"),
darkLink = Color.parseColor("#00B200")
)
val oceanTheme = ReaderTheme(
name = "Ocean",
lightFg = Color.parseColor("#000000"),
lightBg = Color.parseColor("#E4F0F9"),
lightLink = Color.parseColor("#007BFF"),
darkFg = Color.parseColor("#FFFFFF"),
darkBg = Color.parseColor("#0A2E3E"),
darkLink = Color.parseColor("#00A5E4")
)
val sunsetTheme = ReaderTheme(
name = "Sunset",
lightFg = Color.parseColor("#000000"),
lightBg = Color.parseColor("#FDEDE6"),
lightLink = Color.parseColor("#FF5733"),
darkFg = Color.parseColor("#FFFFFF"),
darkBg = Color.parseColor("#441517"),
darkLink = Color.parseColor("#FF6B47")
)
val desertTheme = ReaderTheme(
name = "Desert",
lightFg = Color.parseColor("#000000"),
lightBg = Color.parseColor("#FDF5E6"),
lightLink = Color.parseColor("#FFA500"),
darkFg = Color.parseColor("#FFFFFF"),
darkBg = Color.parseColor("#523B19"),
darkLink = Color.parseColor("#FFBF00")
)
val galaxyTheme = ReaderTheme(
name = "Galaxy",
lightFg = Color.parseColor("#000000"),
lightBg = Color.parseColor("#F2F2F2"),
lightLink = Color.parseColor("#800080"),
darkFg = Color.parseColor("#FFFFFF"),
darkBg = Color.parseColor("#000000"),
darkLink = Color.parseColor("#B300B3")
)
themes.addAll(listOf(forestTheme, oceanTheme, sunsetTheme, desertTheme, galaxyTheme))
}
override fun onAttachedToWindow() {
checkNotch()
super.onAttachedToWindow()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityNovelReaderBinding.inflate(layoutInflater)
setContentView(binding.root)
settings = loadData("novel_reader_settings", this)
?: NovelReaderSettings().apply { saveData("novel_reader_settings", this) }
uiSettings = loadData("ui_settings", this)
?: UserInterfaceSettings().also { saveData("ui_settings", it) }
controllerDuration = (uiSettings.animationSpeed * 200).toLong()
setupViews()
setupBackPressedHandler()
}
@SuppressLint("ClickableViewAccessibility")
private fun setupViews() {
scope.launch { binding.bookReader.openBook(intent.data!!) }
binding.bookReader.setEbookReaderListener(this)
binding.novelReaderBack.setOnClickListener { finish() }
binding.novelReaderSettings.setSafeOnClickListener {
NovelReaderSettingsDialogFragment.newInstance().show(supportFragmentManager, NovelReaderSettingsDialogFragment.TAG)
}
val gestureDetector = GestureDetectorCompat(this, object : GesturesListener() {
override fun onSingleClick(event: MotionEvent) {
handleController()
}
})
binding.bookReader.setOnTouchListener { _, event ->
if (event != null) tryWith { gestureDetector.onTouchEvent(event) } ?: false
else false
}
binding.novelReaderNextChap.setOnClickListener { binding.novelReaderNextChapter.performClick() }
binding.novelReaderNextChapter.setOnClickListener { binding.bookReader.next() }
binding.novelReaderPrevChap.setOnClickListener { binding.novelReaderPreviousChapter.performClick() }
binding.novelReaderPreviousChapter.setOnClickListener { binding.bookReader.prev() }
binding.novelReaderSlider.addOnSliderTouchListener(object : Slider.OnSliderTouchListener {
override fun onStartTrackingTouch(slider: Slider) {
}
override fun onStopTrackingTouch(slider: Slider) {
binding.bookReader.gotoFraction(slider.value.toDouble())
}
})
onVolumeUp = { binding.novelReaderNextChapter.performClick() }
onVolumeDown = { binding.novelReaderPreviousChapter.performClick() }
}
private fun setupBackPressedHandler() {
var lastBackPressedTime: Long = 0
val doublePressInterval: Long = 2000
onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (binding.bookReader.canGoBack()) {
binding.bookReader.goBack()
} else {
if (lastBackPressedTime + doublePressInterval > System.currentTimeMillis()) {
finish()
} else {
snackString("Press back again to exit")
lastBackPressedTime = System.currentTimeMillis()
}
}
}
})
}
override fun onBookLoadFailed(error: ReaderError) {
snackString(error.message)
finish()
}
override fun onBookLoaded(book: Book) {
this.book = book
val bookId = book.identifier!!
toc = book.toc
val illegalCharsRegex = Regex("[^a-zA-Z0-9._-]")
sanitizedBookId = bookId.replace(illegalCharsRegex, "_")
binding.novelReaderTitle.text = book.title
binding.novelReaderSource.text = book.author?.joinToString(", ")
val tocLabels = book.toc.map { it.label ?: "" }
binding.novelReaderChapterSelect.adapter = NoPaddingArrayAdapter(this, R.layout.item_dropdown, tocLabels)
binding.novelReaderChapterSelect.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
binding.bookReader.goto(book.toc[position].href)
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
}
binding.bookReader.getAppearance {
currentTheme = it
themes.add(0, it)
settings.default = loadData("${sanitizedBookId}_current_settings") ?: settings.default
applySettings()
}
val cfi = loadData<String>("${sanitizedBookId}_progress")
cfi?.let { binding.bookReader.goto(it) }
binding.progress.visibility = View.GONE
loaded = true
}
override fun onProgressChanged(info: RelocationInfo) {
currentCfi = info.cfi
binding.novelReaderSlider.value = info.fraction.toFloat()
val pos = info.tocItem?.let { item -> toc.indexOfFirst { it == item } }
if (pos != null) binding.novelReaderChapterSelect.setSelection(pos)
saveData("${sanitizedBookId}_progress", info.cfi)
}
override fun onImageSelected(base64String: String) {
scope.launch(Dispatchers.IO) {
val base64Data = base64String.substringAfter(",")
val imageBytes: ByteArray = Base64.decode(base64Data, Base64.DEFAULT)
val imageFile = File(cacheDir, "/images/ln.jpg")
imageFile.parentFile?.mkdirs()
imageFile.createNewFile()
FileOutputStream(imageFile).use { outputStream -> outputStream.write(imageBytes) }
ImageViewDialog.newInstance(
this@NovelReaderActivity,
book.title,
imageFile.toUri().toString()
)
}
}
override fun onTextSelectionModeChange(mode: Boolean) {
// TODO: Show ui for adding annotations and notes
}
private var onVolumeUp: (() -> Unit)? = null
private var onVolumeDown: (() -> Unit)? = null
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
return when (event.keyCode) {
KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_PAGE_UP -> {
if (event.keyCode == KeyEvent.KEYCODE_VOLUME_UP)
if (!settings.default.volumeButtons)
return false
if (event.action == KeyEvent.ACTION_DOWN) {
onVolumeUp?.invoke()
true
} else false
}
KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_PAGE_DOWN -> {
if (event.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN)
if (!settings.default.volumeButtons)
return false
if (event.action == KeyEvent.ACTION_DOWN) {
onVolumeDown?.invoke()
true
} else false
}
else -> {
super.dispatchKeyEvent(event)
}
}
}
fun applySettings() {
saveData("${sanitizedBookId}_current_settings", settings.default)
hideBars()
currentTheme = themes.first { it.name.equals(settings.default.currentThemeName, ignoreCase = true) }
when (settings.default.layout) {
CurrentNovelReaderSettings.Layouts.PAGED -> {
currentTheme?.flow = ReaderFlow.PAGINATED
}
CurrentNovelReaderSettings.Layouts.SCROLLED -> {
currentTheme?.flow = ReaderFlow.SCROLLED
}
}
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER
when (settings.default.dualPageMode) {
CurrentReaderSettings.DualPageModes.No -> currentTheme?.maxColumnCount = 1
CurrentReaderSettings.DualPageModes.Automatic -> currentTheme?.maxColumnCount = 2
CurrentReaderSettings.DualPageModes.Force -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
}
currentTheme?.lineHeight = settings.default.lineHeight
currentTheme?.gap = settings.default.margin
currentTheme?.maxInlineSize = settings.default.maxInlineSize
currentTheme?.maxBlockSize = settings.default.maxBlockSize
currentTheme?.useDark = settings.default.useDarkTheme
currentTheme?.let { binding.bookReader.setAppearance(it) }
if (settings.default.keepScreenOn) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
else window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
// region Handle Controls
private var isContVisible = false
private var isAnimating = false
private var goneTimer = Timer()
private var controllerDuration by Delegates.notNull<Long>()
private val overshoot = OvershootInterpolator(1.4f)
fun gone() {
goneTimer.cancel()
goneTimer.purge()
val timerTask: TimerTask = object : TimerTask() {
override fun run() {
if (!isContVisible) binding.novelReaderCont.post {
binding.novelReaderCont.visibility = View.GONE
isAnimating = false
}
}
}
goneTimer = Timer()
goneTimer.schedule(timerTask, controllerDuration)
}
fun handleController(shouldShow: Boolean? = null) {
if (!loaded) return
if (!settings.showSystemBars) {
hideBars()
applyNotchMargin()
}
shouldShow?.apply { isContVisible = !this }
if (isContVisible) {
isContVisible = false
if (!isAnimating) {
isAnimating = true
ObjectAnimator.ofFloat(binding.novelReaderCont, "alpha", 1f, 0f).setDuration(controllerDuration).start()
ObjectAnimator.ofFloat(binding.novelReaderBottomCont, "translationY", 0f, 128f)
.apply { interpolator = overshoot;duration = controllerDuration;start() }
ObjectAnimator.ofFloat(binding.novelReaderTopLayout, "translationY", 0f, -128f)
.apply { interpolator = overshoot;duration = controllerDuration;start() }
}
gone()
} else {
isContVisible = true
binding.novelReaderCont.visibility = View.VISIBLE
ObjectAnimator.ofFloat(binding.novelReaderCont, "alpha", 0f, 1f).setDuration(controllerDuration).start()
ObjectAnimator.ofFloat(binding.novelReaderTopLayout, "translationY", -128f, 0f)
.apply { interpolator = overshoot;duration = controllerDuration;start() }
ObjectAnimator.ofFloat(binding.novelReaderBottomCont, "translationY", 128f, 0f)
.apply { interpolator = overshoot;duration = controllerDuration;start() }
}
}
// endregion Handle Controls
private fun checkNotch() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && !settings.showSystemBars) {
val displayCutout = window.decorView.rootWindowInsets.displayCutout
if (displayCutout != null) {
if (displayCutout.boundingRects.size > 0) {
notchHeight = min(displayCutout.boundingRects[0].width(), displayCutout.boundingRects[0].height())
applyNotchMargin()
}
}
}
}
private fun applyNotchMargin() {
binding.novelReaderTopLayout.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = notchHeight ?: return
}
}
private fun hideBars() {
if (!settings.showSystemBars) hideSystemBars()
}
}

View file

@ -0,0 +1,196 @@
package ani.dantotsu.media.novel.novelreader
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.NoPaddingArrayAdapter
import ani.dantotsu.R
import ani.dantotsu.databinding.BottomSheetCurrentNovelReaderSettingsBinding
import ani.dantotsu.settings.CurrentNovelReaderSettings
import ani.dantotsu.settings.CurrentReaderSettings
class NovelReaderSettingsDialogFragment : BottomSheetDialogFragment() {
private var _binding: BottomSheetCurrentNovelReaderSettingsBinding? = null
private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = BottomSheetCurrentNovelReaderSettingsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val activity = requireActivity() as NovelReaderActivity
val settings = activity.settings.default
val themeLabels = activity.themes.map { it.name }
binding.themeSelect.adapter = NoPaddingArrayAdapter(activity, R.layout.item_dropdown, themeLabels)
binding.themeSelect.setSelection(themeLabels.indexOfFirst { it == settings.currentThemeName })
binding.themeSelect.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
settings.currentThemeName = themeLabels[position]
activity.applySettings()
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
}
val layoutList = listOf(
binding.paged,
binding.continuous
)
binding.layoutText.text = settings.layout.string
var selected = layoutList[settings.layout.ordinal]
selected.alpha = 1f
layoutList.forEachIndexed { index, imageButton ->
imageButton.setOnClickListener {
selected.alpha = 0.33f
selected = imageButton
selected.alpha = 1f
settings.layout = CurrentNovelReaderSettings.Layouts[index]?:CurrentNovelReaderSettings.Layouts.PAGED
binding.layoutText.text = settings.layout.string
activity.applySettings()
}
}
val dualList = listOf(
binding.dualNo,
binding.dualAuto,
binding.dualForce
)
binding.dualPageText.text = settings.dualPageMode.toString()
var selectedDual = dualList[settings.dualPageMode.ordinal]
selectedDual.alpha = 1f
dualList.forEachIndexed { index, imageButton ->
imageButton.setOnClickListener {
selectedDual.alpha = 0.33f
selectedDual = imageButton
selectedDual.alpha = 1f
settings.dualPageMode = CurrentReaderSettings.DualPageModes[index] ?: CurrentReaderSettings.DualPageModes.Automatic
binding.dualPageText.text = settings.dualPageMode.toString()
activity.applySettings()
}
}
binding.lineHeight.setText(settings.lineHeight.toString())
binding.lineHeight.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
val value = binding.lineHeight.text.toString().toFloatOrNull() ?: 1.4f
settings.lineHeight = value
binding.lineHeight.setText(value.toString())
activity.applySettings()
}
}
binding.incrementLineHeight.setOnClickListener {
val value = binding.lineHeight.text.toString().toFloatOrNull() ?: 1.4f
settings.lineHeight = value + 0.1f
binding.lineHeight.setText(settings.lineHeight.toString())
activity.applySettings()
}
binding.decrementLineHeight.setOnClickListener {
val value = binding.lineHeight.text.toString().toFloatOrNull() ?: 1.4f
settings.lineHeight = value - 0.1f
binding.lineHeight.setText(settings.lineHeight.toString())
activity.applySettings()
}
binding.margin.setText(settings.margin.toString())
binding.margin.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
val value = binding.margin.text.toString().toFloatOrNull() ?: 0.06f
settings.margin = value
binding.margin.setText(value.toString())
activity.applySettings()
}
}
binding.incrementMargin.setOnClickListener {
val value = binding.margin.text.toString().toFloatOrNull() ?: 0.06f
settings.margin = value + 0.01f
binding.margin.setText(settings.margin.toString())
activity.applySettings()
}
binding.decrementMargin.setOnClickListener {
val value = binding.margin.text.toString().toFloatOrNull() ?: 0.06f
settings.margin = value - 0.01f
binding.margin.setText(settings.margin.toString())
activity.applySettings()
}
binding.maxInlineSize.setText(settings.maxInlineSize.toString())
binding.maxInlineSize.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
val value = binding.maxInlineSize.text.toString().toIntOrNull() ?: 720
settings.maxInlineSize = value
binding.maxInlineSize.setText(value.toString())
activity.applySettings()
}
}
binding.incrementMaxInlineSize.setOnClickListener {
val value = binding.maxInlineSize.text.toString().toIntOrNull() ?: 720
settings.maxInlineSize = value + 10
binding.maxInlineSize.setText(settings.maxInlineSize.toString())
activity.applySettings()
}
binding.decrementMaxInlineSize.setOnClickListener {
val value = binding.maxInlineSize.text.toString().toIntOrNull() ?: 720
settings.maxInlineSize = value - 10
binding.maxInlineSize.setText(settings.maxInlineSize.toString())
activity.applySettings()
}
binding.maxBlockSize.setText(settings.maxBlockSize.toString())
binding.maxBlockSize.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
val value = binding.maxBlockSize.text.toString().toIntOrNull() ?: 720
settings.maxBlockSize = value
binding.maxBlockSize.setText(value.toString())
activity.applySettings()
}
}
binding.useDarkTheme.isChecked = settings.useDarkTheme
binding.useDarkTheme.setOnCheckedChangeListener { _,isChecked ->
settings.useDarkTheme = isChecked
activity.applySettings()
}
binding.keepScreenOn.isChecked = settings.keepScreenOn
binding.keepScreenOn.setOnCheckedChangeListener { _,isChecked ->
settings.keepScreenOn = isChecked
activity.applySettings()
}
binding.volumeButton.isChecked = settings.volumeButtons
binding.volumeButton.setOnCheckedChangeListener { _,isChecked ->
settings.volumeButtons = isChecked
activity.applySettings()
}
}
override fun onDestroy() {
_binding = null
super.onDestroy()
}
companion object{
fun newInstance() = NovelReaderSettingsDialogFragment()
const val TAG = "NovelReaderSettingsDialogFragment"
}
}

View file

@ -0,0 +1,94 @@
package ani.dantotsu.media.user
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.View
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.PopupMenu
import androidx.core.content.ContextCompat
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.lifecycleScope
import ani.dantotsu.R
import ani.dantotsu.Refresh
import ani.dantotsu.databinding.ActivityListBinding
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class ListActivity : AppCompatActivity() {
private lateinit var binding: ActivityListBinding
private val scope = lifecycleScope
private var selectedTabIdx = 0
@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityListBinding.inflate(layoutInflater)
setContentView(binding.root)
window.statusBarColor = ContextCompat.getColor(this, R.color.nav_bg)
val anime = intent.getBooleanExtra("anime", true)
binding.listTitle.text = intent.getStringExtra("username") + "'s " + (if (anime) "Anime" else "Manga") + " List"
binding.listTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
this@ListActivity.selectedTabIdx = tab?.position ?: 0
}
override fun onTabUnselected(tab: TabLayout.Tab?) { }
override fun onTabReselected(tab: TabLayout.Tab?) { }
})
val model: ListViewModel by viewModels()
model.getLists().observe(this) {
val defaultKeys = listOf("Reading", "Watching", "Completed", "Paused", "Dropped", "Planning", "Favourites", "Rewatching", "Rereading", "All")
val userKeys : Array<String> = resources.getStringArray(R.array.keys)
if (it != null) {
binding.listProgressBar.visibility = View.GONE
binding.listViewPager.adapter = ListViewPagerAdapter(it.size, false,this)
val keys = it.keys.toList().map { key -> userKeys.getOrNull(defaultKeys.indexOf(key))?: key }
val values = it.values.toList()
val savedTab = this.selectedTabIdx
TabLayoutMediator(binding.listTabLayout, binding.listViewPager) { tab, position ->
tab.text = "${keys[position]} (${values[position].size})"
}.attach()
binding.listViewPager.setCurrentItem(savedTab, false)
}
}
val live = Refresh.activity.getOrPut(this.hashCode()) { MutableLiveData(true) }
live.observe(this) {
if (it) {
scope.launch {
withContext(Dispatchers.IO) { model.loadLists(anime, intent.getIntExtra("userId", 0)) }
live.postValue(false)
}
}
}
binding.listSort.setOnClickListener {
val popup = PopupMenu(this, it)
popup.setOnMenuItemClickListener { item ->
val sort = when (item.itemId) {
R.id.score -> "score"
R.id.title -> "title"
R.id.updated -> "updatedAt"
R.id.release -> "release"
else -> null
}
binding.listProgressBar.visibility = View.VISIBLE
binding.listViewPager.adapter = null
scope.launch {
withContext(Dispatchers.IO) { model.loadLists(anime, intent.getIntExtra("userId", 0), sort) }
}
true
}
popup.inflate(R.menu.list_sort_menu)
popup.show()
}
}
}

View file

@ -0,0 +1,86 @@
package ani.dantotsu.media.user
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.GridLayoutManager
import ani.dantotsu.databinding.FragmentListBinding
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaAdaptor
import ani.dantotsu.media.OtherDetailsViewModel
class ListFragment : Fragment() {
private var _binding: FragmentListBinding? = null
private val binding get() = _binding!!
private var pos: Int? = null
private var calendar = false
private var grid: Boolean? = null
private var list: MutableList<Media>? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
pos = it.getInt("list")
calendar = it.getBoolean("calendar")
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentListBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val screenWidth = resources.displayMetrics.run { widthPixels / density }
fun update() {
if (grid != null && list != null) {
val adapter = MediaAdaptor(if (grid!!) 0 else 1, list!!, requireActivity(), true)
binding.listRecyclerView.layoutManager =
GridLayoutManager(requireContext(), if (grid!!) (screenWidth / 124f).toInt() else 1)
binding.listRecyclerView.adapter = adapter
}
}
if (calendar) {
val model: OtherDetailsViewModel by activityViewModels()
model.getCalendar().observe(viewLifecycleOwner) {
if (it != null) {
list = it.values.toList().getOrNull(pos!!)
update()
}
}
grid = true
} else {
val model: ListViewModel by activityViewModels()
model.getLists().observe(viewLifecycleOwner) {
if (it != null) {
list = it.values.toList().getOrNull(pos!!)
update()
}
}
model.grid.observe(viewLifecycleOwner) {
grid = it
update()
}
}
}
companion object {
fun newInstance(pos: Int, calendar: Boolean = false): ListFragment =
ListFragment().apply {
arguments = Bundle().apply {
putInt("list", pos)
putBoolean("calendar", calendar)
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View file

@ -0,0 +1,21 @@
package ani.dantotsu.media.user
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.loadData
import ani.dantotsu.media.Media
import ani.dantotsu.tryWithSuspend
class ListViewModel : ViewModel() {
var grid = MutableLiveData(loadData<Boolean>("listGrid") ?: true)
private val lists = MutableLiveData<MutableMap<String, ArrayList<Media>>>()
fun getLists(): LiveData<MutableMap<String, ArrayList<Media>>> = lists
suspend fun loadLists(anime: Boolean, userId: Int, sortOrder: String? = null) {
tryWithSuspend {
lists.postValue(Anilist.query.getMediaLists(anime, userId, sortOrder))
}
}
}

View file

@ -0,0 +1,11 @@
package ani.dantotsu.media.user
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
class ListViewPagerAdapter(private val size: Int, private val calendar: Boolean, fragment: FragmentActivity) :
FragmentStateAdapter(fragment) {
override fun getItemCount(): Int = size
override fun createFragment(position: Int): Fragment = ListFragment.newInstance(position, calendar)
}