Initial commit
This commit is contained in:
commit
21bfbfb139
520 changed files with 47819 additions and 0 deletions
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()
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue