Initial commit
This commit is contained in:
commit
21bfbfb139
520 changed files with 47819 additions and 0 deletions
9
app/src/main/java/ani/dantotsu/media/Author.kt
Normal file
9
app/src/main/java/ani/dantotsu/media/Author.kt
Normal 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
|
111
app/src/main/java/ani/dantotsu/media/AuthorActivity.kt
Normal file
111
app/src/main/java/ani/dantotsu/media/AuthorActivity.kt
Normal 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()
|
||||
}
|
||||
}
|
70
app/src/main/java/ani/dantotsu/media/CalendarActivity.kt
Normal file
70
app/src/main/java/ani/dantotsu/media/CalendarActivity.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
18
app/src/main/java/ani/dantotsu/media/Character.kt
Normal file
18
app/src/main/java/ani/dantotsu/media/Character.kt
Normal 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
|
56
app/src/main/java/ani/dantotsu/media/CharacterAdapter.kt
Normal file
56
app/src/main/java/ani/dantotsu/media/CharacterAdapter.kt
Normal 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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
130
app/src/main/java/ani/dantotsu/media/CharacterDetailsActivity.kt
Normal file
130
app/src/main/java/ani/dantotsu/media/CharacterDetailsActivity.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
60
app/src/main/java/ani/dantotsu/media/GenreActivity.kt
Normal file
60
app/src/main/java/ani/dantotsu/media/GenreActivity.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
71
app/src/main/java/ani/dantotsu/media/GenreAdapter.kt
Normal file
71
app/src/main/java/ani/dantotsu/media/GenreAdapter.kt
Normal 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)
|
||||
}
|
||||
}
|
118
app/src/main/java/ani/dantotsu/media/Media.kt
Normal file
118
app/src/main/java/ani/dantotsu/media/Media.kt
Normal 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
|
||||
}
|
305
app/src/main/java/ani/dantotsu/media/MediaAdaptor.kt
Normal file
305
app/src/main/java/ani/dantotsu/media/MediaAdaptor.kt
Normal 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
|
||||
}
|
||||
}
|
456
app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt
Normal file
456
app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
279
app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt
Normal file
279
app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
490
app/src/main/java/ani/dantotsu/media/MediaInfoFragment.kt
Normal file
490
app/src/main/java/ani/dantotsu/media/MediaInfoFragment.kt
Normal 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()
|
||||
}
|
||||
}
|
267
app/src/main/java/ani/dantotsu/media/MediaListDialogFragment.kt
Normal file
267
app/src/main/java/ani/dantotsu/media/MediaListDialogFragment.kt
Normal 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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
59
app/src/main/java/ani/dantotsu/media/ProgressAdapter.kt
Normal file
59
app/src/main/java/ani/dantotsu/media/ProgressAdapter.kt
Normal 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 }
|
||||
}
|
||||
}
|
||||
}
|
195
app/src/main/java/ani/dantotsu/media/SearchActivity.kt
Normal file
195
app/src/main/java/ani/dantotsu/media/SearchActivity.kt
Normal 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)
|
||||
}
|
||||
}
|
200
app/src/main/java/ani/dantotsu/media/SearchAdapter.kt
Normal file
200
app/src/main/java/ani/dantotsu/media/SearchAdapter.kt
Normal 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
|
||||
}
|
||||
}
|
||||
|
191
app/src/main/java/ani/dantotsu/media/SearchFilterBottomDialog.kt
Normal file
191
app/src/main/java/ani/dantotsu/media/SearchFilterBottomDialog.kt
Normal 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()
|
||||
}
|
||||
|
||||
}
|
15
app/src/main/java/ani/dantotsu/media/Selected.kt
Normal file
15
app/src/main/java/ani/dantotsu/media/Selected.kt
Normal 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
|
10
app/src/main/java/ani/dantotsu/media/Source.kt
Normal file
10
app/src/main/java/ani/dantotsu/media/Source.kt
Normal 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
|
51
app/src/main/java/ani/dantotsu/media/SourceAdapter.kt
Normal file
51
app/src/main/java/ani/dantotsu/media/SourceAdapter.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
9
app/src/main/java/ani/dantotsu/media/Studio.kt
Normal file
9
app/src/main/java/ani/dantotsu/media/Studio.kt
Normal 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
|
111
app/src/main/java/ani/dantotsu/media/StudioActivity.kt
Normal file
111
app/src/main/java/ani/dantotsu/media/StudioActivity.kt
Normal 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()
|
||||
}
|
||||
}
|
21
app/src/main/java/ani/dantotsu/media/TitleAdapter.kt
Normal file
21
app/src/main/java/ani/dantotsu/media/TitleAdapter.kt
Normal 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
|
||||
}
|
29
app/src/main/java/ani/dantotsu/media/anime/Anime.kt
Normal file
29
app/src/main/java/ani/dantotsu/media/anime/Anime.kt
Normal 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
|
|
@ -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)
|
||||
}
|
||||
}
|
271
app/src/main/java/ani/dantotsu/media/anime/AnimeWatchAdapter.kt
Normal file
271
app/src/main/java/ani/dantotsu/media/anime/AnimeWatchAdapter.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
315
app/src/main/java/ani/dantotsu/media/anime/AnimeWatchFragment.kt
Normal file
315
app/src/main/java/ani/dantotsu/media/anime/AnimeWatchFragment.kt
Normal 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()
|
||||
}
|
||||
|
||||
}
|
26
app/src/main/java/ani/dantotsu/media/anime/Episode.kt
Normal file
26
app/src/main/java/ani/dantotsu/media/anime/Episode.kt
Normal 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
|
||||
|
||||
|
221
app/src/main/java/ani/dantotsu/media/anime/EpisodeAdapters.kt
Normal file
221
app/src/main/java/ani/dantotsu/media/anime/EpisodeAdapters.kt
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
|
1622
app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt
Normal file
1622
app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
28
app/src/main/java/ani/dantotsu/media/anime/VideoCache.kt
Normal file
28
app/src/main/java/ani/dantotsu/media/anime/VideoCache.kt
Normal 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
|
||||
}
|
||||
}
|
12
app/src/main/java/ani/dantotsu/media/manga/Manga.kt
Normal file
12
app/src/main/java/ani/dantotsu/media/manga/Manga.kt
Normal 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
|
30
app/src/main/java/ani/dantotsu/media/manga/MangaChapter.kt
Normal file
30
app/src/main/java/ani/dantotsu/media/manga/MangaChapter.kt
Normal 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
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
226
app/src/main/java/ani/dantotsu/media/manga/MangaReadAdapter.kt
Normal file
226
app/src/main/java/ani/dantotsu/media/manga/MangaReadAdapter.kt
Normal 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)
|
||||
}
|
278
app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt
Normal file
278
app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt
Normal 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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
255
app/src/main/java/ani/dantotsu/media/manga/mangareader/Swipy.kt
Normal file
255
app/src/main/java/ani/dantotsu/media/manga/mangareader/Swipy.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
77
app/src/main/java/ani/dantotsu/media/novel/BookDialog.kt
Normal file
77
app/src/main/java/ani/dantotsu/media/novel/BookDialog.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
138
app/src/main/java/ani/dantotsu/media/novel/NovelReadFragment.kt
Normal file
138
app/src/main/java/ani/dantotsu/media/novel/NovelReadFragment.kt
Normal 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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
54
app/src/main/java/ani/dantotsu/media/novel/UrlAdapter.kt
Normal file
54
app/src/main/java/ani/dantotsu/media/novel/UrlAdapter.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
94
app/src/main/java/ani/dantotsu/media/user/ListActivity.kt
Normal file
94
app/src/main/java/ani/dantotsu/media/user/ListActivity.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
86
app/src/main/java/ani/dantotsu/media/user/ListFragment.kt
Normal file
86
app/src/main/java/ani/dantotsu/media/user/ListFragment.kt
Normal 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
|
||||
}
|
||||
}
|
21
app/src/main/java/ani/dantotsu/media/user/ListViewModel.kt
Normal file
21
app/src/main/java/ani/dantotsu/media/user/ListViewModel.kt
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue