Initial commit

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,226 @@
package ani.dantotsu.media.manga
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.LinearLayout
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.*
import ani.dantotsu.media.anime.handleProgress
import ani.dantotsu.databinding.ItemAnimeWatchBinding
import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.SourceSearchDialogFragment
import ani.dantotsu.parsers.MangaReadSources
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
import com.google.android.material.chip.Chip
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
class MangaReadAdapter(
private val media: Media,
private val fragment: MangaReadFragment,
private val mangaReadSources: MangaReadSources
) : RecyclerView.Adapter<MangaReadAdapter.ViewHolder>() {
var subscribe: MediaDetailsActivity.PopImageButton? = null
private var _binding: ItemAnimeWatchBinding? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val bind = ItemAnimeWatchBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(bind)
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val binding = holder.binding
_binding = binding
binding.sourceTitle.setText(R.string.chaps)
//Wrong Title
binding.animeSourceSearch.setOnClickListener {
SourceSearchDialogFragment().show(fragment.requireActivity().supportFragmentManager, null)
}
//Source Selection
val source = media.selected!!.source.let { if (it >= mangaReadSources.names.size) 0 else it }
if (mangaReadSources.names.isNotEmpty() && source in 0 until mangaReadSources.names.size) {
binding.animeSource.setText(mangaReadSources.names[source])
mangaReadSources[source].apply {
binding.animeSourceTitle.text = showUserText
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
}
}
binding.animeSource.setAdapter(ArrayAdapter(fragment.requireContext(), R.layout.item_dropdown, mangaReadSources.names))
binding.animeSourceTitle.isSelected = true
binding.animeSource.setOnItemClickListener { _, _, i, _ ->
fragment.onSourceChange(i).apply {
binding.animeSourceTitle.text = showUserText
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
}
subscribeButton(false)
fragment.loadChapters(i)
}
//Subscription
subscribe = MediaDetailsActivity.PopImageButton(
fragment.lifecycleScope,
binding.animeSourceSubscribe,
R.drawable.ic_round_notifications_active_24,
R.drawable.ic_round_notifications_none_24,
R.color.bg_opp,
R.color.violet_400,
fragment.subscribed
) {
fragment.onNotificationPressed(it, binding.animeSource.text.toString())
}
subscribeButton(false)
binding.animeSourceSubscribe.setOnLongClickListener {
openSettings(fragment.requireContext(), getChannelId(true, media.id))
}
//Icons
binding.animeSourceGrid.visibility = View.GONE
var reversed = media.selected!!.recyclerReversed
var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.mangaDefaultView
binding.animeSourceTop.rotation = if (reversed) -90f else 90f
binding.animeSourceTop.setOnClickListener {
reversed = !reversed
binding.animeSourceTop.rotation = if (reversed) -90f else 90f
fragment.onIconPressed(style, reversed)
}
var selected = when (style) {
0 -> binding.animeSourceList
1 -> binding.animeSourceCompact
else -> binding.animeSourceList
}
selected.alpha = 1f
fun selected(it: ImageView) {
selected.alpha = 0.33f
selected = it
selected.alpha = 1f
}
binding.animeSourceList.setOnClickListener {
selected(it as ImageView)
style = 0
fragment.onIconPressed(style, reversed)
}
binding.animeSourceCompact.setOnClickListener {
selected(it as ImageView)
style = 1
fragment.onIconPressed(style, reversed)
}
//Chapter Handling
handleChapters()
}
fun subscribeButton(enabled: Boolean) {
subscribe?.enabled(enabled)
}
//Chips
@SuppressLint("SetTextI18n")
fun updateChips(limit: Int, names: Array<String>, arr: Array<Int>, selected: Int = 0) {
val binding = _binding
if (binding != null) {
val screenWidth = fragment.screenWidth.px
var select: Chip? = null
for (position in arr.indices) {
val last = if (position + 1 == arr.size) names.size else (limit * (position + 1))
val chip =
ItemChipBinding.inflate(LayoutInflater.from(fragment.context), binding.animeSourceChipGroup, false).root
chip.isCheckable = true
fun selected() {
chip.isChecked = true
binding.animeWatchChipScroll.smoothScrollTo((chip.left - screenWidth / 2) + (chip.width / 2), 0)
}
chip.text = "${names[limit * (position)]} - ${names[last - 1]}"
chip.setOnClickListener {
selected()
fragment.onChipClicked(position, limit * (position), last - 1)
}
binding.animeSourceChipGroup.addView(chip)
if (selected == position) {
selected()
select = chip
}
}
if (select != null)
binding.animeWatchChipScroll.apply { post { scrollTo((select.left - screenWidth / 2) + (select.width / 2), 0) } }
}
}
fun clearChips() {
_binding?.animeSourceChipGroup?.removeAllViews()
}
@SuppressLint("SetTextI18n")
fun handleChapters() {
val binding = _binding
if (binding != null) {
if (media.manga?.chapters != null) {
val chapters = media.manga.chapters!!.keys.toTypedArray()
val anilistEp = (media.userProgress ?: 0).plus(1)
val appEp = loadData<String>("${media.id}_current_chp")?.toIntOrNull() ?: 1
var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString()
if (chapters.contains(continueEp)) {
binding.animeSourceContinue.visibility = View.VISIBLE
handleProgress(
binding.itemEpisodeProgressCont,
binding.itemEpisodeProgress,
binding.itemEpisodeProgressEmpty,
media.id,
continueEp
)
if ((binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams).weight > 0.8f) {
val e = chapters.indexOf(continueEp)
if (e != -1 && e + 1 < chapters.size) {
continueEp = chapters[e + 1]
}
}
val ep = media.manga.chapters!![continueEp]!!
binding.itemEpisodeImage.loadImage(media.banner ?: media.cover)
binding.animeSourceContinueText.text =
currActivity()!!.getString(R.string.continue_chapter) + "${ep.number}${if (!ep.title.isNullOrEmpty()) "\n${ep.title}" else ""}"
binding.animeSourceContinue.setOnClickListener {
fragment.onMangaChapterClick(continueEp)
}
if (fragment.continueEp) {
if ((binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams).weight < 0.8f) {
binding.animeSourceContinue.performClick()
fragment.continueEp = false
}
}
} else {
binding.animeSourceContinue.visibility = View.GONE
}
binding.animeSourceProgressBar.visibility = View.GONE
if (media.manga.chapters!!.isNotEmpty())
binding.animeSourceNotFound.visibility = View.GONE
else
binding.animeSourceNotFound.visibility = View.VISIBLE
} else {
binding.animeSourceContinue.visibility = View.GONE
binding.animeSourceNotFound.visibility = View.GONE
clearChips()
binding.animeSourceProgressBar.visibility = View.VISIBLE
}
}
}
override fun getItemCount(): Int = 1
inner class ViewHolder(val binding: ItemAnimeWatchBinding) : RecyclerView.ViewHolder(binding.root)
}

View file

@ -0,0 +1,278 @@
package ani.dantotsu.media.manga
import android.annotation.SuppressLint
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.math.MathUtils.clamp
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager
import ani.dantotsu.*
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.media.manga.mangareader.ChapterLoaderDialog
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.parsers.HMangaSources
import ani.dantotsu.parsers.MangaParser
import ani.dantotsu.parsers.MangaSources
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.subcriptions.Notifications
import ani.dantotsu.subcriptions.Notifications.Group.MANGA_GROUP
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
import ani.dantotsu.subcriptions.SubscriptionHelper
import ani.dantotsu.subcriptions.SubscriptionHelper.Companion.saveSubscription
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.roundToInt
open class MangaReadFragment : Fragment() {
private var _binding: FragmentAnimeWatchBinding? = null
private val binding get() = _binding!!
private val model: MediaDetailsViewModel by activityViewModels()
private lateinit var media: Media
private var start = 0
private var end: Int? = null
private var style: Int? = null
private var reverse = false
private lateinit var headerAdapter: MangaReadAdapter
private lateinit var chapterAdapter: MangaChapterAdapter
var screenWidth = 0f
private var progress = View.VISIBLE
var continueEp: Boolean = false
var loaded = false
val uiSettings = loadData("ui_settings", toast = false) ?: UserInterfaceSettings().apply { saveData("ui_settings", this) }
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentAnimeWatchBinding.inflate(inflater, container, false)
return _binding?.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight)
screenWidth = resources.displayMetrics.widthPixels.dp
var maxGridSize = (screenWidth / 100f).roundToInt()
maxGridSize = max(4, maxGridSize - (maxGridSize % 2))
val gridLayoutManager = GridLayoutManager(requireContext(), maxGridSize)
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
val style = chapterAdapter.getItemViewType(position)
return when (position) {
0 -> maxGridSize
else -> when (style) {
0 -> maxGridSize
1 -> 1
else -> maxGridSize
}
}
}
}
binding.animeSourceRecycler.layoutManager = gridLayoutManager
model.scrolledToTop.observe(viewLifecycleOwner) {
if (it) binding.animeSourceRecycler.scrollToPosition(0)
}
continueEp = model.continueMedia ?: false
model.getMedia().observe(viewLifecycleOwner) {
if (it != null) {
media = it
progress = View.GONE
binding.mediaInfoProgressBar.visibility = progress
if (media.format == "MANGA" || media.format == "ONE SHOT") {
media.selected = model.loadSelected(media)
subscribed = SubscriptionHelper.getSubscriptions(requireContext()).containsKey(media.id)
style = media.selected!!.recyclerStyle
reverse = media.selected!!.recyclerReversed
if (!loaded) {
model.mangaReadSources = if (media.isAdult) HMangaSources else MangaSources
headerAdapter = MangaReadAdapter(it, this, model.mangaReadSources!!)
chapterAdapter = MangaChapterAdapter(style ?: uiSettings.mangaDefaultView, media, this)
binding.animeSourceRecycler.adapter = ConcatAdapter(headerAdapter, chapterAdapter)
lifecycleScope.launch(Dispatchers.IO) {
model.loadMangaChapters(media, media.selected!!.source)
}
loaded = true
} else {
reload()
}
} else {
binding.animeNotSupported.visibility = View.VISIBLE
binding.animeNotSupported.text = getString(R.string.not_supported, media.format ?: "")
}
}
}
model.getMangaChapters().observe(viewLifecycleOwner) { loadedChapters ->
if (loadedChapters != null) {
val chapters = loadedChapters[media.selected!!.source]
if (chapters != null) {
media.manga?.chapters = chapters
//CHIP GROUP
val total = chapters.size
val divisions = total.toDouble() / 10
start = 0
end = null
val limit = when {
(divisions < 25) -> 25
(divisions < 50) -> 50
else -> 100
}
headerAdapter.clearChips()
if (total > limit) {
val arr = chapters.keys.toTypedArray()
val stored = ceil((total).toDouble() / limit).toInt()
val position = clamp(media.selected!!.chip, 0, stored - 1)
val last = if (position + 1 == stored) total else (limit * (position + 1))
start = limit * (position)
end = last - 1
headerAdapter.updateChips(
limit,
arr,
(1..stored).toList().toTypedArray(),
position
)
}
headerAdapter.subscribeButton(true)
reload()
}
}
}
}
fun onSourceChange(i: Int): MangaParser {
media.manga?.chapters = null
reload()
val selected = model.loadSelected(media)
model.mangaReadSources?.get(selected.source)?.showUserTextListener = null
selected.source = i
selected.server = null
model.saveSelected(media.id, selected, requireActivity())
media.selected = selected
return model.mangaReadSources?.get(i)!!
}
fun loadChapters(i: Int) {
lifecycleScope.launch(Dispatchers.IO) { model.loadMangaChapters(media, i) }
}
fun onIconPressed(viewType: Int, rev: Boolean) {
style = viewType
reverse = rev
media.selected!!.recyclerStyle = style
media.selected!!.recyclerReversed = reverse
model.saveSelected(media.id, media.selected!!, requireActivity())
reload()
}
fun onChipClicked(i: Int, s: Int, e: Int) {
media.selected!!.chip = i
start = s
end = e
model.saveSelected(media.id, media.selected!!, requireActivity())
reload()
}
var subscribed = false
fun onNotificationPressed(subscribed: Boolean, source: String) {
this.subscribed = subscribed
saveSubscription(requireContext(), media, subscribed)
if (!subscribed)
Notifications.deleteChannel(requireContext(), getChannelId(true, media.id))
else
Notifications.createChannel(
requireContext(),
MANGA_GROUP,
getChannelId(true, media.id),
media.userPreferredName
)
snackString(
if (subscribed) getString(R.string.subscribed_notification, source)
else getString(R.string.unsubscribed_notification)
)
}
fun onMangaChapterClick(i: String) {
model.continueMedia = false
media.manga?.chapters?.get(i)?.let {
media.manga?.selectedChapter = i
model.saveSelected(media.id, media.selected!!, requireActivity())
ChapterLoaderDialog.newInstance(it, true).show(requireActivity().supportFragmentManager, "dialog")
}
}
@SuppressLint("NotifyDataSetChanged")
private fun reload() {
val selected = model.loadSelected(media)
//Find latest chapter for subscription
selected.latest = media.manga?.chapters?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f
selected.latest = media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: selected.latest
model.saveSelected(media.id, selected, requireActivity())
headerAdapter.handleChapters()
chapterAdapter.notifyItemRangeRemoved(0, chapterAdapter.arr.size)
var arr: ArrayList<MangaChapter> = arrayListOf()
if (media.manga!!.chapters != null) {
val end = if (end != null && end!! < media.manga!!.chapters!!.size) end else null
arr.addAll(
media.manga!!.chapters!!.values.toList().slice(start..(end ?: (media.manga!!.chapters!!.size - 1)))
)
if (reverse)
arr = (arr.reversed() as? ArrayList<MangaChapter>) ?: arr
}
chapterAdapter.arr = arr
chapterAdapter.updateType(style ?: uiSettings.mangaDefaultView)
chapterAdapter.notifyItemRangeInserted(0, arr.size)
}
override fun onDestroy() {
model.mangaReadSources?.flushText()
super.onDestroy()
}
private var state: Parcelable? = null
override fun onResume() {
super.onResume()
binding.mediaInfoProgressBar.visibility = progress
binding.animeSourceRecycler.layoutManager?.onRestoreInstanceState(state)
}
override fun onPause() {
super.onPause()
state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState()
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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