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,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()
}
}
}