diff --git a/README.md b/README.md
index 7a6a566ed..2f8242d68 100644
--- a/README.md
+++ b/README.md
@@ -94,8 +94,6 @@ Here is the libraries
- [libarchive](https://www.libarchive.org/)
- [jsoup](https://jsoup.org/)
-Specially thanks [Tachiyomi](https://tachiyomi.org/) for its reader implementation!
-
# License
Copyright 2014-2019 Hippo Seven
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index f10495314..a15e0e3ea 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -169,7 +169,6 @@ android {
buildFeatures {
buildConfig = true
compose = true
- viewBinding = true
}
namespace = "com.hippo.ehviewer"
@@ -223,15 +222,11 @@ dependencies {
// https://developer.android.com/jetpack/androidx/releases/paging
implementation(libs.androidx.paging.compose)
- implementation(libs.androidx.recyclerview)
- implementation(libs.androidx.viewpager2)
-
// https://developer.android.com/jetpack/androidx/releases/room
ksp(libs.androidx.room.compiler)
implementation(libs.androidx.room.paging)
implementation(libs.androidx.work.runtime)
- implementation(libs.photoview)
implementation(libs.material.motion.core)
implementation(libs.bundles.splitties)
@@ -246,8 +241,6 @@ dependencies {
implementation(libs.aboutlibraries.compose.m3)
implementation(libs.accompanist.drawable.painter)
- implementation(libs.insetter) // Dead Dependency
-
// implementation(libs.reorderable)
implementation(platform(libs.arrow.stack))
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e9f00a844..0ee8582a7 100755
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -118,11 +118,6 @@
-
-
- * s prefixes - coordinates, translations and distances measured in rotated and cropped source image pixels (scaled)
- *
- * f prefixes - coordinates, translations and distances measured in original unrotated, uncropped source file pixels
- *
- *
- * [View project on GitHub](https://github.com/davemorrissey/subsampling-scale-image-view)
- *
- */
-@Suppress("unused")
-open class SubsamplingScaleImageView @JvmOverloads constructor(
- context: Context,
- attr: AttributeSet? = null,
-) : View(context, attr) {
- // Coroutines scope
- private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
-
- // Current quickscale state
- private val quickScaleThreshold =
- TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20F, context.resources.displayMetrics)
-
- // Long click handler
- private var longClickJob: Job? = null
-
- // The logical density of the display
- private val density: Float = resources.displayMetrics.density
-
- // Bitmap (preview or full image)
- private var bitmap: Bitmap? = null
-
- // Specifies if a cache handler is also referencing the bitmap. Do not recycle if so.
- private var bitmapIsCached = false
-
- /**
- * Maximum scale allowed. A value of 1 means 1:1 pixels at maximum scale. You may wish to set this according
- * to screen density - on a retina screen, 1:1 may still be too small. Consider using [setMinimumDpi],
- * which is density aware.
- */
- var maxScale = 2F
-
- // Pan limiting style
- @PanLimit
- private var panLimit = PAN_LIMIT_INSIDE
-
- // Minimum scale type
- @ScaleType
- private var minimumScaleType = SCALE_TYPE_CENTER_INSIDE
-
- // Gesture detection settings
- private var panEnabled = true
-
- /**
- * Enable or disable zoom gesture detection. Disabling zoom locks the the current scale.
- */
- var isZoomEnabled = true
-
- /**
- * Enable or disable double tap & swipe to zoom.
- */
- var isQuickScaleEnabled = true
-
- // Double tap zoom behaviour
- private var doubleTapZoomScale = 1F
-
- @ZoomStyle
- private var doubleTapZoomStyle = ZOOM_FOCUS_FIXED
- private var doubleTapZoomDuration = 500
-
- /**
- * Returns the current scale value.
- *
- * @return the current scale as a source/view pixels ratio.
- */
- var scale = 0F
- private set
- private var scaleStart = 0F
-
- // Screen coordinate of top-left corner of source image
- private var vTranslate: PointF? = null
- private var vTranslateStart: PointF? = null
- private var vTranslateBefore: PointF? = null
-
- // Source coordinate to center on, used when new position is set externally before view is ready
- private var pendingScale = -1F
- private var sPendingCenter: PointF? = null
- private var sRequestedCenter: PointF? = null
-
- /**
- * Get source width.
- *
- * @return the source image width in pixels.
- */
- var sWidth = 0
- private set
-
- /**
- * Get source height.
- *
- * @return the source image height in pixels.
- */
- var sHeight = 0
- private set
-
- // Min scale allowed (prevent infinite zoom)
- private var minScale = minScale()
- private var srcRect = Rect(0, 0, 0, 0)
-
- // Is two-finger zooming in progress
- private var isZooming = false
-
- // Is one-finger panning in progress
- private var isPanning = false
-
- // Is quick-scale gesture in progress
- private var isQuickScaling = false
-
- // Max touches used in current gesture
- private var maxTouchCount = 0
-
- // Fling detector
- private var detector: GestureDetector? = null
- private var singleDetector: GestureDetector? = null
-
- // Debug values
- private var vCenterStart: PointF? = null
- private var vDistStart = 0F
- private var quickScaleLastDistance = 0F
- private var quickScaleMoved = false
- private var quickScaleVLastPoint: PointF? = null
- private var quickScaleSCenter: PointF? = null
- private var quickScaleVStart: PointF? = null
-
- // Scale and center animation tracking
- private var anim: Anim? = null
-
- /**
- * Call to find whether the view is initialised, has dimensions, and will display an image on
- * the next draw. If a preview has been provided, it may be the preview that will be displayed
- * and the full size image may still be loading. If no preview was provided, this is called once
- * the base layer tiles of the full size image are loaded.
- *
- * @return true if the view is ready to display an image and accept touch gestures.
- */
- var isReady = false
- private set
-
- // Event listener
- private var onImageEventListener: OnImageEventListener? = null
-
- // Scale and center listener
- private var onStateChangedListener: OnStateChangedListener? = null
-
- // Long click listener
- private var onLongClickListener: OnLongClickListener? = null
-
- // Paint objects created once and reused for efficiency
- private val bitmapPaint by lazy {
- Paint().apply {
- isAntiAlias = true
- isFilterBitmap = true
- isDither = true
- }
- }
-
- // Volatile fields used to reduce object creation
- private var satTemp: ScaleAndTranslate? = null
- private val mtrx = Matrix()
-
- init {
- setGestureDetector(context)
- }
-
- /**
- * Set the image source from a bitmap, resource, asset, file or other URI.
- *
- * @param bitmap Image source.
- */
- fun setImage(bitmap: Bitmap, rect: Rect) {
- reset(true)
- srcRect = Rect(rect)
- onImageLoaded(bitmap)
- }
-
- /**
- * Reset all state before setting/changing image or setting new rotation.
- */
- private fun reset(newImage: Boolean) {
- scale = 0F
- scaleStart = 0F
- vTranslate = null
- vTranslateStart = null
- vTranslateBefore = null
- pendingScale = -1F
- sPendingCenter = null
- sRequestedCenter = null
- isZooming = false
- isPanning = false
- isQuickScaling = false
- maxTouchCount = 0
- vCenterStart = null
- vDistStart = 0F
- quickScaleLastDistance = 0F
- quickScaleMoved = false
- quickScaleSCenter = null
- quickScaleVLastPoint = null
- quickScaleVStart = null
- anim = null
- satTemp = null
- mtrx.reset()
- if (newImage) {
- if (!bitmapIsCached) {
- bitmap?.recycle()
- }
- sWidth = 0
- sHeight = 0
- srcRect.setEmpty()
- isReady = false
- bitmap = null
- bitmapIsCached = false
- }
- setGestureDetector(context)
- }
-
- private fun setGestureDetector(context: Context) {
- detector = GestureDetector(
- context,
- object : GestureDetector.SimpleOnGestureListener() {
- override fun onFling(e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
- if (panEnabled && isReady && vTranslate != null && e1 != null && (abs(e1.x - e2.x) > 50 || abs(e1.y - e2.y) > 50) && (abs(velocityX) > 500 || abs(velocityY) > 500) && !isZooming) {
- val vTranslateEnd = PointF(vTranslate!!.x + velocityX * 0.25F, vTranslate!!.y + velocityY * 0.25F)
- val sCenterXEnd = (width / 2 - vTranslateEnd.x) / scale
- val sCenterYEnd = (height / 2 - vTranslateEnd.y) / scale
- AnimationBuilder(PointF(sCenterXEnd, sCenterYEnd))
- .withEasing(EASE_OUT_QUAD)
- .withPanLimited(false)
- .withOrigin(ORIGIN_FLING)
- .start()
- return true
- }
- return super.onFling(e1, e2, velocityX, velocityY)
- }
-
- override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
- performClick()
- return true
- }
-
- override fun onDoubleTap(e: MotionEvent): Boolean {
- if (!ReaderPreferences.doubleTapToZoom().get()) return false
- if (isZoomEnabled && isReady && vTranslate != null) {
- // Hacky solution for #15 - after a double tap the GestureDetector gets in a state
- // where the next fling is ignored, so here we replace it with a new one.
- setGestureDetector(context)
- return if (isQuickScaleEnabled) {
- // Store quick scale params. This will become either a double tap zoom or a
- // quick scale depending on whether the user swipes.
- vCenterStart = PointF(e.x, e.y)
- vTranslateStart = PointF(vTranslate!!.x, vTranslate!!.y)
- scaleStart = scale
- isQuickScaling = true
- isZooming = true
- quickScaleLastDistance = -1F
- quickScaleSCenter = viewToSourceCoord(vCenterStart!!)
- quickScaleVStart = PointF(e.x, e.y)
- quickScaleVLastPoint = PointF(quickScaleSCenter!!.x, quickScaleSCenter!!.y)
- quickScaleMoved = false
- // We need to get events in onTouchEvent after this.
- false
- } else {
- // Start double tap zoom animation.
- viewToSourceCoord(PointF(e.x, e.y))?.let { center ->
- doubleTapZoom(center, PointF(e.x, e.y))
- }
- true
- }
- }
- return super.onDoubleTapEvent(e)
- }
- },
- )
- singleDetector = GestureDetector(
- context,
- object : GestureDetector.SimpleOnGestureListener() {
- override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
- performClick()
- return true
- }
- },
- )
- }
-
- override fun onDetachedFromWindow() {
- super.onDetachedFromWindow()
- coroutineScope.cancel()
- }
-
- /**
- * On resize, preserve center and scale. Various behaviours are possible, override this method to use another.
- */
- override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
- if (isReady && center != null) {
- anim = null
- pendingScale = scale
- sPendingCenter = center
- }
- }
-
- /**
- * Measures the width and height of the view, preserving the aspect ratio of the image displayed if wrap_content is
- * used. The image will scale within this box, not resizing the view as it is zoomed.
- */
- override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
- val widthSpecMode = MeasureSpec.getMode(widthMeasureSpec)
- val heightSpecMode = MeasureSpec.getMode(heightMeasureSpec)
- val parentWidth = MeasureSpec.getSize(widthMeasureSpec)
- val parentHeight = MeasureSpec.getSize(heightMeasureSpec)
- val resizeWidth = widthSpecMode != MeasureSpec.EXACTLY
- val resizeHeight = heightSpecMode != MeasureSpec.EXACTLY
- var width = parentWidth
- var height = parentHeight
- if (sWidth > 0 && sHeight > 0) {
- if (resizeWidth && resizeHeight) {
- width = sWidth
- height = sHeight
- } else if (resizeHeight) {
- height = (sHeight.toDouble() / sWidth.toDouble() * width).toInt()
- } else if (resizeWidth) {
- width = (sWidth.toDouble() / sHeight.toDouble() * height).toInt()
- }
- }
- width = width.coerceAtLeast(suggestedMinimumWidth)
- height = height.coerceAtLeast(suggestedMinimumHeight)
- setMeasuredDimension(width, height)
- }
-
- /**
- * Handle touch events. One finger pans, and two finger pinch and zoom plus panning.
- */
- @SuppressLint("ClickableViewAccessibility")
- override fun onTouchEvent(event: MotionEvent): Boolean {
- // During non-interruptible anims, ignore all touch events
- if (anim?.interruptible == false) {
- requestDisallowInterceptTouchEvent(true)
- return true
- }
-
- anim = null
-
- // Abort if not ready
- if (vTranslate == null) {
- singleDetector?.onTouchEvent(event)
- return true
- }
- // Detect flings, taps and double taps
- // May throw NPE
- runCatching {
- if (!isQuickScaling && detector?.onTouchEvent(event) != false) {
- isZooming = false
- isPanning = false
- maxTouchCount = 0
- return true
- }
- }
-
- if (vTranslateStart == null) {
- vTranslateStart = PointF(0F, 0F)
- }
- if (vTranslateBefore == null) {
- vTranslateBefore = PointF(0F, 0F)
- }
- if (vCenterStart == null) {
- vCenterStart = PointF(0F, 0F)
- }
-
- // Store current values so we can send an event if they change
- vTranslateBefore!!.set(vTranslate!!)
- val handled = onTouchEventInternal(event)
- sendStateChanged(scale, vTranslateBefore!!, ORIGIN_TOUCH)
- return handled || super.onTouchEvent(event)
- }
-
- private fun onTouchEventInternal(event: MotionEvent): Boolean {
- val touchCount = event.pointerCount
- when (event.actionMasked) {
- MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> {
- anim = null
- requestDisallowInterceptTouchEvent(true)
- maxTouchCount = maxTouchCount.coerceAtLeast(touchCount)
- if (touchCount >= 2) {
- if (isZoomEnabled) {
- // Start pinch to zoom. Calculate distance between touch points and center point of the pinch.
- scaleStart = scale
- vDistStart = distance(event.getX(0), event.getX(1), event.getY(0), event.getY(1))
- vTranslateStart!![vTranslate!!.x] = vTranslate!!.y
- vCenterStart!![(event.getX(0) + event.getX(1)) / 2] = (event.getY(0) + event.getY(1)) / 2
- } else {
- // Abort all gestures on second touch
- maxTouchCount = 0
- }
- // Cancel long click timer
- longClickJob?.cancel()
- } else if (!isQuickScaling) {
- // Start one-finger pan
- vTranslateStart!![vTranslate!!.x] = vTranslate!!.y
- vCenterStart!![event.x] = event.y
-
- // Start long click timer
- longClickJob = coroutineScope.launch {
- if (onLongClickListener != null) {
- delay(600)
- super@SubsamplingScaleImageView.setOnLongClickListener(onLongClickListener)
- performLongClick()
- super@SubsamplingScaleImageView.setOnLongClickListener(null)
- }
- }
- }
- return true
- }
- MotionEvent.ACTION_MOVE -> {
- var consumed = false
- if (maxTouchCount > 0) {
- if (touchCount >= 2) {
- // Calculate new distance between touch points, to scale and pan relative to start values.
- val vDistEnd = distance(event.getX(0), event.getX(1), event.getY(0), event.getY(1))
- val vCenterEndX = (event.getX(0) + event.getX(1)) / 2
- val vCenterEndY = (event.getY(0) + event.getY(1)) / 2
- if (isZoomEnabled && (distance(vCenterStart!!.x, vCenterEndX, vCenterStart!!.y, vCenterEndY) > 5 || abs(vDistEnd - vDistStart) > 5 || isPanning)) {
- isZooming = true
- isPanning = true
- consumed = true
- val previousScale = scale.toDouble()
- scale = (vDistEnd / vDistStart * scaleStart).coerceAtMost(maxScale)
- if (scale <= minScale()) {
- // Minimum scale reached so don't pan. Adjust start settings so any expand will zoom in.
- vDistStart = vDistEnd
- scaleStart = minScale()
- vCenterStart!![vCenterEndX] = vCenterEndY
- vTranslateStart!!.set(vTranslate!!)
- } else if (panEnabled) {
- // Translate to place the source image coordinate that was at the center of the pinch at the start
- // at the center of the pinch now, to give simultaneous pan + zoom.
- val vLeftStart = vCenterStart!!.x - vTranslateStart!!.x
- val vTopStart = vCenterStart!!.y - vTranslateStart!!.y
- val vLeftNow = vLeftStart * (scale / scaleStart)
- val vTopNow = vTopStart * (scale / scaleStart)
- vTranslate!!.x = vCenterEndX - vLeftNow
- vTranslate!!.y = vCenterEndY - vTopNow
- if (previousScale * sHeight < height && scale * sHeight >= height || previousScale * sWidth < width && scale * sWidth >= width) {
- fitToBounds(true)
- vCenterStart!![vCenterEndX] = vCenterEndY
- vTranslateStart!!.set(vTranslate!!)
- scaleStart = scale
- vDistStart = vDistEnd
- }
- } else if (sRequestedCenter != null) {
- // With a center specified from code, zoom around that point.
- vTranslate!!.x = width / 2 - scale * sRequestedCenter!!.x
- vTranslate!!.y = height / 2 - scale * sRequestedCenter!!.y
- } else {
- // With no requested center, scale around the image center.
- vTranslate!!.x = width / 2 - scale * (sWidth / 2)
- vTranslate!!.y = height / 2 - scale * (sHeight / 2)
- }
- fitToBounds(true)
- }
- } else if (isQuickScaling) {
- // One finger zoom
- // Stole Google's Magical Formula™ to make sure it feels the exact same
- var dist = abs(quickScaleVStart!!.y - event.y) * 2 + quickScaleThreshold
- if (quickScaleLastDistance == -1F) {
- quickScaleLastDistance = dist
- }
- val isUpwards = event.y > quickScaleVLastPoint!!.y
- quickScaleVLastPoint!![0F] = event.y
- val spanDiff = abs(1 - dist / quickScaleLastDistance) * 0.5F
- if (spanDiff > 0.03F || quickScaleMoved) {
- quickScaleMoved = true
- var multiplier = 1F
- if (quickScaleLastDistance > 0) {
- multiplier = if (isUpwards) 1 + spanDiff else 1 - spanDiff
- }
- val previousScale = scale.toDouble()
- scale = (scale * multiplier).coerceIn(minScale(), maxScale)
- if (panEnabled) {
- val vLeftStart = vCenterStart!!.x - vTranslateStart!!.x
- val vTopStart = vCenterStart!!.y - vTranslateStart!!.y
- val vLeftNow = vLeftStart * (scale / scaleStart)
- val vTopNow = vTopStart * (scale / scaleStart)
- vTranslate!!.x = vCenterStart!!.x - vLeftNow
- vTranslate!!.y = vCenterStart!!.y - vTopNow
- if (previousScale * sHeight < height && scale * sHeight >= height || previousScale * sWidth < width && scale * sWidth >= width) {
- fitToBounds(true)
- vCenterStart!!.set(sourceToViewCoord(quickScaleSCenter)!!)
- vTranslateStart!!.set(vTranslate!!)
- scaleStart = scale
- dist = 0F
- }
- } else if (sRequestedCenter != null) {
- // With a center specified from code, zoom around that point.
- vTranslate!!.x = width / 2 - scale * sRequestedCenter!!.x
- vTranslate!!.y = height / 2 - scale * sRequestedCenter!!.y
- } else {
- // With no requested center, scale around the image center.
- vTranslate!!.x = width / 2 - scale * (sWidth / 2)
- vTranslate!!.y = height / 2 - scale * (sHeight / 2)
- }
- }
- quickScaleLastDistance = dist
- fitToBounds(true)
- consumed = true
- } else if (!isZooming) {
- // One finger pan - translate the image. We do this calculation even with pan disabled so click
- // and long click behaviour is preserved.
- val dx = abs(event.x - vCenterStart!!.x)
- val dy = abs(event.y - vCenterStart!!.y)
-
- // On the Samsung S6 long click event does not work, because the dx > 5 usually true
- val offset = density * 5
- if (dx > offset || dy > offset || isPanning) {
- consumed = true
- vTranslate!!.x = vTranslateStart!!.x + (event.x - vCenterStart!!.x)
- vTranslate!!.y = vTranslateStart!!.y + (event.y - vCenterStart!!.y)
- val lastX = vTranslate!!.x
- val lastY = vTranslate!!.y
- fitToBounds(true)
- val atXEdge = lastX != vTranslate!!.x
- val atYEdge = lastY != vTranslate!!.y
- val edgeXSwipe = atXEdge && dx > dy && !isPanning
- val edgeYSwipe = atYEdge && dy > dx && !isPanning
- val yPan = lastY == vTranslate!!.y && dy > offset * 3
- if (!edgeXSwipe && !edgeYSwipe && (!atXEdge || !atYEdge || yPan || isPanning)) {
- isPanning = true
- } else if (dx > offset || dy > offset) {
- // Haven't panned the image, and we're at the left or right edge. Switch to page swipe.
- maxTouchCount = 0
- longClickJob?.cancel()
- requestDisallowInterceptTouchEvent(false)
- }
- if (!panEnabled) {
- vTranslate!!.x = vTranslateStart!!.x
- vTranslate!!.y = vTranslateStart!!.y
- requestDisallowInterceptTouchEvent(false)
- }
- }
- }
- }
- if (consumed) {
- longClickJob?.cancel()
- invalidate()
- return true
- }
- }
- MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> {
- longClickJob?.cancel()
- if (isQuickScaling) {
- isQuickScaling = false
- if (!quickScaleMoved && quickScaleSCenter != null && vCenterStart != null) {
- doubleTapZoom(quickScaleSCenter!!, vCenterStart!!)
- }
- }
- if (maxTouchCount > 0 && (isZooming || isPanning)) {
- if (isZooming && touchCount == 2) {
- // Convert from zoom to pan with remaining touch
- isPanning = true
- vTranslateStart!![vTranslate!!.x] = vTranslate!!.y
- if (event.actionIndex == 1) {
- vCenterStart!![event.getX(0)] = event.getY(0)
- } else {
- vCenterStart!![event.getX(1)] = event.getY(1)
- }
- }
- if (touchCount < 3) {
- // End zooming when only one touch point
- isZooming = false
- }
- if (touchCount < 2) {
- // End panning when no touch points
- isPanning = false
- maxTouchCount = 0
- }
- // Trigger load of tiles now required
- return true
- }
- if (touchCount == 1) {
- isZooming = false
- isPanning = false
- maxTouchCount = 0
- }
- return true
- }
- }
- return false
- }
-
- private fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
- parent?.requestDisallowInterceptTouchEvent(disallowIntercept)
- }
-
- /**
- * Double tap zoom handler triggered from gesture detector or on touch, depending on whether
- * quick scale is enabled.
- */
- private fun doubleTapZoom(sCenter: PointF, vFocus: PointF) {
- if (!panEnabled) {
- if (sRequestedCenter != null) {
- // With a center specified from code, zoom around that point.
- sCenter.x = sRequestedCenter!!.x
- sCenter.y = sRequestedCenter!!.y
- } else {
- // With no requested center, scale around the image center.
- sCenter.x = (sWidth / 2).toFloat()
- sCenter.y = (sHeight / 2).toFloat()
- }
- }
- val doubleTapZoomScale = doubleTapZoomScale.coerceAtMost(maxScale)
- val zoomIn = scale <= doubleTapZoomScale * 0.9 || scale == minScale
- val targetScale = if (zoomIn) doubleTapZoomScale else minScale()
- if (doubleTapZoomStyle == ZOOM_FOCUS_CENTER_IMMEDIATE) {
- setScaleAndCenter(targetScale, sCenter)
- } else if (doubleTapZoomStyle == ZOOM_FOCUS_CENTER || !zoomIn || !panEnabled) {
- AnimationBuilder(targetScale, sCenter)
- .withInterruptible(false)
- .withDuration(doubleTapZoomDuration.toLong())
- .withOrigin(ORIGIN_DOUBLE_TAP_ZOOM)
- .start()
- } else if (doubleTapZoomStyle == ZOOM_FOCUS_FIXED) {
- AnimationBuilder(targetScale, sCenter, vFocus)
- .withInterruptible(false)
- .withDuration(doubleTapZoomDuration.toLong())
- .withOrigin(ORIGIN_DOUBLE_TAP_ZOOM)
- .start()
- }
- invalidate()
- }
-
- /**
- * Draw method should not be called until the view has dimensions so the first calls are used as triggers to calculate
- * the scaling and tiling required. Once the view is setup, tiles are displayed as they are loaded.
- */
- override fun onDraw(canvas: Canvas) {
- super.onDraw(canvas)
-
- // If image or view dimensions are not known yet, abort.
- if (sWidth == 0 || sHeight == 0 || width == 0 || height == 0) {
- return
- }
-
- // If image has been loaded or supplied as a bitmap, onDraw may be the first time the view has
- // dimensions and therefore the first opportunity to set scale and translate. If this call returns
- // false there is nothing to be drawn so return immediately.
- if (!checkReady()) {
- return
- }
-
- // Set scale and translate before draw.
- preDraw()
-
- // If animating scale, calculate current scale and center with easing equations
- if (anim != null && anim!!.vFocusStart != null) {
- // Store current values so we can send an event if they change
- val scaleBefore = scale
- if (vTranslateBefore == null) {
- vTranslateBefore = PointF(0F, 0F)
- }
- vTranslateBefore!!.set(vTranslate!!)
- var scaleElapsed = System.currentTimeMillis() - anim!!.time
- val finished = scaleElapsed > anim!!.duration
- scaleElapsed = scaleElapsed.coerceAtMost(anim!!.duration)
- scale = ease(
- anim!!.easing,
- scaleElapsed,
- anim!!.scaleStart,
- anim!!.scaleEnd - anim!!.scaleStart,
- anim!!.duration,
- )
-
- // Apply required animation to the focal point
- val vFocusNowX = ease(
- anim!!.easing,
- scaleElapsed,
- anim!!.vFocusStart!!.x,
- anim!!.vFocusEnd.x - anim!!.vFocusStart!!.x,
- anim!!.duration,
- )
- val vFocusNowY = ease(
- anim!!.easing,
- scaleElapsed,
- anim!!.vFocusStart!!.y,
- anim!!.vFocusEnd.y - anim!!.vFocusStart!!.y,
- anim!!.duration,
- )
- // Find out where the focal point is at this scale and adjust its position to follow the animation path
- vTranslate!!.x -= sourceToViewX(anim!!.sCenterEnd.x) - vFocusNowX
- vTranslate!!.y -= sourceToViewY(anim!!.sCenterEnd.y) - vFocusNowY
-
- // For translate anims, showing the image non-centered is never allowed, for scaling anims it is during the animation.
- fitToBounds(finished || anim!!.scaleStart == anim!!.scaleEnd)
- sendStateChanged(scaleBefore, vTranslateBefore!!, anim!!.origin)
- if (finished) {
- anim = null
- }
- invalidate()
- }
- if (bitmap != null && !bitmap!!.isRecycled) {
- val xScale = scale
- val yScale = scale
- mtrx.reset()
- mtrx.postTranslate((-srcRect.left).toFloat(), (-srcRect.top).toFloat())
- mtrx.postScale(xScale, yScale)
- mtrx.postTranslate(vTranslate!!.x, vTranslate!!.y)
- canvas.drawBitmap(bitmap!!, mtrx, bitmapPaint)
- }
- }
-
- /**
- * Check whether view and image dimensions are known and either a preview, full size image or
- * base layer tiles are loaded. First time, send ready event to listener. The next draw will
- * display an image.
- */
- private fun checkReady(): Boolean {
- val ready = width > 0 && height > 0 && sWidth > 0 && sHeight > 0 && bitmap != null
- if (!isReady && ready) {
- preDraw()
- isReady = true
- onImageEventListener?.onReady()
- }
- return ready
- }
-
- /**
- * Sets scale and translate ready for the next draw.
- */
- private fun preDraw() {
- if (width == 0 || height == 0 || sWidth <= 0 || sHeight <= 0) {
- return
- }
-
- // If waiting to translate to new center position, set translate now
- if (sPendingCenter != null && pendingScale >= 0) {
- scale = pendingScale
- if (vTranslate == null) {
- vTranslate = PointF()
- }
- vTranslate!!.x = width / 2 - scale * sPendingCenter!!.x
- vTranslate!!.y = height / 2 - scale * sPendingCenter!!.y
- sPendingCenter = null
- pendingScale = -1F
- fitToBounds(true)
- }
-
- // On first display of base image set up position, and in other cases make sure scale is correct.
- fitToBounds(false)
- }
-
- /**
- * Adjusts hypothetical future scale and translate values to keep scale within the allowed range and the image on screen. Minimum scale
- * is set so one dimension fills the view and the image is centered on the other dimension. Used to calculate what the target of an
- * animation should be.
- *
- * @param maybeCenter Whether the image should be centered in the dimension it's too small to fill. While animating this can be false to avoid changes in direction as bounds are reached.
- * @param sat The scale we want and the translation we're aiming for. The values are adjusted to be valid.
- */
- private fun fitToBounds(maybeCenter: Boolean, sat: ScaleAndTranslate) {
- val center = if (panLimit == PAN_LIMIT_OUTSIDE && isReady) {
- false
- } else {
- maybeCenter
- }
- val vTranslate = sat.vTranslate
- val scale = limitedScale(sat.scale)
- val scaleWidth = scale * sWidth
- val scaleHeight = scale * sHeight
- if (panLimit == PAN_LIMIT_CENTER && isReady) {
- vTranslate.x = vTranslate.x.coerceAtLeast(width / 2 - scaleWidth)
- vTranslate.y = vTranslate.y.coerceAtLeast(height / 2 - scaleHeight)
- } else if (center) {
- vTranslate.x = vTranslate.x.coerceAtLeast(width - scaleWidth)
- vTranslate.y = vTranslate.y.coerceAtLeast(height - scaleHeight)
- } else {
- vTranslate.x = vTranslate.x.coerceAtLeast(-scaleWidth)
- vTranslate.y = vTranslate.y.coerceAtLeast(-scaleHeight)
- }
-
- // Asymmetric padding adjustments
- val xPaddingRatio =
- if (paddingLeft > 0 || paddingRight > 0) paddingLeft / (paddingLeft + paddingRight).toFloat() else 0.5F
- val yPaddingRatio =
- if (paddingTop > 0 || paddingBottom > 0) paddingTop / (paddingTop + paddingBottom).toFloat() else 0.5F
-
- val maxTx: Float
- val maxTy: Float
- if (panLimit == PAN_LIMIT_CENTER && isReady) {
- maxTx = (width / 2F).coerceAtLeast(0F)
- maxTy = (height / 2F).coerceAtLeast(0F)
- } else if (center) {
- maxTx = ((width - scaleWidth) * xPaddingRatio).coerceAtLeast(0F)
- maxTy = ((height - scaleHeight) * yPaddingRatio).coerceAtLeast(0F)
- } else {
- maxTx = width.toFloat().coerceAtLeast(0F)
- maxTy = height.toFloat().coerceAtLeast(0F)
- }
-
- vTranslate.x = vTranslate.x.coerceAtMost(maxTx)
- vTranslate.y = vTranslate.y.coerceAtMost(maxTy)
- sat.scale = scale
- }
-
- /**
- * Adjusts current scale and translate values to keep scale within the allowed range and the image on screen. Minimum scale
- * is set so one dimension fills the view and the image is centered on the other dimension.
- *
- * @param center Whether the image should be centered in the dimension it's too small to fill. While animating this can be false to avoid changes in direction as bounds are reached.
- */
- private fun fitToBounds(center: Boolean) {
- var init = false
- if (vTranslate == null) {
- init = true
- vTranslate = PointF(0F, 0F)
- }
- if (satTemp == null) {
- satTemp = ScaleAndTranslate(0F, PointF(0F, 0F))
- }
- satTemp!!.scale = scale
- satTemp!!.vTranslate.set(vTranslate!!)
- fitToBounds(center, satTemp!!)
- scale = satTemp!!.scale
- vTranslate!!.set(satTemp!!.vTranslate)
- if (init) {
- vTranslate!!.set(vTranslateForSCenter((sWidth / 2).toFloat(), (sHeight / 2).toFloat(), scale))
- }
- }
-
- /**
- * Called by worker task when full size image bitmap is ready (tiling is disabled).
- */
- @Synchronized
- private fun onImageLoaded(bitmap: Bitmap) {
- // If actual dimensions don't match the declared size, reset everything.
- if (sWidth > 0 && sHeight > 0 && (sWidth != srcRect.width() || sHeight != srcRect.height())) {
- reset(false)
- }
- if (!this.bitmapIsCached) {
- this.bitmap?.recycle()
- }
- this.bitmapIsCached = true
- this.bitmap = bitmap
- sWidth = srcRect.width()
- sHeight = srcRect.height()
- checkReady()
- invalidate()
- requestLayout()
- }
-
- /**
- * Pythagoras distance between two points.
- */
- private fun distance(x0: Float, x1: Float, y0: Float, y1: Float): Float {
- val x = x0 - x1
- val y = y0 - y1
- return sqrt((x * x + y * y).toDouble()).toFloat()
- }
-
- /**
- * Releases all resources the view is using and resets the state, nulling any fields that use significant memory.
- * After you have called this method, the view can be re-used by setting a new image. Settings are remembered
- * but state (scale and center) is forgotten. You can restore these yourself if required.
- */
- fun recycle() {
- reset(true)
- }
-
- /**
- * Convert screen to source x coordinate.
- */
- private fun viewToSourceX(vx: Float): Float = if (vTranslate == null) Float.NaN else (vx - vTranslate!!.x) / scale
-
- /**
- * Convert screen to source y coordinate.
- */
- private fun viewToSourceY(vy: Float): Float = if (vTranslate == null) Float.NaN else (vy - vTranslate!!.y) / scale
-
- /**
- * Convert screen coordinate to source coordinate.
- *
- * @param vxy view X/Y coordinate.
- * @return a coordinate representing the corresponding source coordinate.
- */
- fun viewToSourceCoord(vxy: PointF): PointF? = viewToSourceCoord(vxy.x, vxy.y, PointF())
-
- /**
- * Convert screen coordinate to source coordinate.
- *
- * @param vx view X coordinate.
- * @param vy view Y coordinate.
- * @param sTarget target object for result. The same instance is also returned.
- * @return a coordinate representing the corresponding source coordinate. This is the same instance passed to the sTarget param.
- */
- private fun viewToSourceCoord(vx: Float, vy: Float, sTarget: PointF = PointF()): PointF? {
- if (vTranslate == null) {
- return null
- }
- sTarget[viewToSourceX(vx)] = viewToSourceY(vy)
- return sTarget
- }
-
- /**
- * Convert source to view x coordinate.
- */
- private fun sourceToViewX(sx: Float): Float = if (vTranslate == null) Float.NaN else sx * scale + vTranslate!!.x
-
- /**
- * Convert source to view y coordinate.
- */
- private fun sourceToViewY(sy: Float): Float = if (vTranslate == null) Float.NaN else sy * scale + vTranslate!!.y
-
- /**
- * Convert source coordinate to view coordinate.
- *
- * @param sxy source coordinates to convert.
- * @return view coordinates.
- */
- fun sourceToViewCoord(sxy: PointF?): PointF? = sourceToViewCoord(sxy!!.x, sxy.y, PointF())
-
- /**
- * Convert source coordinate to view coordinate.
- *
- * @param sx source X coordinate.
- * @param sy source Y coordinate.
- * @param vTarget target object for result. The same instance is also returned.
- * @return view coordinates. This is the same instance passed to the vTarget param.
- */
- private fun sourceToViewCoord(sx: Float, sy: Float, vTarget: PointF = PointF()): PointF? {
- if (vTranslate == null) {
- return null
- }
- vTarget[sourceToViewX(sx)] = sourceToViewY(sy)
- return vTarget
- }
-
- /**
- * Get the translation required to place a given source coordinate at the center of the screen, with the center
- * adjusted for asymmetric padding. Accepts the desired scale as an argument, so this is independent of current
- * translate and scale. The result is fitted to bounds, putting the image point as near to the screen center as permitted.
- */
- private fun vTranslateForSCenter(sCenterX: Float, sCenterY: Float, scale: Float): PointF {
- val vxCenter = paddingLeft + (width - paddingRight - paddingLeft) / 2
- val vyCenter = paddingTop + (height - paddingBottom - paddingTop) / 2
- if (satTemp == null) {
- satTemp = ScaleAndTranslate(0F, PointF(0F, 0F))
- }
- satTemp!!.scale = scale
- satTemp!!.vTranslate[vxCenter - sCenterX * scale] = vyCenter - sCenterY * scale
- fitToBounds(true, satTemp!!)
- return satTemp!!.vTranslate
- }
-
- /**
- * Given a requested source center and scale, calculate what the actual center will have to be to keep the image in
- * pan limits, keeping the requested center as near to the middle of the screen as allowed.
- */
- private fun limitedSCenter(sCenterX: Float, sCenterY: Float, scale: Float, sTarget: PointF): PointF {
- val vTranslate = vTranslateForSCenter(sCenterX, sCenterY, scale)
- val vxCenter = paddingLeft + (width - paddingRight - paddingLeft) / 2
- val vyCenter = paddingTop + (height - paddingBottom - paddingTop) / 2
- val sx = (vxCenter - vTranslate.x) / scale
- val sy = (vyCenter - vTranslate.y) / scale
- sTarget[sx] = sy
- return sTarget
- }
-
- /**
- * Returns the minimum allowed scale.
- */
- private fun minScale(): Float {
- val vPadding = paddingBottom + paddingTop
- val hPadding = paddingLeft + paddingRight
- return when (minimumScaleType) {
- SCALE_TYPE_CENTER_INSIDE -> min(
- (width - hPadding) / sWidth.toFloat(),
- (height - vPadding) / sHeight.toFloat(),
- )
- SCALE_TYPE_CENTER_CROP -> max(
- (width - hPadding) / sWidth.toFloat(),
- (height - vPadding) / sHeight.toFloat(),
- )
- SCALE_TYPE_FIT_WIDTH -> (width - hPadding) / sWidth.toFloat()
- SCALE_TYPE_FIT_HEIGHT -> (height - vPadding) / sHeight.toFloat()
- SCALE_TYPE_ORIGINAL_SIZE -> 1F
- SCALE_TYPE_SMART_FIT -> if (sHeight > sWidth) {
- // Fit to width
- (width - hPadding) / sWidth.toFloat()
- } else {
- // Fit to height
- (height - vPadding) / sHeight.toFloat()
- }
- SCALE_TYPE_CUSTOM -> minScale
- else -> min((width - hPadding) / sWidth.toFloat(), (height - vPadding) / sHeight.toFloat())
- }
- }
-
- /**
- * Adjust a requested scale to be within the allowed limits.
- */
- private fun limitedScale(targetScale: Float): Float = targetScale.coerceIn(minScale(), maxScale)
-
- /**
- * Apply a selected type of easing.
- *
- * @param type Easing type, from static fields
- * @param time Elapsed time
- * @param from Start value
- * @param change Target value
- * @param duration Anm duration
- * @return Current value
- */
- private fun ease(type: Int, time: Long, from: Float, change: Float, duration: Long): Float = when (type) {
- EASE_IN_OUT_QUAD -> easeInOutQuad(time, from, change, duration)
- EASE_OUT_QUAD -> easeOutQuad(time, from, change, duration)
- else -> throw IllegalStateException("Unexpected easing type: $type")
- }
-
- /**
- * Quadratic easing for fling. With thanks to Robert Penner - http://gizma.com/easing/
- *
- * @param time Elapsed time
- * @param from Start value
- * @param change Target value
- * @param duration Anm duration
- * @return Current value
- */
- private fun easeOutQuad(time: Long, from: Float, change: Float, duration: Long): Float {
- val progress = time.toFloat() / duration.toFloat()
- return -change * progress * (progress - 2) + from
- }
-
- /**
- * Quadratic easing for scale and center animations. With thanks to Robert Penner - http://gizma.com/easing/
- *
- * @param time Elapsed time
- * @param from Start value
- * @param change Target value
- * @param duration Anm duration
- * @return Current value
- */
- private fun easeInOutQuad(time: Long, from: Float, change: Float, duration: Long): Float {
- var timeF = time / (duration / 2F)
- return if (timeF < 1) {
- change / 2f * timeF * timeF + from
- } else {
- timeF--
- -change / 2f * (timeF * (timeF - 2) - 1) + from
- }
- }
-
- /**
- * Calculate how much further the image can be panned in each direction. The results are set on
- * the supplied [RectF] and expressed as screen pixels. For example, if the image cannot be
- * panned any further towards the left, the value of [RectF.left] will be set to 0.
- *
- * @param vTarget target object for results. Re-use for efficiency.
- */
- fun getPanRemaining(vTarget: RectF) {
- if (!isReady) {
- return
- }
- val scaleWidth = scale * sWidth
- val scaleHeight = scale * sHeight
- when (panLimit) {
- PAN_LIMIT_CENTER -> {
- vTarget.top = (-(vTranslate!!.y - height / 2)).coerceAtLeast(0F)
- vTarget.left = (-(vTranslate!!.x - width / 2)).coerceAtLeast(0F)
- vTarget.bottom = (vTranslate!!.y - (height / 2 - scaleHeight)).coerceAtLeast(0F)
- vTarget.right = (vTranslate!!.x - (width / 2 - scaleWidth)).coerceAtLeast(0F)
- }
- PAN_LIMIT_OUTSIDE -> {
- vTarget.top = (-(vTranslate!!.y - height)).coerceAtLeast(0F)
- vTarget.left = (-(vTranslate!!.x - width)).coerceAtLeast(0F)
- vTarget.bottom = (vTranslate!!.y + scaleHeight).coerceAtLeast(0F)
- vTarget.right = (vTranslate!!.x + scaleWidth).coerceAtLeast(0F)
- }
- else -> {
- vTarget.top = (-vTranslate!!.y).coerceAtLeast(0F)
- vTarget.left = (-vTranslate!!.x).coerceAtLeast(0F)
- vTarget.bottom = (scaleHeight + vTranslate!!.y - height).coerceAtLeast(0F)
- vTarget.right = (scaleWidth + vTranslate!!.x - width).coerceAtLeast(0F)
- }
- }
- }
-
- /**
- * Set the pan limiting style. See static fields. Normally [.PAN_LIMIT_INSIDE] is best, for image galleries.
- *
- * @param panLimit a pan limit constant. See static fields.
- */
- fun setPanLimit(@PanLimit panLimit: Int) {
- this.panLimit = panLimit
- if (isReady) {
- fitToBounds(true)
- invalidate()
- }
- }
-
- /**
- * Set the minimum scale type. See static fields. Normally [.SCALE_TYPE_CENTER_INSIDE] is best, for image galleries.
- *
- * @param scaleType a scale type constant. See static fields.
- */
- fun setMinimumScaleType(@ScaleType scaleType: Int) {
- minimumScaleType = scaleType
- if (isReady) {
- fitToBounds(true)
- invalidate()
- }
- }
-
- /**
- * This is a screen density aware alternative to [.setMaxScale]; it allows you to express the maximum
- * allowed scale in terms of the minimum pixel density. This avoids the problem of 1:1 scale still being
- * too small on a high density screen. A sensible starting point is 160 - the default used by this view.
- *
- * @param dpi Source image pixel density at maximum zoom.
- */
- fun setMinimumDpi(dpi: Int) {
- val metrics = resources.displayMetrics
- val averageDpi = (metrics.xdpi + metrics.ydpi) / 2
- maxScale = averageDpi / dpi
- }
-
- /**
- * This is a screen density aware alternative to [.setMinScale]; it allows you to express the minimum
- * allowed scale in terms of the maximum pixel density.
- *
- * @param dpi Source image pixel density at minimum zoom.
- */
- fun setMaximumDpi(dpi: Int) {
- val metrics = resources.displayMetrics
- val averageDpi = (metrics.xdpi + metrics.ydpi) / 2
- setMinScale(averageDpi / dpi)
- }
-
- /**
- * Returns the minimum allowed scale.
- *
- * @return the minimum scale as a source/view pixels ratio.
- */
- fun getMinScale(): Float = minScale()
-
- /**
- * Set the minimum scale allowed. A value of 1 means 1:1 pixels at minimum scale. You may wish to set this according
- * to screen density. Consider using [.setMaximumDpi], which is density aware.
- *
- * @param minScale minimum scale expressed as a source/view pixels ratio.
- */
- fun setMinScale(minScale: Float) {
- this.minScale = minScale
- }
-
- /**
- * Returns the source point at the center of the view.
- *
- * @return the source coordinates current at the center of the view.
- */
- val center: PointF?
- get() {
- val mX = width / 2F
- val mY = height / 2F
- return viewToSourceCoord(mX, mY)
- }
-
- /**
- * Externally change the scale and translation of the source image. This may be used with getCenter() and getScale()
- * to restore the scale and zoom after a screen rotate.
- *
- * @param scale New scale to set.
- * @param sCenter New source image coordinate to center on the screen, subject to boundaries.
- */
- fun setScaleAndCenter(scale: Float, sCenter: PointF?) {
- anim = null
- pendingScale = scale
- sPendingCenter = sCenter
- sRequestedCenter = sCenter
- invalidate()
- }
-
- /**
- * Set the scale the image will zoom in to when double tapped. This also the scale point where a double tap is interpreted
- * as a zoom out gesture - if the scale is greater than 90% of this value, a double tap zooms out. Avoid using values
- * greater than the max zoom.
- *
- * @param doubleTapZoomScale New value for double tap gesture zoom scale.
- */
- fun setDoubleTapZoomScale(doubleTapZoomScale: Float) {
- this.doubleTapZoomScale = doubleTapZoomScale
- }
-
- /**
- * A density aware alternative to [.setDoubleTapZoomScale]; this allows you to express the scale the
- * image will zoom in to when double tapped in terms of the image pixel density. Values lower than the max scale will
- * be ignored. A sensible starting point is 160 - the default used by this view.
- *
- * @param dpi New value for double tap gesture zoom scale.
- */
- fun setDoubleTapZoomDpi(dpi: Int) {
- val metrics = resources.displayMetrics
- val averageDpi = (metrics.xdpi + metrics.ydpi) / 2
- setDoubleTapZoomScale(averageDpi / dpi)
- }
-
- /**
- * Set the type of zoom animation to be used for double taps. See static fields.
- *
- * @param doubleTapZoomStyle New value for zoom style.
- */
- fun setDoubleTapZoomStyle(@ZoomStyle doubleTapZoomStyle: Int) {
- this.doubleTapZoomStyle = doubleTapZoomStyle
- }
-
- /**
- * Set the duration of the double tap zoom animation.
- *
- * @param durationMs Duration in milliseconds.
- */
- fun setDoubleTapZoomDuration(durationMs: Int) {
- doubleTapZoomDuration = durationMs.coerceAtLeast(0)
- }
-
- /**
- * {@inheritDoc}
- */
- override fun setOnLongClickListener(onLongClickListener: OnLongClickListener?) {
- this.onLongClickListener = onLongClickListener
- }
-
- /**
- * Add a listener allowing notification of load and error events.
- *
- * @param onImageEventListener an [OnImageEventListener] instance.
- */
- fun setOnImageEventListener(onImageEventListener: OnImageEventListener?) {
- this.onImageEventListener = onImageEventListener
- }
-
- /**
- * Add a listener for pan and zoom events.
- *
- * @param onStateChangedListener an [OnStateChangedListener] instance.
- */
- fun setOnStateChangedListener(onStateChangedListener: OnStateChangedListener?) {
- this.onStateChangedListener = onStateChangedListener
- }
-
- private fun sendStateChanged(oldScale: Float, oldVTranslate: PointF, origin: Int) {
- if (scale != oldScale) {
- onStateChangedListener?.onScaleChanged(scale, origin)
- }
- if (vTranslate != oldVTranslate) {
- onStateChangedListener?.onCenterChanged(center, origin)
- }
- }
-
- /**
- * Creates a panning animation builder, that when started will animate the image to place the given coordinates of
- * the image in the center of the screen. If doing this would move the image beyond the edges of the screen, the
- * image is instead animated to move the center point as near to the center of the screen as is allowed - it's
- * guaranteed to be on screen.
- *
- * @param sCenter Target center point
- * @return [AnimationBuilder] instance. Call [SubsamplingScaleImageView.AnimationBuilder.start] to start the anim.
- */
- fun animateCenter(sCenter: PointF): AnimationBuilder? = if (isReady) AnimationBuilder(sCenter) else null
-
- /**
- * Creates a scale animation builder, that when started will animate a zoom in or out. If this would move the image
- * beyond the panning limits, the image is automatically panned during the animation.
- *
- * @param scale Target scale.
- * @param sCenter Target source center.
- * @return [AnimationBuilder] instance. Call [SubsamplingScaleImageView.AnimationBuilder.start] to start the anim.
- */
- fun animateScaleAndCenter(scale: Float, sCenter: PointF?): AnimationBuilder? = if (isReady) AnimationBuilder(scale, sCenter) else null
-
- /**
- * An event listener, allowing subclasses and activities to be notified of significant events.
- */
- fun interface OnImageEventListener {
- /**
- * Called when the dimensions of the image and view are known, and either a preview image,
- * the full size image, or base layer tiles are loaded. This indicates the scale and translate
- * are known and the next draw will display an image. This event can be used to hide a loading
- * graphic, or inform a subclass that it is safe to draw overlays.
- */
- fun onReady()
- }
-
- /**
- * An event listener, allowing activities to be notified of pan and zoom events. Initialisation
- * and calls made by your code do not trigger events; touch events and animations do. Methods in
- * this listener will be called on the UI thread and may be called very frequently - your
- * implementation should return quickly.
- */
- interface OnStateChangedListener {
- /**
- * The scale has changed. Use with [.getMaxScale] and [.getMinScale] to determine
- * whether the image is fully zoomed in or out.
- *
- * @param newScale The new scale.
- * @param origin Where the event originated from - one of [.ORIGIN_ANIM], [.ORIGIN_TOUCH].
- */
- fun onScaleChanged(newScale: Float, origin: Int)
-
- /**
- * The source center has been changed. This can be a result of panning or zooming.
- *
- * @param newCenter The new source center point.
- * @param origin Where the event originated from - one of [.ORIGIN_ANIM], [.ORIGIN_TOUCH].
- */
- fun onCenterChanged(newCenter: PointF?, origin: Int)
- }
-
- /**
- * @param scaleStart Scale at start of anim
- * @param scaleEnd Scale at end of anim (target)
- * @param sCenterStart Source center point at start
- * @param sCenterEnd Source center point at end, adjusted for pan limits
- * @param sCenterEndRequested Source center point that was requested, without adjustments
- * @param vFocusStart View point that was double tapped
- * @param vFocusEnd Where the view focal point should be moved to during the anim
- * @param duration How long the anim takes
- * @param interruptible Whether the anim can be interrupted by a touch
- * @param easing Easing style
- * @param origin Animation origin (API, double tap or fling)
- * @param time Start time
- */
- private data class Anim(
- val scaleStart: Float,
- val scaleEnd: Float,
- val sCenterStart: PointF?,
- val sCenterEnd: PointF,
- val sCenterEndRequested: PointF,
- val vFocusStart: PointF?,
- val vFocusEnd: PointF,
- val duration: Long,
- val interruptible: Boolean,
- val easing: Int,
- val origin: Int,
- val time: Long = System.currentTimeMillis(),
- )
-
- private data class ScaleAndTranslate(var scale: Float, val vTranslate: PointF)
-
- /**
- * Builder class used to set additional options for a scale animation. Create an instance using [.animateScale],
- * then set your options and call [.start].
- */
- inner class AnimationBuilder {
- private val targetScale: Float
- private val targetSCenter: PointF?
- private val vFocus: PointF?
- private var duration: Long = 500
- private var easing = EASE_IN_OUT_QUAD
- private var origin = ORIGIN_ANIM
- private var interruptible = true
- private var panLimited = true
-
- constructor(sCenter: PointF) {
- targetScale = scale
- targetSCenter = sCenter
- vFocus = null
- }
-
- constructor(scale: Float, sCenter: PointF?) {
- targetScale = scale
- targetSCenter = sCenter
- vFocus = null
- }
-
- constructor(scale: Float, sCenter: PointF?, vFocus: PointF?) {
- targetScale = scale
- targetSCenter = sCenter
- this.vFocus = vFocus
- }
-
- /**
- * Desired duration of the anim in milliseconds. Default is 500.
- *
- * @param duration duration in milliseconds.
- * @return this builder for method chaining.
- */
- fun withDuration(duration: Long): AnimationBuilder {
- this.duration = duration
- return this
- }
-
- /**
- * Whether the animation can be interrupted with a touch. Default is true.
- *
- * @param interruptible interruptible flag.
- * @return this builder for method chaining.
- */
- fun withInterruptible(interruptible: Boolean): AnimationBuilder {
- this.interruptible = interruptible
- return this
- }
-
- /**
- * Set the easing style. See static fields. [.EASE_IN_OUT_QUAD] is recommended, and the default.
- *
- * @param easing easing style.
- * @return this builder for method chaining.
- */
- fun withEasing(@EasingStyle easing: Int): AnimationBuilder {
- this.easing = easing
- return this
- }
-
- /**
- * Only for internal use. When set to true, the animation proceeds towards the actual end point - the nearest
- * point to the center allowed by pan limits. When false, animation is in the direction of the requested end
- * point and is stopped when the limit for each axis is reached. The latter behaviour is used for flings but
- * nothing else.
- */
- fun withPanLimited(panLimited: Boolean): AnimationBuilder {
- this.panLimited = panLimited
- return this
- }
-
- /**
- * Only for internal use. Indicates what caused the animation.
- */
- fun withOrigin(origin: Int): AnimationBuilder {
- this.origin = origin
- return this
- }
-
- /**
- * Starts the animation.
- */
- fun start() {
- val vxCenter = paddingLeft + (width - paddingRight - paddingLeft) / 2
- val vyCenter = paddingTop + (height - paddingBottom - paddingTop) / 2
- val targetScale = limitedScale(targetScale)
- val targetSCenter = if (panLimited) {
- limitedSCenter(
- targetSCenter!!.x,
- targetSCenter.y,
- targetScale,
- PointF(),
- )
- } else {
- targetSCenter!!
- }
- anim = Anim(
- scaleStart = scale,
- scaleEnd = targetScale,
- sCenterEndRequested = targetSCenter,
- sCenterStart = center,
- sCenterEnd = targetSCenter,
- vFocusStart = sourceToViewCoord(targetSCenter),
- vFocusEnd = PointF(
- vxCenter.toFloat(),
- vyCenter.toFloat(),
- ),
- duration = duration,
- interruptible = interruptible,
- easing = easing,
- origin = origin,
- )
- if (vFocus != null) {
- // Calculate where translation will be at the end of the anim
- val vTranslateXEnd = vFocus.x - targetScale * anim!!.sCenterStart!!.x
- val vTranslateYEnd = vFocus.y - targetScale * anim!!.sCenterStart!!.y
- val satEnd = ScaleAndTranslate(targetScale, PointF(vTranslateXEnd, vTranslateYEnd))
- // Fit the end translation into bounds
- fitToBounds(true, satEnd)
- // Adjust the position of the focus point at end so image will be in bounds
- anim!!.vFocusEnd.set(
- vFocus.x + (satEnd.vTranslate.x - vTranslateXEnd),
- vFocus.y + (satEnd.vTranslate.y - vTranslateYEnd),
- )
- }
- invalidate()
- }
- }
-
- companion object {
- @Retention(AnnotationRetention.SOURCE)
- @IntDef(
- ZOOM_FOCUS_FIXED,
- ZOOM_FOCUS_CENTER,
- ZOOM_FOCUS_CENTER_IMMEDIATE,
- )
- annotation class ZoomStyle
-
- /**
- * During zoom animation, keep the point of the image that was tapped in the same place, and scale the image around it.
- */
- const val ZOOM_FOCUS_FIXED = 1
-
- /**
- * During zoom animation, move the point of the image that was tapped to the center of the screen.
- */
- const val ZOOM_FOCUS_CENTER = 2
-
- /**
- * Zoom in to and center the tapped point immediately without animating.
- */
- const val ZOOM_FOCUS_CENTER_IMMEDIATE = 3
-
- @Retention(AnnotationRetention.SOURCE)
- @IntDef(
- EASE_IN_OUT_QUAD,
- EASE_OUT_QUAD,
- )
- annotation class EasingStyle
-
- /**
- * Quadratic ease out. Not recommended for scale animation, but good for panning.
- */
- const val EASE_OUT_QUAD = 1
-
- /**
- * Quadratic ease in and out.
- */
- const val EASE_IN_OUT_QUAD = 2
-
- @Retention(AnnotationRetention.SOURCE)
- @IntDef(
- PAN_LIMIT_INSIDE,
- PAN_LIMIT_OUTSIDE,
- PAN_LIMIT_CENTER,
- )
- annotation class PanLimit
-
- /**
- * Don't allow the image to be panned off screen. As much of the image as possible is always displayed, centered in the view when it is smaller. This is the best option for galleries.
- */
- const val PAN_LIMIT_INSIDE = 1
-
- /**
- * Allows the image to be panned until it is just off screen, but no further. The edge of the image will stop when it is flush with the screen edge.
- */
- const val PAN_LIMIT_OUTSIDE = 2
-
- /**
- * Allows the image to be panned until a corner reaches the center of the screen but no further. Useful when you want to pan any spot on the image to the exact center of the screen.
- */
- const val PAN_LIMIT_CENTER = 3
-
- @Retention(AnnotationRetention.SOURCE)
- @IntDef(
- SCALE_TYPE_CENTER_INSIDE,
- SCALE_TYPE_CENTER_CROP,
- SCALE_TYPE_FIT_WIDTH,
- SCALE_TYPE_FIT_HEIGHT,
- SCALE_TYPE_ORIGINAL_SIZE,
- SCALE_TYPE_SMART_FIT,
- SCALE_TYPE_CUSTOM,
- )
- annotation class ScaleType
-
- /**
- * Scale the image so that both dimensions of the image will be equal to or less than the corresponding dimension of the view. The image is then centered in the view. This is the default behaviour and best for galleries.
- */
- const val SCALE_TYPE_CENTER_INSIDE = 1
-
- /**
- * Scale the image uniformly so that both dimensions of the image will be equal to or larger than the corresponding dimension of the view. The image is then centered in the view.
- */
- const val SCALE_TYPE_CENTER_CROP = 2
- const val SCALE_TYPE_FIT_WIDTH = 3
- const val SCALE_TYPE_FIT_HEIGHT = 4
- const val SCALE_TYPE_ORIGINAL_SIZE = 5
- const val SCALE_TYPE_SMART_FIT = 6
-
- /**
- * Scale the image so that both dimensions of the image will be equal to or less than the maxScale and equal to or larger than minScale. The image is then centered in the view.
- */
- const val SCALE_TYPE_CUSTOM = 7
-
- /**
- * State change originated from animation.
- */
- const val ORIGIN_ANIM = 1
-
- /**
- * State change originated from touch gesture.
- */
- const val ORIGIN_TOUCH = 2
-
- /**
- * State change originated from a fling momentum anim.
- */
- const val ORIGIN_FLING = 3
-
- /**
- * State change originated from a double tap zoom anim.
- */
- const val ORIGIN_DOUBLE_TAP_ZOOM = 4
- }
-}
diff --git a/app/src/main/kotlin/com/hippo/ehviewer/Settings.kt b/app/src/main/kotlin/com/hippo/ehviewer/Settings.kt
index 5ea5f076f..ca7cd4133 100644
--- a/app/src/main/kotlin/com/hippo/ehviewer/Settings.kt
+++ b/app/src/main/kotlin/com/hippo/ehviewer/Settings.kt
@@ -175,9 +175,7 @@ object Settings : DataStorePreferences(null) {
var defaultDownloadLabel by stringOrNullPref("default_download_label", null)
var lastUpdateTime by longPref("last_update_time", BuildConfig.COMMIT_TIME)
- // Tachiyomi Reader
- var newReader by boolPref("new_compose_reader", false)
-
+ // Reader
val cropBorder = boolPref("crop_borders", false)
val colorFilter = boolPref("pref_color_filter_key", false)
val colorFilterValue = intPref("color_filter_value", 0)
diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/CommonOperations.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/CommonOperations.kt
index 46f5e201a..bc0346ddf 100644
--- a/app/src/main/kotlin/com/hippo/ehviewer/ui/CommonOperations.kt
+++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/CommonOperations.kt
@@ -70,7 +70,6 @@ import com.hippo.files.delete
import com.hippo.files.isDirectory
import com.hippo.files.openOutputStream
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
-import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlinx.coroutines.sync.Mutex
@@ -282,32 +281,14 @@ suspend fun removeFromFavorites(galleryInfo: BaseGalleryInfo) = doModifyFavorite
localFavorited = EhDB.containLocalFavorites(galleryInfo.gid),
)
-context (Context, DestinationsNavigator)
+context (DestinationsNavigator)
fun navToReader(
info: BaseGalleryInfo,
page: Int = -1,
-) {
- if (Settings.newReader) {
- navigate(ReaderScreenDestination(ReaderScreenArgs.Gallery(info, page)))
- } else {
- val intent = Intent(this@Context, ReaderActivity::class.java)
- intent.action = ReaderActivity.ACTION_EH
- intent.putExtra(ReaderActivity.KEY_GALLERY_INFO, info)
- intent.putExtra(ReaderActivity.KEY_PAGE, page)
- startActivity(intent)
- }
-}
+) = navigate(ReaderScreenDestination(ReaderScreenArgs.Gallery(info, page)))
-fun DestinationsNavigator.navToReader(context: Context, uri: Uri) {
- if (Settings.newReader) {
- navigate(ReaderScreenDestination(ReaderScreenArgs.Archive(uri)))
- } else {
- val intent = Intent(context, ReaderActivity::class.java)
- intent.action = Intent.ACTION_VIEW
- intent.data = uri
- context.startActivity(intent)
- }
-}
+fun DestinationsNavigator.navToReader(uri: Uri) =
+ navigate(ReaderScreenDestination(ReaderScreenArgs.Archive(uri)))
context(DialogState, Context, DestinationsNavigator)
suspend fun doGalleryInfoAction(info: BaseGalleryInfo) {
diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/ComposeDefault.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/ComposeDefault.kt
index 2c031b845..72a7a6836 100644
--- a/app/src/main/kotlin/com/hippo/ehviewer/ui/ComposeDefault.kt
+++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/ComposeDefault.kt
@@ -17,7 +17,7 @@ inline fun composing(
block: @Composable context(MainActivity, SnackbarHostState, DialogState, DestinationsNavigator, CoroutineScope)
() -> R,
) = block(
- LocalContext.current.run { remember { findActivity() } },
+ LocalContext.current.run { remember { findActivity() } },
LocalSnackBarHostState.current,
LocalDialogState.current,
navigator,
diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/MainActivity.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/MainActivity.kt
index e95f268a0..5916a9a22 100644
--- a/app/src/main/kotlin/com/hippo/ehviewer/ui/MainActivity.kt
+++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/MainActivity.kt
@@ -270,7 +270,7 @@ class MainActivity : EhActivity() {
val uri = intent.data ?: return@collect
when (uri.scheme) {
SCHEME_CONTENT, SCHEME_FILE -> {
- navigator.navToReader(applicationContext, uri)
+ navigator.navToReader(uri)
}
else -> {
diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/Theme.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/Theme.kt
index ae0d3d7ec..230ca8f93 100644
--- a/app/src/main/kotlin/com/hippo/ehviewer/ui/Theme.kt
+++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/Theme.kt
@@ -8,7 +8,6 @@ import androidx.compose.animation.ExitTransition
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
-import androidx.compose.ui.platform.ComposeView
import androidx.navigation.NavBackStackEntry
import com.hippo.ehviewer.ui.theme.EhTheme
import com.hippo.ehviewer.ui.tools.DialogState
@@ -19,18 +18,6 @@ import soup.compose.material.motion.animation.materialSharedAxisXIn
import soup.compose.material.motion.animation.materialSharedAxisXOut
import soup.compose.material.motion.animation.rememberSlideDistance
-inline fun ComposeView.setMD3Content(crossinline content: @Composable () -> Unit) = setContent {
- EhTheme {
- val dialogState = remember { DialogState() }
- dialogState.Intercept()
- CompositionLocalProvider(
- LocalDialogState provides dialogState,
- ) {
- content()
- }
- }
-}
-
inline fun ComponentActivity.setMD3Content(crossinline content: @Composable () -> Unit) = setContent {
EhTheme {
ProvideVectorPainterCache {
diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/GalleryPager.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/GalleryPager.kt
index 8c45a0c5f..951dce044 100644
--- a/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/GalleryPager.kt
+++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/GalleryPager.kt
@@ -38,7 +38,6 @@ import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.collectAsState
import com.hippo.ehviewer.gallery.PageLoader2
import com.hippo.ehviewer.ui.main.plus
-import eu.kanade.tachiyomi.data.preference.PreferenceValues.TappingInvertMode
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
@@ -46,6 +45,7 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType.CONTINUOUS_VERTICAL
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType.RIGHT_TO_LEFT
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType.VERTICAL
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType.WEBTOON
+import eu.kanade.tachiyomi.ui.reader.setting.TappingInvertMode
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation.NavigationRegion
import kotlinx.coroutines.CoroutineScope
diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/AdvancedScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/AdvancedScreen.kt
index f19f45c86..1a3300277 100644
--- a/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/AdvancedScreen.kt
+++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/AdvancedScreen.kt
@@ -89,12 +89,6 @@ fun AdvancedScreen(navigator: DestinationsNavigator) {
snackbarHost = { SnackbarHost(snackbarHostState) },
) { paddingValues ->
Column(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection).verticalScroll(rememberScrollState()).padding(paddingValues)) {
- if (AppConfig.isSnapshot) {
- SwitchPreference(
- title = "New Compose Reader [WIP!!!]",
- value = Settings::newReader,
- )
- }
SwitchPreference(
title = stringResource(id = R.string.settings_advanced_save_parse_error_body),
summary = stringResource(id = R.string.settings_advanced_save_parse_error_body_summary),
diff --git a/app/src/main/kotlin/com/hippo/ehviewer/util/LazyDelegate.kt b/app/src/main/kotlin/com/hippo/ehviewer/util/LazyDelegate.kt
index 0c6cc9d32..0b10170be 100644
--- a/app/src/main/kotlin/com/hippo/ehviewer/util/LazyDelegate.kt
+++ b/app/src/main/kotlin/com/hippo/ehviewer/util/LazyDelegate.kt
@@ -1,24 +1,3 @@
-@file:Suppress("NOTHING_TO_INLINE")
-
package com.hippo.ehviewer.util
-import kotlin.reflect.KMutableProperty0
-import kotlin.reflect.KProperty
-
fun unsafeLazy(initializer: () -> T) = lazy(LazyThreadSafetyMode.NONE, initializer)
-
-interface Lazy {
- var value: T
-}
-
-fun lazyMut(init: () -> KMutableProperty0) = object : Lazy {
- private val lazy by lazy { init() }
- override var value
- get() = lazy.get()
- set(value) = lazy.set(value)
-}
-
-inline operator fun Lazy.getValue(thisRef: Any?, property: KProperty<*>): T = value
-inline operator fun Lazy.setValue(thisRef: Any?, prop: KProperty<*>?, newValue: T) {
- value = newValue
-}
diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/core/preference/AndroidPreference.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/core/preference/AndroidPreference.kt
deleted file mode 100644
index 7a2087b7d..000000000
--- a/app/src/main/kotlin/eu/kanade/tachiyomi/core/preference/AndroidPreference.kt
+++ /dev/null
@@ -1,150 +0,0 @@
-package eu.kanade.tachiyomi.core.preference
-
-import android.content.SharedPreferences
-import android.content.SharedPreferences.Editor
-import androidx.core.content.edit
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.conflate
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.flow.stateIn
-
-sealed class AndroidPreference(
- private val preferences: SharedPreferences,
- private val keyFlow: Flow,
- private val key: String,
- private val defaultValue: T,
-) : Preference {
-
- abstract fun read(preferences: SharedPreferences, key: String, defaultValue: T): T
-
- abstract fun write(key: String, value: T): Editor.() -> Unit
-
- override fun key(): String = key
-
- override fun get(): T = read(preferences, key, defaultValue)
-
- // Async, don't call get() immediately after this
- override fun set(value: T) {
- preferences.edit(action = write(key, value))
- }
-
- override fun isSet(): Boolean = preferences.contains(key)
-
- override fun delete() {
- preferences.edit {
- remove(key)
- }
- }
-
- override fun defaultValue(): T = defaultValue
-
- override fun changes(): Flow = keyFlow
- .filter { it == key || it == null }
- .onStart { emit("ignition") }
- .map { get() }
- .conflate()
-
- override fun stateIn(scope: CoroutineScope): StateFlow = changes().stateIn(scope, SharingStarted.Eagerly, get())
-
- class StringPrimitive(
- preferences: SharedPreferences,
- keyFlow: Flow,
- key: String,
- defaultValue: String,
- ) : AndroidPreference(preferences, keyFlow, key, defaultValue) {
- override fun read(preferences: SharedPreferences, key: String, defaultValue: String): String = preferences.getString(key, defaultValue) ?: defaultValue
-
- override fun write(key: String, value: String): Editor.() -> Unit = {
- putString(key, value)
- }
- }
-
- class LongPrimitive(
- preferences: SharedPreferences,
- keyFlow: Flow,
- key: String,
- defaultValue: Long,
- ) : AndroidPreference(preferences, keyFlow, key, defaultValue) {
- override fun read(preferences: SharedPreferences, key: String, defaultValue: Long): Long = preferences.getLong(key, defaultValue)
-
- override fun write(key: String, value: Long): Editor.() -> Unit = {
- putLong(key, value)
- }
- }
-
- class IntPrimitive(
- preferences: SharedPreferences,
- keyFlow: Flow,
- key: String,
- defaultValue: Int,
- ) : AndroidPreference(preferences, keyFlow, key, defaultValue) {
- override fun read(preferences: SharedPreferences, key: String, defaultValue: Int): Int = preferences.getInt(key, defaultValue)
-
- override fun write(key: String, value: Int): Editor.() -> Unit = {
- putInt(key, value)
- }
- }
-
- class FloatPrimitive(
- preferences: SharedPreferences,
- keyFlow: Flow,
- key: String,
- defaultValue: Float,
- ) : AndroidPreference(preferences, keyFlow, key, defaultValue) {
- override fun read(preferences: SharedPreferences, key: String, defaultValue: Float): Float = preferences.getFloat(key, defaultValue)
-
- override fun write(key: String, value: Float): Editor.() -> Unit = {
- putFloat(key, value)
- }
- }
-
- class BooleanPrimitive(
- preferences: SharedPreferences,
- keyFlow: Flow,
- key: String,
- defaultValue: Boolean,
- ) : AndroidPreference(preferences, keyFlow, key, defaultValue) {
- override fun read(preferences: SharedPreferences, key: String, defaultValue: Boolean): Boolean = preferences.getBoolean(key, defaultValue)
-
- override fun write(key: String, value: Boolean): Editor.() -> Unit = {
- putBoolean(key, value)
- }
- }
-
- class StringSetPrimitive(
- preferences: SharedPreferences,
- keyFlow: Flow,
- key: String,
- defaultValue: Set,
- ) : AndroidPreference>(preferences, keyFlow, key, defaultValue) {
- override fun read(preferences: SharedPreferences, key: String, defaultValue: Set): Set = preferences.getStringSet(key, defaultValue) ?: defaultValue
-
- override fun write(key: String, value: Set): Editor.() -> Unit = {
- putStringSet(key, value)
- }
- }
-
- class Object(
- preferences: SharedPreferences,
- keyFlow: Flow,
- key: String,
- defaultValue: T,
- val serializer: (T) -> String,
- val deserializer: (String) -> T,
- ) : AndroidPreference(preferences, keyFlow, key, defaultValue) {
- override fun read(preferences: SharedPreferences, key: String, defaultValue: T): T = try {
- preferences.getString(key, null)?.let(deserializer) ?: defaultValue
- } catch (e: Exception) {
- defaultValue
- }
-
- override fun write(key: String, value: T): Editor.() -> Unit = {
- putString(key, serializer(value))
- }
- }
-}
diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/core/preference/AndroidPreferenceStore.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/core/preference/AndroidPreferenceStore.kt
deleted file mode 100644
index 90b6d8645..000000000
--- a/app/src/main/kotlin/eu/kanade/tachiyomi/core/preference/AndroidPreferenceStore.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-package eu.kanade.tachiyomi.core.preference
-
-import android.content.SharedPreferences
-import com.hippo.ehviewer.Settings
-import eu.kanade.tachiyomi.core.preference.AndroidPreference.BooleanPrimitive
-import eu.kanade.tachiyomi.core.preference.AndroidPreference.FloatPrimitive
-import eu.kanade.tachiyomi.core.preference.AndroidPreference.IntPrimitive
-import eu.kanade.tachiyomi.core.preference.AndroidPreference.LongPrimitive
-import eu.kanade.tachiyomi.core.preference.AndroidPreference.Object
-import eu.kanade.tachiyomi.core.preference.AndroidPreference.StringPrimitive
-import eu.kanade.tachiyomi.core.preference.AndroidPreference.StringSetPrimitive
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.callbackFlow
-
-object AndroidPreferenceStore : PreferenceStore {
-
- private val sharedPreferences = Settings.prefs
-
- private val keyFlow = sharedPreferences.keyFlow
-
- override fun getString(key: String, defaultValue: String): Preference = StringPrimitive(sharedPreferences, keyFlow, key, defaultValue)
-
- override fun getLong(key: String, defaultValue: Long): Preference = LongPrimitive(sharedPreferences, keyFlow, key, defaultValue)
-
- override fun getInt(key: String, defaultValue: Int): Preference = IntPrimitive(sharedPreferences, keyFlow, key, defaultValue)
-
- override fun getFloat(key: String, defaultValue: Float): Preference = FloatPrimitive(sharedPreferences, keyFlow, key, defaultValue)
-
- override fun getBoolean(key: String, defaultValue: Boolean): Preference = BooleanPrimitive(sharedPreferences, keyFlow, key, defaultValue)
-
- override fun getStringSet(key: String, defaultValue: Set): Preference> = StringSetPrimitive(sharedPreferences, keyFlow, key, defaultValue)
-
- override fun getObject(
- key: String,
- defaultValue: T,
- serializer: (T) -> String,
- deserializer: (String) -> T,
- ): Preference = Object(
- preferences = sharedPreferences,
- keyFlow = keyFlow,
- key = key,
- defaultValue = defaultValue,
- serializer = serializer,
- deserializer = deserializer,
- )
-}
-
-private val SharedPreferences.keyFlow
- get() = callbackFlow {
- val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key: String? -> trySend(key) }
- registerOnSharedPreferenceChangeListener(listener)
- awaitClose {
- unregisterOnSharedPreferenceChangeListener(listener)
- }
- }
diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/core/preference/Preference.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/core/preference/Preference.kt
deleted file mode 100644
index dc898f4d8..000000000
--- a/app/src/main/kotlin/eu/kanade/tachiyomi/core/preference/Preference.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-package eu.kanade.tachiyomi.core.preference
-
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.StateFlow
-
-interface Preference {
-
- fun key(): String
-
- fun get(): T
-
- fun set(value: T)
-
- fun isSet(): Boolean
-
- fun delete()
-
- fun defaultValue(): T
-
- fun changes(): Flow
-
- fun stateIn(scope: CoroutineScope): StateFlow
-}
-
-inline fun Preference.getAndSet(crossinline block: (T) -> R) = set(block(get()))
diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/core/preference/PreferenceStore.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/core/preference/PreferenceStore.kt
deleted file mode 100644
index 923d72a51..000000000
--- a/app/src/main/kotlin/eu/kanade/tachiyomi/core/preference/PreferenceStore.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-package eu.kanade.tachiyomi.core.preference
-
-interface PreferenceStore {
-
- fun getString(key: String, defaultValue: String = ""): Preference
-
- fun getLong(key: String, defaultValue: Long = 0): Preference
-
- fun getInt(key: String, defaultValue: Int = 0): Preference
-
- fun getFloat(key: String, defaultValue: Float = 0f): Preference
-
- fun getBoolean(key: String, defaultValue: Boolean = false): Preference
-
- fun getStringSet(key: String, defaultValue: Set = emptySet()): Preference>
-
- fun getObject(
- key: String,
- defaultValue: T,
- serializer: (T) -> String,
- deserializer: (String) -> T,
- ): Preference
-}
-
-inline fun > PreferenceStore.getEnum(
- key: String,
- defaultValue: T,
-): Preference = getObject(
- key = key,
- defaultValue = defaultValue,
- serializer = { it.name },
- deserializer = {
- try {
- enumValueOf(it)
- } catch (e: IllegalArgumentException) {
- defaultValue
- }
- },
-)
diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/data/preference/PreferenceValues.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/data/preference/PreferenceValues.kt
deleted file mode 100644
index 50ed91a91..000000000
--- a/app/src/main/kotlin/eu/kanade/tachiyomi/data/preference/PreferenceValues.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package eu.kanade.tachiyomi.data.preference
-
-const val MANGA_NON_COMPLETED = "manga_ongoing"
-const val MANGA_HAS_UNREAD = "manga_fully_read"
-const val MANGA_NON_READ = "manga_started"
-
-/**
- * This class stores the values for the preferences in the application.
- */
-object PreferenceValues {
-
- enum class TappingInvertMode(val shouldInvertHorizontal: Boolean = false, val shouldInvertVertical: Boolean = false) {
- NONE,
- HORIZONTAL(shouldInvertHorizontal = true),
- VERTICAL(shouldInvertVertical = true),
- BOTH(shouldInvertHorizontal = true, shouldInvertVertical = true),
- }
-
- enum class ReaderHideThreshold(val threshold: Int) {
- HIGHEST(5),
- HIGH(13),
- LOW(31),
- LOWEST(47),
- }
-}
diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ChapterNavigator.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ChapterNavigator.kt
index accee0ee9..787ace083 100644
--- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ChapterNavigator.kt
+++ b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ChapterNavigator.kt
@@ -26,9 +26,10 @@ import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
+import androidx.window.core.layout.WindowWidthSizeClass
import com.hippo.ehviewer.ui.tools.HapticFeedbackType
+import com.hippo.ehviewer.ui.tools.LocalWindowSizeClass
import com.hippo.ehviewer.ui.tools.rememberHapticFeedback
-import eu.kanade.tachiyomi.util.system.isTabletUi
import kotlin.math.roundToInt
@Composable
@@ -38,7 +39,8 @@ fun ChapterNavigator(
totalPages: Int,
onSliderValueChange: (Int) -> Unit,
) {
- val isTabletUi = LocalConfiguration.current.isTabletUi()
+ val windowSizeClass = LocalWindowSizeClass.current
+ val isTabletUi = windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED
val horizontalPadding = if (isTabletUi) 24.dp else 16.dp
val layoutDirection = if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr
val configuration = LocalConfiguration.current
diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
deleted file mode 100644
index 6e337a245..000000000
--- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
+++ /dev/null
@@ -1,790 +0,0 @@
-/*
- * Copyright 2016 Hippo Seven
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package eu.kanade.tachiyomi.ui.reader
-
-import android.Manifest
-import android.app.assist.AssistContent
-import android.content.ClipData
-import android.content.ContentValues
-import android.content.Context
-import android.content.Intent
-import android.graphics.ColorMatrix
-import android.graphics.ColorMatrixColorFilter
-import android.graphics.Paint
-import android.net.Uri
-import android.os.Bundle
-import android.os.Environment
-import android.provider.MediaStore
-import android.view.KeyEvent
-import android.view.MotionEvent
-import android.view.View.LAYER_TYPE_HARDWARE
-import android.webkit.MimeTypeMap
-import android.widget.Toast
-import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
-import androidx.activity.viewModels
-import androidx.annotation.StringRes
-import androidx.compose.animation.animateColorAsState
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.WindowInsetsSides
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.navigationBarsPadding
-import androidx.compose.foundation.layout.only
-import androidx.compose.foundation.layout.safeDrawing
-import androidx.compose.foundation.layout.windowInsetsPadding
-import androidx.compose.material3.BottomSheetDefaults
-import androidx.compose.material3.LocalTextStyle
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.ModalBottomSheet
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.BlendMode
-import androidx.compose.ui.graphics.Color
-import androidx.core.content.FileProvider
-import androidx.core.view.WindowInsetsCompat
-import androidx.core.view.WindowInsetsControllerCompat
-import androidx.core.view.isVisible
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.lifecycleScope
-import arrow.core.raise.ensure
-import com.hippo.ehviewer.BuildConfig
-import com.hippo.ehviewer.R
-import com.hippo.ehviewer.Settings
-import com.hippo.ehviewer.client.EhUrl
-import com.hippo.ehviewer.client.data.BaseGalleryInfo
-import com.hippo.ehviewer.collectAsState
-import com.hippo.ehviewer.databinding.ReaderActivityBinding
-import com.hippo.ehviewer.download.DownloadManager
-import com.hippo.ehviewer.download.archiveFile
-import com.hippo.ehviewer.gallery.ArchivePageLoader
-import com.hippo.ehviewer.gallery.EhPageLoader
-import com.hippo.ehviewer.gallery.PageLoader2
-import com.hippo.ehviewer.ui.EhActivity
-import com.hippo.ehviewer.ui.reader.SettingsPager
-import com.hippo.ehviewer.ui.reader.setCustomBrightnessValue
-import com.hippo.ehviewer.ui.reader.setOrientation
-import com.hippo.ehviewer.ui.reader.updateKeepScreenOn
-import com.hippo.ehviewer.ui.screen.implicit
-import com.hippo.ehviewer.ui.setMD3Content
-import com.hippo.ehviewer.ui.tools.DialogState
-import com.hippo.ehviewer.util.AppConfig
-import com.hippo.ehviewer.util.FileUtils
-import com.hippo.ehviewer.util.awaitActivityResult
-import com.hippo.ehviewer.util.displayPath
-import com.hippo.ehviewer.util.getParcelableCompat
-import com.hippo.ehviewer.util.getParcelableExtraCompat
-import com.hippo.ehviewer.util.getValue
-import com.hippo.ehviewer.util.isAtLeastQ
-import com.hippo.ehviewer.util.lazyMut
-import com.hippo.ehviewer.util.requestPermission
-import com.hippo.ehviewer.util.setValue
-import com.hippo.files.toOkioPath
-import dev.chrisbanes.insetter.applyInsetter
-import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
-import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
-import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
-import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
-import eu.kanade.tachiyomi.util.lang.launchIO
-import eu.kanade.tachiyomi.util.lang.withUIContext
-import eu.kanade.tachiyomi.util.system.isNightMode
-import eu.kanade.tachiyomi.util.system.logcat
-import java.io.File
-import kotlin.coroutines.resume
-import kotlinx.coroutines.FlowPreview
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.debounce
-import kotlinx.coroutines.flow.drop
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.merge
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.flow.sample
-import kotlinx.coroutines.launch
-import kotlinx.datetime.Clock
-import moe.tarsin.coroutines.runSuspendCatching
-import okio.Path.Companion.toOkioPath
-import splitties.systemservices.clipboardManager
-
-class GalleryModel : ViewModel() {
- var galleryProvider: PageLoader2? = null
- override fun onCleared() {
- super.onCleared()
- galleryProvider?.stop()
- galleryProvider = null
- }
-}
-
-class ReaderActivity : EhActivity() {
- lateinit var binding: ReaderActivityBinding
- private var mAction: String? = null
- private var mUri: Uri? = null
- private var mGalleryInfo: BaseGalleryInfo? = null
- private var mPage: Int = -1
- private val vm: GalleryModel by viewModels()
- private val dialogState = DialogState()
-
- /**
- * Whether the menu is currently visible.
- */
- var menuVisible by mutableStateOf(false)
- private var mGalleryProvider by lazyMut { vm::galleryProvider }
- private var mCurrentIndex: Int
- get() = mGalleryProvider!!.startPage
- set(value) {
- mGalleryProvider!!.startPage = value
- }
-
- private val galleryDetailUrl: String?
- get() = mGalleryInfo?.run { EhUrl.getGalleryDetailUrl(gid, token) }
-
- private suspend fun buildProvider(replace: Boolean = false): PageLoader2? {
- mGalleryProvider?.let {
- if (replace) it.stop() else return it
- }
-
- val provider = when (mAction) {
- ACTION_EH -> {
- mGalleryInfo?.let {
- DownloadManager.getDownloadInfo(it.gid)?.archiveFile
- ?.let { file -> ArchivePageLoader(file, it.gid, mPage) }
- ?: EhPageLoader(it, mPage)
- }
- }
-
- Intent.ACTION_VIEW -> {
- mUri?.run {
- ArchivePageLoader(toOkioPath()) { invalidator ->
- runCatching {
- dialogState.awaitInputText(
- title = getString(R.string.archive_need_passwd),
- hint = getString(R.string.archive_passwd),
- ) { text ->
- ensure(text.isNotBlank()) { getString(R.string.passwd_cannot_be_empty) }
- ensure(invalidator(text)) { getString(R.string.passwd_wrong) }
- }
- }.onFailure {
- finish()
- }.getOrThrow()
- }
- }
- }
-
- else -> null
- }
- if (provider == null) {
- makeToast(R.string.error_reading_failed)
- finish()
- }
- return provider
- }
-
- private fun handleIntent(intent: Intent?) {
- intent ?: return
- mAction = intent.action
- mUri = intent.data
- mGalleryInfo = intent.getParcelableExtraCompat(KEY_GALLERY_INFO)
- mPage = intent.getIntExtra(KEY_PAGE, -1)
- }
-
- private fun onInit() {
- handleIntent(intent)
- }
-
- private fun onRestore(savedInstanceState: Bundle) {
- mAction = savedInstanceState.getString(KEY_ACTION)
- mUri = savedInstanceState.getParcelableCompat(KEY_URI)
- mGalleryInfo = savedInstanceState.getParcelableCompat(KEY_GALLERY_INFO)
- mPage = savedInstanceState.getInt(KEY_PAGE, -1)
- }
-
- override fun onNewIntent(intent: Intent) {
- super.onNewIntent(intent)
- handleIntent(intent)
- lifecycleScope.launchIO {
- buildProvider(true)?.let {
- mGalleryProvider = it
- it.start()
- if (it.awaitReady()) {
- withUIContext {
- totalPage = it.size
- viewer?.setGalleryProvider(it)
- moveToPageIndex(0)
- }
- } else {
- makeToast(R.string.error_reading_failed)
- finish()
- }
- }
- }
- }
-
- override fun onSaveInstanceState(outState: Bundle) {
- super.onSaveInstanceState(outState)
- outState.putString(KEY_ACTION, mAction)
- outState.putParcelable(KEY_URI, mUri)
- if (mGalleryInfo != null) {
- outState.putParcelable(KEY_GALLERY_INFO, mGalleryInfo)
- }
- outState.putInt(KEY_PAGE, mPage)
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- if (savedInstanceState == null) {
- onInit()
- } else {
- onRestore(savedInstanceState)
- }
- binding = ReaderActivityBinding.inflate(layoutInflater)
- binding.dialogStub.setMD3Content {
- val brightness by Settings.customBrightness.collectAsState()
- val brightnessValue by Settings.customBrightnessValue.collectAsState()
- val colorOverlayEnabled by Settings.colorFilter.collectAsState()
- val colorOverlay by Settings.colorFilterValue.collectAsState()
- val colorOverlayMode by Settings.colorFilterMode.collectAsState {
- when (it) {
- 1 -> BlendMode.Multiply
- 2 -> BlendMode.Screen
- 3 -> BlendMode.Overlay
- 4 -> BlendMode.Lighten
- 5 -> BlendMode.Darken
- else -> BlendMode.SrcOver
- }
- }
- dialogState.Intercept()
-
- val showSeekbar by Settings.showReaderSeekbar.collectAsState()
- val sliderValueFlow = remember { MutableStateFlow(-1) }
- LaunchedEffect(sliderValueFlow) {
- sliderValueFlow.drop(1).debounce(200).collect {
- moveToPageIndex(it)
- }
- }
- ReaderAppBars(
- visible = menuVisible,
- isRtl = isRtl,
- showSeekBar = showSeekbar,
- currentPage = currentPage,
- totalPages = totalPage,
- onSliderValueChange = {
- isScrollingThroughPages = true
- sliderValueFlow.value = it
- currentPage = it + 1
- },
- onClickSettings = {
- lifecycleScope.launch {
- dialogState.dialog { cont ->
- fun dispose() = cont.resume(Unit)
- var isColorFilter by remember { mutableStateOf(false) }
- val scrim by animateColorAsState(
- targetValue = if (isColorFilter) Color.Transparent else BottomSheetDefaults.ScrimColor,
- label = "ScrimColor",
- )
- ModalBottomSheet(
- onDismissRequest = { dispose() },
- modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Top)),
- dragHandle = null,
- // Yeah, I know color state should not be read here, but we have to do it...
- scrimColor = scrim,
- contentWindowInsets = { WindowInsets(0) },
- ) {
- SettingsPager(modifier = Modifier.fillMaxSize()) { page ->
- isColorFilter = page == 2
- setMenuVisibility(!isColorFilter)
- }
- }
- }
- }
- },
- )
-
- ReaderContentOverlay(
- brightness = { brightnessValue }.takeIf { brightness && brightnessValue < 0 },
- color = { colorOverlay }.takeIf { colorOverlayEnabled },
- colorBlendMode = colorOverlayMode,
- )
- }
- setContentView(binding.root)
- lifecycleScope.launchIO {
- buildProvider()?.let {
- mGalleryProvider = it
- it.start()
- if (it.awaitReady()) {
- withUIContext { setGallery() }
- } else {
- makeToast(R.string.error_reading_failed)
- finish()
- }
- }
- }
-
- config = ReaderConfig()
- initializeMenu()
- }
-
- fun retryPage(index: Int, orgImg: Boolean = false) {
- mGalleryProvider?.retryPage(index, orgImg)
- }
-
- fun restartGalleryProvider() {
- mGalleryProvider?.let {
- it.restart()
- viewer?.refreshAdapter()
- }
- }
-
- private fun setGallery() {
- if (mGalleryProvider?.isReady != true) return
-
- totalPage = mGalleryProvider!!.size
- viewer?.destroy()
- viewer = ReadingModeType.toViewer(ReaderPreferences.defaultReadingMode().get(), this)
- isRtl = viewer!!.isRtl
- updateViewerInsets(ReaderPreferences.cutoutShort().get())
- binding.viewerContainer.removeAllViews()
- setOrientation(ReaderPreferences.defaultOrientationType().get())
- binding.viewerContainer.addView(viewer?.getView())
- viewer?.setGalleryProvider(mGalleryProvider!!)
- moveToPageIndex(mCurrentIndex)
- }
-
- override fun onDestroy() {
- super.onDestroy()
- config = null
- viewer?.destroy()
- viewer = null
- }
-
- private suspend fun makeToast(text: String) = withUIContext {
- Toast.makeText(this@ReaderActivity, text, Toast.LENGTH_SHORT).show()
- }
-
- private suspend fun makeToast(@StringRes resId: Int) = makeToast(getString(resId))
-
- private fun provideImage(index: Int): Uri? = AppConfig.externalTempDir?.let { dir ->
- mGalleryProvider?.run {
- getImageFilename(index)?.let { filename ->
- val file = File(dir, filename).takeIf { save(index, it.toOkioPath()) } ?: return null
- FileProvider.getUriForFile(implicit(), BuildConfig.APPLICATION_ID + ".fileprovider", file)
- }
- }
- }
-
- fun shareImage(index: Int) {
- lifecycleScope.launchIO {
- val uri = provideImage(index) ?: return@launchIO makeToast(R.string.error_cant_save_image)
- val intent = Intent()
- intent.action = Intent.ACTION_SEND
- intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
- intent.putExtra(Intent.EXTRA_STREAM, uri)
- mGalleryInfo?.run {
- intent.putExtra(Intent.EXTRA_TEXT, EhUrl.getGalleryDetailUrl(gid, token))
- }
-
- val extension = FileUtils.getExtensionFromFilename(uri.path)
- val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) ?: "image/jpeg"
- intent.setDataAndType(uri, mimeType)
-
- try {
- startActivity(Intent.createChooser(intent, getString(R.string.share_image)))
- } catch (e: Throwable) {
- makeToast(R.string.error_cant_find_activity)
- }
- }
- }
-
- fun copyImage(index: Int) {
- lifecycleScope.launchIO {
- val uri = provideImage(index) ?: return@launchIO makeToast(R.string.error_cant_save_image)
- val clipData = ClipData.newUri(contentResolver, "ehviewer", uri)
- clipboardManager.setPrimaryClip(clipData)
- makeToast(R.string.copied_to_clipboard)
- }
- }
-
- fun saveImage(index: Int) {
- mGalleryProvider ?: return
-
- lifecycleScope.launchIO {
- val granted = isAtLeastQ || requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
- if (granted) {
- val filename = mGalleryProvider!!.getImageFilename(index) ?: return@launchIO makeToast(R.string.error_cant_save_image)
- val extension = FileUtils.getExtensionFromFilename(filename)
- val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) ?: "image/jpeg"
-
- val realPath: String
- val resolver = contentResolver
- val values = ContentValues()
- values.put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
- values.put(MediaStore.MediaColumns.DATE_ADDED, Clock.System.now().epochSeconds)
- values.put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
- if (isAtLeastQ) {
- realPath = Environment.DIRECTORY_PICTURES + File.separator + AppConfig.APP_DIRNAME
- values.put(MediaStore.MediaColumns.RELATIVE_PATH, realPath)
- values.put(MediaStore.MediaColumns.IS_PENDING, 1)
- } else {
- val dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
- val path = File(dir, AppConfig.APP_DIRNAME)
- realPath = path.toString()
- if (!FileUtils.ensureDirectory(path)) {
- return@launchIO makeToast(R.string.error_cant_save_image)
- }
- values.put(MediaStore.MediaColumns.DATA, realPath + File.separator + filename)
- }
- val imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
- ?: return@launchIO makeToast(R.string.error_cant_save_image)
- if (!mGalleryProvider!!.save(index, imageUri.toOkioPath())) {
- try {
- resolver.delete(imageUri, null, null)
- } catch (e: Exception) {
- logcat(e)
- }
- return@launchIO makeToast(R.string.error_cant_save_image)
- } else if (isAtLeastQ) {
- val contentValues = ContentValues()
- contentValues.put(MediaStore.MediaColumns.IS_PENDING, 0)
- resolver.update(imageUri, contentValues, null, null)
- }
-
- makeToast(getString(R.string.image_saved, realPath + File.separator + filename))
- } else {
- makeToast(R.string.permission_denied)
- }
- }
- }
-
- fun saveImageTo(index: Int) {
- lifecycleScope.launchIO {
- val filename = mGalleryProvider?.getImageFilename(index) ?: return@launchIO makeToast(R.string.error_cant_save_image)
- val extension = FileUtils.getExtensionFromFilename(filename)
- val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) ?: "image/jpeg"
- runSuspendCatching {
- awaitActivityResult(CreateDocument(mimeType), filename)?.let {
- mGalleryProvider!!.save(index, it.toOkioPath())
- makeToast(getString(R.string.image_saved, it.displayPath))
- }
- }.onFailure {
- logcat(it)
- makeToast(R.string.error_cant_find_activity)
- }
- }
- }
-
- override fun onProvideAssistContent(outContent: AssistContent) {
- super.onProvideAssistContent(outContent)
-
- val url = galleryDetailUrl
- if (url != null) {
- outContent.webUri = Uri.parse(url)
- }
- }
-
- companion object {
- const val ACTION_EH = "eh"
- const val KEY_ACTION = "action"
- const val KEY_URI = "uri"
- const val KEY_GALLERY_INFO = "gallery_info"
- const val KEY_PAGE = "page"
- }
-
- // Tachiyomi funcs
-
- var isScrollingThroughPages = false
- private set
-
- /**
- * Viewer used to display the pages (pager, webtoon, ...).
- */
- var viewer: BaseViewer? = null
- private set
-
- private var config: ReaderConfig? = null
-
- private val windowInsetsController by lazy {
- WindowInsetsControllerCompat(
- window,
- binding.root,
- )
- }
-
- /**
- * Sets the visibility of the menu according to [visible].
- */
- private fun setMenuVisibility(visible: Boolean) {
- menuVisible = visible
- if (visible) {
- windowInsetsController.show(WindowInsetsCompat.Type.systemBars())
-
- if (ReaderPreferences.showPageNumber().get()) {
- config?.setPageNumberVisibility(false)
- }
- } else {
- if (ReaderPreferences.fullscreen().get()) {
- windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
- windowInsetsController.systemBarsBehavior =
- WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
- }
-
- if (ReaderPreferences.showPageNumber().get()) {
- config?.setPageNumberVisibility(true)
- }
- }
- }
-
- private var currentPage by mutableIntStateOf(-1)
- private var totalPage by mutableIntStateOf(-1)
- private var isRtl by mutableStateOf(false)
-
- /**
- * Initializes the reader menu. It sets up click listeners and the initial visibility.
- */
- private fun initializeMenu() {
- binding.pageNumber.setMD3Content {
- CompositionLocalProvider(
- LocalTextStyle provides MaterialTheme.typography.bodySmall,
- ) {
- PageIndicatorText(
- currentPage = currentPage,
- totalPages = totalPage,
- modifier = Modifier.navigationBarsPadding(),
- )
- }
- }
-
- // Set initial visibility
- setMenuVisibility(menuVisible)
- }
-
- /**
- * Dispatches a key event. If the viewer doesn't handle it, call the default implementation.
- */
- override fun dispatchKeyEvent(event: KeyEvent): Boolean {
- val handled = viewer?.handleKeyEvent(event) ?: false
- return handled || super.dispatchKeyEvent(event)
- }
-
- /**
- * Dispatches a generic motion event. If the viewer doesn't handle it, call the default
- * implementation.
- */
- override fun dispatchGenericMotionEvent(event: MotionEvent): Boolean {
- val handled = viewer?.handleGenericMotionEvent(event) ?: false
- return handled || super.dispatchGenericMotionEvent(event)
- }
-
- /**
- * Moves the viewer to the given page [index]. It does nothing if the viewer is null or the
- * page is not found.
- */
- private fun moveToPageIndex(index: Int) {
- val viewer = viewer ?: return
- val page = mGalleryProvider?.pages?.getOrNull(index) ?: return
- viewer.moveToPage(page)
- }
-
- /**
- * Called from the viewer whenever a [page] is marked as active. It updates the values of the
- * bottom menu and delegates the change to the presenter.
- */
- fun onPageSelected(page: ReaderPage) {
- // Set bottom page number
- currentPage = page.number
-
- mCurrentIndex = page.index
- }
-
- /**
- * Called from the viewer whenever a [page] is long clicked. A bottom sheet with a list of
- * actions to perform is shown.
- */
- fun onPageLongTap(page: ReaderPage) {
- lifecycleScope.launch {
- dialogState.dialog {
- ModalBottomSheet(
- onDismissRequest = { it.cancel() },
- modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Top)),
- contentWindowInsets = { WindowInsets(0) },
- ) {
- ReaderPageSheet(page) { it.cancel() }
- }
- }
- }
- }
-
- /**
- * Called from the viewer to toggle the visibility of the menu. It's implemented on the
- * viewer because each one implements its own touch and key events.
- */
- fun toggleMenu() {
- setMenuVisibility(!menuVisible)
- }
-
- /**
- * Called from the viewer to hide the menu.
- */
- fun hideMenu() {
- if (menuVisible) {
- setMenuVisibility(false)
- }
- }
-
- /**
- * Updates viewer insets depending on fullscreen reader preferences.
- */
- fun updateViewerInsets(drawInCutout: Boolean) {
- viewer?.getViewForInsets()?.applyInsetter {
- type(navigationBars = true, statusBars = true, displayCutout = !drawInCutout) {
- padding()
- }
- }
- }
-
- /**
- * Class that handles the user preferences of the reader.
- */
- private inner class ReaderConfig {
-
- private fun getCombinedPaint(grayscale: Boolean, invertedColors: Boolean): Paint = Paint().apply {
- colorFilter = ColorMatrixColorFilter(
- ColorMatrix().apply {
- if (grayscale) {
- setSaturation(0f)
- }
- if (invertedColors) {
- postConcat(
- ColorMatrix(
- floatArrayOf(
- -1f, 0f, 0f, 0f, 255f,
- 0f, -1f, 0f, 0f, 255f,
- 0f, 0f, -1f, 0f, 255f,
- 0f, 0f, 0f, 1f, 0f,
- ),
- ),
- )
- }
- },
- )
- }
-
- /**
- * Initializes the reader subscriptions.
- */
- init {
- ReaderPreferences.defaultReadingMode().changes()
- .drop(1)
- .onEach { setGallery() }
- .launchIn(lifecycleScope)
-
- ReaderPreferences.defaultOrientationType().changes()
- .drop(1)
- .onEach { setGallery() }
- .launchIn(lifecycleScope)
-
- ReaderPreferences.cropBorders().changes()
- .onEach { restartGalleryProvider() }
- .launchIn(lifecycleScope)
-
- ReaderPreferences.readerTheme().changes()
- .onEach { theme ->
- binding.readerContainer.setBackgroundResource(
- when (theme) {
- 0 -> android.R.color.white
- 2 -> R.color.reader_background_dark
- 3 -> automaticBackgroundColor()
- else -> android.R.color.black
- },
- )
- }
- .launchIn(lifecycleScope)
-
- ReaderPreferences.showPageNumber().changes()
- .onEach { setPageNumberVisibility(it) }
- .launchIn(lifecycleScope)
-
- ReaderPreferences.cutoutShort().changes()
- .onEach { updateViewerInsets(it) }
- .launchIn(lifecycleScope)
-
- ReaderPreferences.keepScreenOn().changes()
- .onEach { window.updateKeepScreenOn(it) }
- .launchIn(lifecycleScope)
-
- ReaderPreferences.customBrightness().changes()
- .onEach { setCustomBrightness(it) }
- .launchIn(lifecycleScope)
-
- merge(
- ReaderPreferences.grayscale().changes(),
- ReaderPreferences.invertedColors().changes(),
- )
- .onEach {
- setLayerPaint(
- ReaderPreferences.grayscale().get(),
- ReaderPreferences.invertedColors().get(),
- )
- }
- .launchIn(lifecycleScope)
- }
-
- /**
- * Picks background color for [ReaderActivity] based on light/dark theme preference
- */
- private fun automaticBackgroundColor(): Int = if (baseContext.isNightMode()) {
- R.color.reader_background_dark
- } else {
- android.R.color.white
- }
-
- /**
- * Sets the visibility of the bottom page indicator according to [visible].
- */
- fun setPageNumberVisibility(visible: Boolean) {
- binding.pageNumber.isVisible = visible
- }
-
- /**
- * Sets the custom brightness overlay according to [enabled].
- */
- @OptIn(FlowPreview::class)
- private fun setCustomBrightness(enabled: Boolean) {
- if (enabled) {
- ReaderPreferences.customBrightnessValue().changes()
- .sample(100)
- .onEach { setCustomBrightnessValue(it) }
- .launchIn(lifecycleScope)
- } else {
- setCustomBrightnessValue(0)
- }
- }
-
- private fun setLayerPaint(grayscale: Boolean, invertedColors: Boolean) {
- val paint = if (grayscale || invertedColors) {
- getCombinedPaint(
- grayscale,
- invertedColors,
- )
- } else {
- null
- }
- binding.viewerContainer.setLayerType(LAYER_TYPE_HARDWARE, paint)
- }
- }
-}
diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ReaderNavigationOverlayView.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ReaderNavigationOverlayView.kt
deleted file mode 100644
index c7a283453..000000000
--- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ReaderNavigationOverlayView.kt
+++ /dev/null
@@ -1,112 +0,0 @@
-package eu.kanade.tachiyomi.ui.reader
-
-import android.content.Context
-import android.graphics.Canvas
-import android.graphics.Color
-import android.graphics.Paint
-import android.util.AttributeSet
-import android.view.MotionEvent
-import android.view.View
-import android.view.ViewPropertyAnimator
-import androidx.core.graphics.withScale
-import androidx.core.graphics.withTranslation
-import androidx.core.view.isVisible
-import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
-import eu.kanade.tachiyomi.ui.reader.viewer.navigation.DisabledNavigation
-import kotlin.math.abs
-
-class ReaderNavigationOverlayView(context: Context, attributeSet: AttributeSet) : View(context, attributeSet) {
-
- private var viewPropertyAnimator: ViewPropertyAnimator? = null
-
- private var navigation: ViewerNavigation? = null
-
- fun setNavigation(navigation: ViewerNavigation, showOnStart: Boolean) {
- val firstLaunch = this.navigation == null
- this.navigation = navigation
- invalidate()
-
- if (isVisible || (!showOnStart && firstLaunch) || navigation is DisabledNavigation) {
- return
- }
-
- viewPropertyAnimator = animate()
- .alpha(1f)
- .setDuration(FADE_DURATION)
- .withStartAction {
- isVisible = true
- }
- .withEndAction {
- viewPropertyAnimator = null
- }
- viewPropertyAnimator?.start()
- }
-
- private val regionPaint = Paint()
-
- private val textPaint = Paint().apply {
- textAlign = Paint.Align.CENTER
- color = Color.WHITE
- textSize = 64f
- }
-
- private val textBorderPaint = Paint().apply {
- textAlign = Paint.Align.CENTER
- color = Color.BLACK
- textSize = 64f
- style = Paint.Style.STROKE
- strokeWidth = 8f
- }
-
- override fun onDraw(canvas: Canvas) {
- if (navigation == null) return
-
- navigation?.regions?.forEach { region ->
- val rect = region.rectF
-
- // Scale rect from 1f,1f to screen width and height
- canvas.withScale(width.toFloat(), height.toFloat()) {
- regionPaint.color = context.getColor(region.type.colorRes)
- drawRect(rect, regionPaint)
- }
-
- // Don't want scale anymore because it messes with drawText
- // Translate origin to rect start (left, top)
- canvas.withTranslation(x = (width * rect.left), y = (height * rect.top)) {
- // Calculate center of rect width on screen
- val x = width * (abs(rect.left - rect.right) / 2)
-
- // Calculate center of rect height on screen
- val y = height * (abs(rect.top - rect.bottom) / 2)
-
- drawText(context.getString(region.type.nameRes), x, y, textBorderPaint)
- drawText(context.getString(region.type.nameRes), x, y, textPaint)
- }
- }
- }
-
- override fun performClick(): Boolean {
- super.performClick()
-
- if (viewPropertyAnimator == null && isVisible) {
- viewPropertyAnimator = animate()
- .alpha(0f)
- .setDuration(FADE_DURATION)
- .withEndAction {
- isVisible = false
- viewPropertyAnimator = null
- }
- viewPropertyAnimator?.start()
- }
-
- return true
- }
-
- override fun onTouchEvent(event: MotionEvent?): Boolean {
- // Hide overlay if user start tapping or swiping
- performClick()
- return super.onTouchEvent(event)
- }
-}
-
-private const val FADE_DURATION = 1000L
diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ReaderPageSheet.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ReaderPageSheet.kt
index d612c2ef9..c70a8e8b3 100644
--- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ReaderPageSheet.kt
+++ b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ReaderPageSheet.kt
@@ -21,16 +21,12 @@ import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.hippo.ehviewer.R
-import com.hippo.ehviewer.util.findActivity
-import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import moe.tarsin.kt.andThen
@Composable
@@ -64,17 +60,3 @@ fun ReaderPageSheetMeta(
Item(icon = Icons.Default.Save, text = R.string.action_save_to, onClick = saveTo)
}
}
-
-@Composable
-fun ReaderPageSheet(page: ReaderPage, dismiss: () -> Unit) {
- val activity = LocalContext.current.run { remember { findActivity() } }
- ReaderPageSheetMeta(
- retry = { activity.retryPage(page.index) },
- retryOrigin = { activity.retryPage(page.index, true) },
- share = { activity.shareImage(page.index) },
- copy = { activity.copyImage(page.index) },
- save = { activity.saveImage(page.index) },
- saveTo = { activity.saveImageTo(page.index) },
- dismiss = dismiss,
- )
-}
diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/setting/ReaderPreferences.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/setting/ReaderPreferences.kt
deleted file mode 100644
index ce51a19b9..000000000
--- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/setting/ReaderPreferences.kt
+++ /dev/null
@@ -1,116 +0,0 @@
-package eu.kanade.tachiyomi.ui.reader.setting
-
-import eu.kanade.tachiyomi.core.preference.AndroidPreferenceStore
-import eu.kanade.tachiyomi.core.preference.getEnum
-import eu.kanade.tachiyomi.data.preference.PreferenceValues
-
-object ReaderPreferences {
- private val preferenceStore = AndroidPreferenceStore
-
- // region General
-
- fun pageTransitions() = preferenceStore.getBoolean("pref_enable_transitions_key", true)
-
- fun doubleTapAnimSpeed() = preferenceStore.getInt("pref_double_tap_anim_speed", 500)
-
- fun showPageNumber() = preferenceStore.getBoolean("pref_show_page_number_key", true)
-
- fun showReaderSeekbar() = preferenceStore.getBoolean("pref_show_reader_seekbar", true)
- fun doubleTapToZoom() = preferenceStore.getBoolean("pref_double_tap_to_zoom", true)
-
- fun trueColor() = preferenceStore.getBoolean("pref_true_color_key", false)
-
- fun fullscreen() = preferenceStore.getBoolean("fullscreen", true)
-
- fun cutoutShort() = preferenceStore.getBoolean("cutout_short", true)
-
- fun keepScreenOn() = preferenceStore.getBoolean("pref_keep_screen_on_key", true)
-
- fun defaultReadingMode() = preferenceStore.getInt("pref_default_reading_mode_key", ReadingModeType.LEFT_TO_RIGHT.flagValue)
-
- fun defaultOrientationType() = preferenceStore.getInt("pref_default_orientation_type_key", OrientationType.FREE.flagValue)
-
- fun imageScaleType() = preferenceStore.getInt("pref_image_scale_type_key", 1)
-
- fun zoomStart() = preferenceStore.getInt("pref_zoom_start_key", 1)
-
- fun readerTheme() = preferenceStore.getInt("pref_reader_theme_key", 1)
-
- fun alwaysShowChapterTransition() = preferenceStore.getBoolean("always_show_chapter_transition", true)
-
- fun cropBorders() = preferenceStore.getBoolean("crop_borders", false)
-
- fun navigateToPan() = preferenceStore.getBoolean("navigate_pan", true)
-
- fun landscapeZoom() = preferenceStore.getBoolean("landscape_zoom", false)
-
- fun cropBordersWebtoon() = preferenceStore.getBoolean("crop_borders_webtoon", false)
-
- fun webtoonSidePadding() = preferenceStore.getInt("webtoon_side_padding", 0)
-
- fun readerHideThreshold() = preferenceStore.getEnum("reader_hide_threshold", PreferenceValues.ReaderHideThreshold.LOW)
-
- fun folderPerManga() = preferenceStore.getBoolean("create_folder_per_manga", false)
-
- fun skipRead() = preferenceStore.getBoolean("skip_read", false)
-
- fun skipFiltered() = preferenceStore.getBoolean("skip_filtered", true)
-
- // endregion
-
- // region Split two page spread
-
- fun dualPageSplitPaged() = preferenceStore.getBoolean("pref_dual_page_split", false)
-
- fun dualPageInvertPaged() = preferenceStore.getBoolean("pref_dual_page_invert", false)
-
- fun dualPageSplitWebtoon() = preferenceStore.getBoolean("pref_dual_page_split_webtoon", false)
-
- fun dualPageInvertWebtoon() = preferenceStore.getBoolean("pref_dual_page_invert_webtoon", false)
-
- // endregion
-
- // region Color filter
-
- fun customBrightness() = preferenceStore.getBoolean("pref_custom_brightness_key", false)
-
- fun customBrightnessValue() = preferenceStore.getInt("custom_brightness_value", 0)
-
- fun colorFilter() = preferenceStore.getBoolean("pref_color_filter_key", false)
-
- fun colorFilterValue() = preferenceStore.getInt("color_filter_value", 0)
-
- fun colorFilterMode() = preferenceStore.getInt("color_filter_mode", 0)
-
- fun grayscale() = preferenceStore.getBoolean("pref_grayscale", false)
-
- fun invertedColors() = preferenceStore.getBoolean("pref_inverted_colors", false)
-
- // endregion
-
- // region Controls
-
- fun readWithLongTap() = preferenceStore.getBoolean("reader_long_tap", true)
-
- fun readWithVolumeKeys() = preferenceStore.getBoolean("reader_volume_keys", false)
-
- fun readWithVolumeKeysInterval() = preferenceStore.getInt("reader_volume_keys_interval", 0)
-
- fun readWithVolumeKeysInverted() = preferenceStore.getBoolean("reader_volume_keys_inverted", false)
-
- fun navigationModePager() = preferenceStore.getInt("reader_navigation_mode_pager", 0)
-
- fun navigationModeWebtoon() = preferenceStore.getInt("reader_navigation_mode_webtoon", 0)
-
- fun pagerNavInverted() = preferenceStore.getInt("reader_tapping_inverted_2", 0)
-
- fun webtoonNavInverted() = preferenceStore.getInt("reader_tapping_inverted_webtoon_2", 0)
-
- fun showNavigationOverlayNewUser() = preferenceStore.getBoolean("reader_navigation_overlay_new_user", true)
-
- fun showNavigationOverlayOnStart() = preferenceStore.getBoolean("reader_navigation_overlay_on_start", false)
-
- fun wideColorGamut() = preferenceStore.getBoolean("wide_color_gamut", false)
-
- // endregion
-}
diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/setting/ReadingModeType.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/setting/ReadingModeType.kt
index 39d931d8f..0895afeac 100644
--- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/setting/ReadingModeType.kt
+++ b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/setting/ReadingModeType.kt
@@ -10,10 +10,6 @@ import com.hippo.ehviewer.icons.reader.LeftToRight
import com.hippo.ehviewer.icons.reader.RightToLeft
import com.hippo.ehviewer.icons.reader.Vertical
import com.hippo.ehviewer.icons.reader.Webtoon
-import eu.kanade.tachiyomi.ui.reader.ReaderActivity
-import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
-import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerViewer
-import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer
@Stable
enum class ReadingModeType(
@@ -52,14 +48,5 @@ enum class ReadingModeType(
}
fun fromSpinner(position: Int?) = entries.find { value -> value.prefValue == position } ?: DEFAULT
-
- fun toViewer(preference: Int?, activity: ReaderActivity): BaseViewer = when (fromPreference(preference)) {
- LEFT_TO_RIGHT -> PagerViewer(activity)
- RIGHT_TO_LEFT -> PagerViewer(activity, true)
- VERTICAL -> PagerViewer(activity, isVertical = true)
- WEBTOON -> WebtoonViewer(activity)
- CONTINUOUS_VERTICAL -> WebtoonViewer(activity, isContinuous = false)
- DEFAULT -> PagerViewer(activity)
- }
}
}
diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/setting/TappingInvertMode.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/setting/TappingInvertMode.kt
new file mode 100644
index 000000000..e6314b3d4
--- /dev/null
+++ b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/setting/TappingInvertMode.kt
@@ -0,0 +1,11 @@
+package eu.kanade.tachiyomi.ui.reader.setting
+
+enum class TappingInvertMode(
+ val shouldInvertHorizontal: Boolean = false,
+ val shouldInvertVertical: Boolean = false,
+) {
+ NONE,
+ HORIZONTAL(shouldInvertHorizontal = true),
+ VERTICAL(shouldInvertVertical = true),
+ BOTH(shouldInvertHorizontal = true, shouldInvertVertical = true),
+}
diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/BaseViewer.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/BaseViewer.kt
deleted file mode 100644
index d43ff3a6c..000000000
--- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/BaseViewer.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-package eu.kanade.tachiyomi.ui.reader.viewer
-
-import android.view.KeyEvent
-import android.view.MotionEvent
-import android.view.View
-import eu.kanade.tachiyomi.ui.reader.ReaderActivity
-import eu.kanade.tachiyomi.ui.reader.loader.PageLoader
-import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
-
-/**
- * Interface for implementing a viewer.
- */
-interface BaseViewer {
- val activity: ReaderActivity
-
- val readerConfig: ReaderPageImageView.Config
-
- val isRtl: Boolean
-
- fun getViewForInsets() = getView()
-
- /**
- * Returns the view this viewer uses.
- */
- fun getView(): View
-
- /**
- * Destroys this viewer. Called when leaving the reader or swapping viewers.
- */
- fun destroy()
-
- /**
- * Tells this viewer to set the given [provider] as active.
- */
- fun setGalleryProvider(provider: PageLoader)
-
- /**
- * Tells this viewer to move to the given [page].
- */
- fun moveToPage(page: ReaderPage)
-
- /**
- * Called from the containing activity when a key [event] is received. It should return true
- * if the event was handled, false otherwise.
- */
- fun handleKeyEvent(event: KeyEvent): Boolean
-
- /**
- * Called from the containing activity when a generic motion [event] is received. It should
- * return true if the event was handled, false otherwise.
- */
- fun handleGenericMotionEvent(event: MotionEvent): Boolean
-
- fun refreshAdapter()
-}
diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/GestureDetectorWithLongTap.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/GestureDetectorWithLongTap.kt
deleted file mode 100644
index a85f8dc7d..000000000
--- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/GestureDetectorWithLongTap.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-package eu.kanade.tachiyomi.ui.reader.viewer
-
-import android.content.Context
-import android.os.Handler
-import android.os.Looper
-import android.view.GestureDetector
-import android.view.MotionEvent
-import android.view.ViewConfiguration
-import kotlin.math.abs
-
-/**
- * A custom gesture detector that also implements an on long tap confirmed, because the built-in
- * one conflicts with the quick scale feature.
- */
-open class GestureDetectorWithLongTap(
- context: Context,
- listener: Listener,
-) : GestureDetector(context, listener) {
-
- private val handler = Handler(Looper.getMainLooper())
- private val slop = ViewConfiguration.get(context).scaledTouchSlop
- private val longTapTime = ViewConfiguration.getLongPressTimeout().toLong()
- private val doubleTapTime = ViewConfiguration.getDoubleTapTimeout().toLong()
-
- private var downX = 0f
- private var downY = 0f
- private var lastUp = 0L
- private var lastDownEvent: MotionEvent? = null
-
- /**
- * Runnable to execute when a long tap is confirmed.
- */
- private val longTapFn = Runnable { listener.onLongTapConfirmed(lastDownEvent!!) }
-
- override fun onTouchEvent(ev: MotionEvent): Boolean {
- when (ev.actionMasked) {
- MotionEvent.ACTION_DOWN -> {
- lastDownEvent?.recycle()
- lastDownEvent = MotionEvent.obtain(ev)
-
- // This is the key difference with the built-in detector. We have to ignore the
- // event if the last up and current down are too close in time (double tap).
- if (ev.downTime - lastUp > doubleTapTime) {
- downX = ev.rawX
- downY = ev.rawY
- handler.postDelayed(longTapFn, longTapTime)
- }
- }
- MotionEvent.ACTION_MOVE -> {
- if (abs(ev.rawX - downX) > slop || abs(ev.rawY - downY) > slop) {
- handler.removeCallbacks(longTapFn)
- }
- }
- MotionEvent.ACTION_UP -> {
- lastUp = ev.eventTime
- handler.removeCallbacks(longTapFn)
- }
- MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_POINTER_DOWN -> {
- handler.removeCallbacks(longTapFn)
- }
- }
- return super.onTouchEvent(ev)
- }
-
- /**
- * Custom listener to also include a long tap confirmed
- */
- open class Listener : SimpleOnGestureListener() {
- /**
- * Notified when a long tap occurs with the initial on down [ev] that triggered it.
- */
- open fun onLongTapConfirmed(ev: MotionEvent) {
- }
- }
-}
diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ReaderErrorLayout.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ReaderErrorLayout.kt
deleted file mode 100644
index 0e79a423a..000000000
--- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ReaderErrorLayout.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-package eu.kanade.tachiyomi.ui.reader.viewer
-
-import android.content.Context
-import android.util.AttributeSet
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.aspectRatio
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.Button
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.AbstractComposeView
-import androidx.compose.ui.platform.ViewCompositionStrategy
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.dp
-import com.hippo.ehviewer.R
-import com.hippo.ehviewer.ui.theme.EhTheme
-
-class ReaderErrorLayout @JvmOverloads constructor(
- context: Context,
- private val errorMsg: String? = null,
- attrs: AttributeSet? = null,
- defStyleAttr: Int = 0,
- private val onRetry: () -> Unit = {},
-) : AbstractComposeView(context, attrs, defStyleAttr) {
- init {
- setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnDetachedFromWindow)
- }
-
- @Composable
- override fun Content() = EhTheme {
- val defaultError = stringResource(id = R.string.decode_image_error)
- Box(modifier = Modifier.fillMaxWidth().aspectRatio(DEFAULT_ASPECT)) {
- Column(
- modifier = Modifier.align(Alignment.Center),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- Text(
- text = errorMsg ?: defaultError,
- modifier = Modifier.padding(8.dp),
- color = MaterialTheme.colorScheme.onSurface,
- textAlign = TextAlign.Center,
- style = MaterialTheme.typography.bodyMedium,
- )
- Button(onClick = onRetry, modifier = Modifier.padding(8.dp)) {
- Text(text = stringResource(id = R.string.action_retry))
- }
- }
- }
- }
-}
-
-private const val DEFAULT_ASPECT = 1 / 1.4125f
diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageAdapter.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageAdapter.kt
deleted file mode 100644
index 04bb1e6e6..000000000
--- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageAdapter.kt
+++ /dev/null
@@ -1,73 +0,0 @@
-package eu.kanade.tachiyomi.ui.reader.viewer
-
-import android.view.ViewGroup
-import androidx.recyclerview.widget.RecyclerView
-import eu.kanade.tachiyomi.ui.reader.loader.PageLoader
-import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
-import eu.kanade.tachiyomi.util.system.createReaderThemeContext
-
-/**
- * RecyclerView Adapter used by this [viewer] to where [PageLoader] updates are posted.
- */
-class ReaderPageAdapter(
- val viewer: BaseViewer,
- private val isWebtoon: Boolean = false,
-) : RecyclerView.Adapter() {
-
- /**
- * List of currently set items.
- */
- var items: List = emptyList()
- private set
-
- private var currentChapter: PageLoader? = null
-
- /**
- * Context that has been wrapped to use the correct theme values based on the
- * current app theme and reader background color
- */
- private var readerThemedContext = viewer.activity.createReaderThemeContext()
-
- /**
- * Updates this adapter with the given [chapter]. It handles setting a few pages of the
- * next/previous chapter to allow seamless transitions.
- */
- fun setChapters(chapter: PageLoader) {
- currentChapter = chapter
- items = chapter.pages
- }
-
- fun refresh() {
- readerThemedContext = viewer.activity.createReaderThemeContext()
- }
-
- /**
- * Returns the amount of items of the adapter.
- */
- override fun getItemCount(): Int = items.size
-
- /**
- * Creates a new view holder for an item with the given [viewType].
- */
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ReaderPageHolder {
- val view = ReaderPageImageView(readerThemedContext, isWebtoon = isWebtoon)
- return ReaderPageHolder(view, viewer)
- }
-
- /**
- * Binds an existing view [holder] with the item at the given [position].
- */
- override fun onBindViewHolder(holder: ReaderPageHolder, position: Int) {
- val item = items[position]
- currentChapter!!.request(position)
- holder.bind(item)
- }
-
- /**
- * Recycles an existing view [holder] before adding it to the view pool.
- */
- override fun onViewRecycled(holder: ReaderPageHolder) {
- currentChapter?.cancelRequest(holder.bindingAdapterPosition)
- holder.recycle()
- }
-}
diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageHolder.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageHolder.kt
deleted file mode 100644
index 5a78774dc..000000000
--- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageHolder.kt
+++ /dev/null
@@ -1,208 +0,0 @@
-package eu.kanade.tachiyomi.ui.reader.viewer
-
-import android.content.Context
-import android.content.res.Resources
-import android.view.ViewGroup.LayoutParams.MATCH_PARENT
-import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
-import android.widget.FrameLayout
-import androidx.recyclerview.widget.RecyclerView
-import com.hippo.ehviewer.image.Image
-import eu.kanade.tachiyomi.source.model.Page
-import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
-import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer
-import eu.kanade.tachiyomi.util.system.dpToPx
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.launch
-
-/**
- * Holder of the reader for a single page of a chapter.
- *
- * @param frame the root view for this holder.
- * @param viewer the page viewer.
- * @constructor creates a new page holder.
- */
-class ReaderPageHolder(
- private val frame: ReaderPageImageView,
- private val viewer: BaseViewer,
-) : RecyclerView.ViewHolder(frame) {
-
- /**
- * Context getter because it's used often.
- */
- val context: Context get() = frame.context
-
- /**
- * Loading progress bar to indicate the current progress.
- * Needed to keep a minimum height size of the holder, otherwise the adapter would create more
- * views to fill the screen, which is not wanted.
- */
- private val progressIndicator = ReaderProgressIndicator(context)
-
- /**
- * Error layout to show when the image fails to load.
- */
- private var errorLayout: ReaderErrorLayout? = null
-
- /**
- * Page of a chapter.
- */
- private lateinit var page: ReaderPage
-
- private val scope = CoroutineScope(Dispatchers.IO)
-
- /**
- * Subscription for status changes of the page.
- */
- private var statusJob: Job? = null
-
- init {
- if (viewer is WebtoonViewer) {
- val defaultHeight = (viewer.recycler.width * 1.4125).toInt()
- frame.addView(progressIndicator, MATCH_PARENT, defaultHeight)
- refreshLayoutParams()
- } else {
- frame.addView(progressIndicator)
- frame.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
- }
-
- frame.onImageLoaded = ::onImageLoaded
- frame.onScaleChanged = { viewer.activity.hideMenu() }
- }
-
- /**
- * Binds the given [page] with this view holder, subscribing to its state.
- */
- fun bind(page: ReaderPage) {
- this.page = page
- statusJob?.cancel()
- statusJob = scope.launch(Dispatchers.Main) {
- page.status.collectLatest {
- processStatus(it)
- }
- }
- refreshLayoutParams()
- }
-
- private fun refreshLayoutParams() {
- if (viewer is WebtoonViewer) {
- frame.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT).apply {
- if (!viewer.isContinuous) {
- bottomMargin = 15.dpToPx
- }
-
- val margin = Resources.getSystem().displayMetrics.widthPixels * (viewer.config.sidePadding / 100f)
- marginEnd = margin.toInt()
- marginStart = margin.toInt()
- }
- }
- }
-
- fun onPageSelected(forward: Boolean) = frame.onPageSelected(forward)
- fun panEnd(reverse: Boolean) = if (reverse) frame.panLeft() else frame.panRight()
-
- /**
- * Called when the view is recycled and added to the view pool.
- */
- fun recycle() {
- statusJob?.cancel()
- statusJob = null
-
- removeErrorLayout()
- frame.recycle()
- progressIndicator.reset()
- }
-
- /**
- * Called when the status of the page changes.
- *
- * @param status the new status of the page.
- */
- private suspend fun processStatus(status: Page.State) {
- when (status) {
- Page.State.QUEUE -> setQueued()
- Page.State.LOAD_PAGE -> setLoading()
- Page.State.DOWNLOAD_IMAGE -> {
- setDownloading()
- page.progressFlow.collectLatest { value -> progressIndicator.setProgress(value) }
- }
- Page.State.READY -> page.image?.let { setImage(it) } ?: setError()
- Page.State.ERROR -> setError()
- }
- }
-
- /**
- * Called when the page is queued.
- */
- private fun setQueued() {
- progressIndicator.setProgress(0)
- progressIndicator.show()
- removeErrorLayout()
- frame.recycle()
- }
-
- /**
- * Called when the page is loading.
- */
- private fun setLoading() {
- progressIndicator.show()
- removeErrorLayout()
- }
-
- /**
- * Called when the page is downloading
- */
- private fun setDownloading() {
- progressIndicator.show()
- removeErrorLayout()
- }
-
- /**
- * Called when the page is ready.
- */
- private fun setImage(image: Image) {
- progressIndicator.setProgress(0)
- frame.setImage(image, viewer.readerConfig)
- }
-
- /**
- * Called when the page has an error.
- */
- private fun setError() {
- progressIndicator.hide()
- initErrorLayout()
- }
-
- /**
- * Called when the image is displayed.
- */
- private fun onImageLoaded() {
- progressIndicator.hide()
- removeErrorLayout()
- }
-
- /**
- * Initializes a button to retry pages.
- */
- private fun initErrorLayout() {
- if (errorLayout == null) {
- errorLayout = ReaderErrorLayout(context, page.errorMsg) {
- viewer.activity.retryPage(page.index)
- }.also {
- frame.addView(it)
- }
- }
- }
-
- /**
- * Removes the decode error layout from the holder, if found.
- */
- private fun removeErrorLayout() {
- errorLayout?.let {
- frame.removeView(it)
- errorLayout = null
- }
- }
-}
diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt
deleted file mode 100644
index ac3003082..000000000
--- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt
+++ /dev/null
@@ -1,304 +0,0 @@
-package eu.kanade.tachiyomi.ui.reader.viewer
-
-import android.content.Context
-import android.graphics.Bitmap
-import android.graphics.PointF
-import android.graphics.Rect
-import android.graphics.RectF
-import android.graphics.drawable.Animatable
-import android.graphics.drawable.Drawable
-import android.util.AttributeSet
-import android.view.GestureDetector
-import android.view.MotionEvent
-import android.view.View
-import android.view.ViewGroup.LayoutParams.MATCH_PARENT
-import android.widget.FrameLayout
-import androidx.annotation.AttrRes
-import androidx.annotation.StyleRes
-import androidx.appcompat.widget.AppCompatImageView
-import androidx.compose.ui.graphics.toAndroidRect
-import androidx.core.os.postDelayed
-import androidx.core.view.isVisible
-import coil3.BitmapImage
-import coil3.DrawableImage
-import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
-import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.Companion.EASE_OUT_QUAD
-import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.Companion.SCALE_TYPE_CENTER_INSIDE
-import com.hippo.ehviewer.image.Image
-import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
-import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonSubsamplingImageView
-import eu.kanade.tachiyomi.util.system.animatorDurationScale
-import eu.kanade.tachiyomi.util.view.isVisibleOnScreen
-import io.getstream.photoview.PhotoView
-
-/**
- * A wrapper view for showing page image.
- *
- * Animated image will be drawn by [PhotoView] while [SubsamplingScaleImageView] will take non-animated image.
- *
- * @param isWebtoon if true, [WebtoonSubsamplingImageView] will be used instead of [SubsamplingScaleImageView]
- * and [AppCompatImageView] will be used instead of [PhotoView]
- */
-class ReaderPageImageView @JvmOverloads constructor(
- context: Context,
- attrs: AttributeSet? = null,
- @AttrRes defStyleAttrs: Int = 0,
- @StyleRes defStyleRes: Int = 0,
- private val isWebtoon: Boolean = false,
-) : FrameLayout(context, attrs, defStyleAttrs, defStyleRes) {
-
- private var pageView: View? = null
-
- private var config: Config? = null
-
- var onImageLoaded: (() -> Unit)? = null
- var onScaleChanged: ((newScale: Float) -> Unit)? = null
-
- /**
- * For automatic background. Will be set as background color when [onImageLoaded] is called.
- */
- var pageBackground: Drawable? = null
-
- private fun onImageLoaded() {
- onImageLoaded?.invoke()
- background = pageBackground
- }
-
- fun onScaleChanged(newScale: Float) {
- onScaleChanged?.invoke(newScale)
- }
-
- fun onPageSelected(forward: Boolean) {
- with(pageView as? SubsamplingScaleImageView) {
- if (this == null) return
- if (isReady) {
- landscapeZoom(forward)
- } else {
- setOnImageEventListener {
- setupZoom(config)
- landscapeZoom(forward)
- this@ReaderPageImageView.onImageLoaded()
- }
- }
- }
- }
-
- private fun SubsamplingScaleImageView.landscapeZoom(forward: Boolean) {
- if (config != null && config!!.landscapeZoom && config!!.minimumScaleType == SCALE_TYPE_CENTER_INSIDE && sWidth > sHeight && scale == getMinScale()) {
- handler?.postDelayed(500) {
- val point = when (config!!.zoomStartPosition) {
- ZoomStartPosition.LEFT -> if (forward) PointF(0F, 0F) else PointF(sWidth.toFloat(), 0F)
- ZoomStartPosition.RIGHT -> if (forward) PointF(sWidth.toFloat(), 0F) else PointF(0F, 0F)
- ZoomStartPosition.CENTER -> center.also { it?.y = 0F }
- }
-
- val targetScale = height.toFloat() / sHeight.toFloat()
- animateScaleAndCenter(targetScale, point)!!
- .withDuration(500)
- .withEasing(SubsamplingScaleImageView.EASE_IN_OUT_QUAD)
- .withInterruptible(true)
- .start()
- }
- }
- }
-
- fun setImage(drawable: Image, config: Config) {
- this.config = config
- val image = drawable.innerImage ?: return
- if (image is DrawableImage) {
- prepareAnimatedImageView()
- setAnimatedImage(image.drawable, config)
- }
- if (image is BitmapImage) {
- prepareNonAnimatedImageView()
- setNonAnimatedImage(image.bitmap, drawable.rect.toAndroidRect(), config)
- }
- }
-
- fun recycle() = pageView?.let {
- when (it) {
- is SubsamplingScaleImageView -> it.recycle()
- }
- it.isVisible = false
- }
-
- /**
- * Check whether the image can be panned.
- * @param fn a function that returns the direction to check for
- */
- private fun canPan(fn: (RectF) -> Float): Boolean {
- (pageView as? SubsamplingScaleImageView)?.let { view ->
- RectF().let {
- view.getPanRemaining(it)
- return fn(it) > 1
- }
- }
- return false
- }
-
- /**
- * Pans the image to the left by a screen's width worth.
- */
- fun panLeft(): Boolean {
- val canPan = canPan { it.left }
- if (canPan) {
- pan { center, view -> center.also { it.x -= view.width / view.scale } }
- }
- return canPan
- }
-
- /**
- * Pans the image to the right by a screen's width worth.
- */
- fun panRight(): Boolean {
- val canPan = canPan { it.right }
- if (canPan) {
- pan { center, view -> center.also { it.x += view.width / view.scale } }
- }
- return canPan
- }
-
- /**
- * Pans the image.
- * @param fn a function that computes the new center of the image
- */
- private fun pan(fn: (PointF, SubsamplingScaleImageView) -> PointF) {
- (pageView as? SubsamplingScaleImageView)?.let { view ->
-
- val target = fn(view.center ?: return, view)
- view.animateCenter(target)!!
- .withEasing(EASE_OUT_QUAD)
- .withDuration(250)
- .withInterruptible(true)
- .start()
- }
- }
-
- private fun prepareNonAnimatedImageView() {
- if (pageView is SubsamplingScaleImageView) return
- removeView(pageView)
-
- pageView = if (isWebtoon) {
- WebtoonSubsamplingImageView(context)
- } else {
- SubsamplingScaleImageView(context)
- }.apply {
- setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_CENTER)
- setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE)
- setOnStateChangedListener(
- object : SubsamplingScaleImageView.OnStateChangedListener {
- override fun onScaleChanged(newScale: Float, origin: Int) {
- this@ReaderPageImageView.onScaleChanged(newScale)
- }
-
- override fun onCenterChanged(newCenter: PointF?, origin: Int) {
- // Not used
- }
- },
- )
- }
- addView(pageView, MATCH_PARENT, MATCH_PARENT)
- }
-
- private fun SubsamplingScaleImageView.setupZoom(config: Config?) {
- // 5x zoom
- maxScale = scale * MAX_ZOOM_SCALE
- setDoubleTapZoomScale(scale * 2)
-
- when (config?.zoomStartPosition) {
- ZoomStartPosition.LEFT -> setScaleAndCenter(scale, PointF(0F, 0F))
- ZoomStartPosition.RIGHT -> setScaleAndCenter(scale, PointF(sWidth.toFloat(), 0F))
- ZoomStartPosition.CENTER -> setScaleAndCenter(scale, center.also { it?.y = 0F })
- null -> {}
- }
- }
-
- private fun setNonAnimatedImage(
- image: Bitmap,
- bounds: Rect,
- config: Config,
- ) = (pageView as? SubsamplingScaleImageView)?.apply {
- setDoubleTapZoomDuration(config.zoomDuration.getSystemScaledDuration())
- setMinimumScaleType(config.minimumScaleType)
- setMinimumDpi(1) // Just so that very small image will be fit for initial load
- setOnImageEventListener {
- setupZoom(config)
- if (isVisibleOnScreen()) landscapeZoom(true)
- this@ReaderPageImageView.onImageLoaded()
- }
-
- setImage(image, bounds)
- isVisible = true
- }
-
- private fun prepareAnimatedImageView() {
- if (pageView is AppCompatImageView) return
- removeView(pageView)
-
- pageView = if (isWebtoon) {
- AppCompatImageView(context)
- } else {
- PhotoView(context)
- }.apply {
- adjustViewBounds = true
-
- if (this is PhotoView) {
- setScaleLevels(1F, 2F, MAX_ZOOM_SCALE)
- // Force 2 scale levels on double tap
- setOnDoubleTapListener(
- object : GestureDetector.SimpleOnGestureListener() {
- override fun onDoubleTap(e: MotionEvent): Boolean {
- if (ReaderPreferences.doubleTapToZoom().get()) {
- if (scale > 1F) {
- setScale(1F, e.x, e.y, true)
- } else {
- setScale(2F, e.x, e.y, true)
- }
- }
- return true
- }
- },
- )
- setOnScaleChangeListener { _, _, _ ->
- this@ReaderPageImageView.onScaleChanged(scale)
- }
- }
- }
- addView(pageView, MATCH_PARENT, MATCH_PARENT)
- }
-
- private fun setAnimatedImage(
- image: Drawable,
- config: Config,
- ) = (pageView as? AppCompatImageView)?.apply {
- if (this is PhotoView) {
- setZoomTransitionDuration(config.zoomDuration.getSystemScaledDuration())
- }
-
- setImageDrawable(image)
- (image as? Animatable)?.start()
- isVisible = true
- this@ReaderPageImageView.onImageLoaded()
- }
-
- private fun Int.getSystemScaledDuration(): Int = (this * context.animatorDurationScale).toInt().coerceAtLeast(1)
-
- /**
- * All of the config except [zoomDuration] will only be used for non-animated image.
- */
- data class Config(
- val zoomDuration: Int,
- val minimumScaleType: Int = SCALE_TYPE_CENTER_INSIDE,
- val cropBorders: Boolean = false,
- val zoomStartPosition: ZoomStartPosition = ZoomStartPosition.CENTER,
- val landscapeZoom: Boolean = false,
- )
-
- enum class ZoomStartPosition {
- LEFT,
- CENTER,
- RIGHT,
- }
-}
-
-private const val MAX_ZOOM_SCALE = 5F
diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ReaderProgressIndicator.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ReaderProgressIndicator.kt
deleted file mode 100644
index d3f2cefc3..000000000
--- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ReaderProgressIndicator.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-package eu.kanade.tachiyomi.ui.reader.viewer
-
-import android.content.Context
-import android.util.AttributeSet
-import androidx.annotation.IntRange
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableFloatStateOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.AbstractComposeView
-import androidx.compose.ui.platform.ViewCompositionStrategy
-import androidx.core.view.isVisible
-import com.hippo.ehviewer.ui.theme.EhTheme
-
-class ReaderProgressIndicator @JvmOverloads constructor(
- context: Context,
- attrs: AttributeSet? = null,
- defStyleAttr: Int = 0,
-) : AbstractComposeView(context, attrs, defStyleAttr) {
-
- init {
- setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnDetachedFromWindowOrReleasedFromPool)
- }
-
- private var progress by mutableFloatStateOf(0f)
- private var visible by mutableStateOf(false)
-
- @Composable
- override fun Content() {
- EhTheme {
- Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
- if (visible) CombinedCircularProgressIndicator(progress = progress)
- }
- }
- }
-
- fun show() {
- visible = true
- isVisible = true
- }
-
- fun hide() {
- visible = false
- isVisible = false
- }
-
- fun reset() {
- progress = 0f
- visible = false
- isVisible = true
- }
-
- fun setProgress(@IntRange(from = 0, to = 100) progress: Int) {
- this.progress = progress / 100f
- }
-}
diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ViewerConfig.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ViewerConfig.kt
deleted file mode 100644
index a56a0f7ee..000000000
--- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ViewerConfig.kt
+++ /dev/null
@@ -1,102 +0,0 @@
-package eu.kanade.tachiyomi.ui.reader.viewer
-
-import eu.kanade.tachiyomi.core.preference.Preference
-import eu.kanade.tachiyomi.data.preference.PreferenceValues.TappingInvertMode
-import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-
-/**
- * Common configuration for all viewers.
- */
-abstract class ViewerConfig(readerPreferences: ReaderPreferences, private val scope: CoroutineScope) {
-
- var imagePropertyChangedListener: (() -> Unit)? = null
-
- var navigationModeChangedListener: (() -> Unit)? = null
-
- var tappingInverted = TappingInvertMode.NONE
- var longTapEnabled = true
- var usePageTransitions = false
- var doubleTapAnimDuration = 500
- var volumeKeysEnabled = false
- var volumeKeysInterval = 0
- var volumeKeysInverted = false
- var trueColor = false
- var alwaysShowChapterTransition = true
- var navigationMode = 0
- protected set
-
- var forceNavigationOverlay = false
-
- var navigationOverlayOnStart = false
-
- var dualPageSplit = false
- protected set
-
- var dualPageInvert = false
- protected set
-
- abstract var navigator: ViewerNavigation
- protected set
-
- init {
- readerPreferences.readWithLongTap()
- .register({ longTapEnabled = it })
-
- readerPreferences.pageTransitions()
- .register({ usePageTransitions = it })
-
- readerPreferences.doubleTapAnimSpeed()
- .register({ doubleTapAnimDuration = it })
-
- readerPreferences.readWithVolumeKeys()
- .register({ volumeKeysEnabled = it })
-
- readerPreferences.readWithVolumeKeysInterval()
- .register({ volumeKeysInterval = it })
-
- readerPreferences.readWithVolumeKeysInverted()
- .register({ volumeKeysInverted = it })
-
- readerPreferences.trueColor()
- .register({ trueColor = it }, { imagePropertyChangedListener?.invoke() })
-
- readerPreferences.alwaysShowChapterTransition()
- .register({ alwaysShowChapterTransition = it })
-
- forceNavigationOverlay = readerPreferences.showNavigationOverlayNewUser().get()
- if (forceNavigationOverlay) {
- readerPreferences.showNavigationOverlayNewUser().set(false)
- }
-
- readerPreferences.showNavigationOverlayOnStart()
- .register({ navigationOverlayOnStart = it })
- }
-
- protected abstract fun defaultNavigation(): ViewerNavigation
-
- abstract fun updateNavigation(navigationMode: Int)
-
- fun Preference.register(
- valueAssignment: (T) -> Unit,
- onChanged: (T) -> Unit = {},
- ) {
- changes()
- .onEach { valueAssignment(it) }
- .distinctUntilChanged()
- .onEach { onChanged(it) }
- .launchIn(scope)
- }
-
- fun Flow.register(
- valueAssignment: (T) -> Unit,
- onChanged: (T) -> Unit = {},
- ) = onEach { valueAssignment(it) }
- .distinctUntilChanged()
- .onEach { onChanged(it) }
- .launchIn(scope)
-}
diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ViewerNavigation.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ViewerNavigation.kt
index c1f8cc496..b88e4c378 100644
--- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ViewerNavigation.kt
+++ b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ViewerNavigation.kt
@@ -1,10 +1,9 @@
package eu.kanade.tachiyomi.ui.reader.viewer
-import android.graphics.PointF
import android.graphics.RectF
import androidx.annotation.StringRes
import com.hippo.ehviewer.R
-import eu.kanade.tachiyomi.data.preference.PreferenceValues
+import eu.kanade.tachiyomi.ui.reader.setting.TappingInvertMode
import eu.kanade.tachiyomi.ui.reader.viewer.navigation.DisabledNavigation
import eu.kanade.tachiyomi.ui.reader.viewer.navigation.EdgeNavigation
import eu.kanade.tachiyomi.ui.reader.viewer.navigation.KindlishNavigation
@@ -26,32 +25,19 @@ abstract class ViewerNavigation {
val rectF: RectF,
val type: NavigationRegion,
) {
- fun invert(invertMode: PreferenceValues.TappingInvertMode): Region {
- if (invertMode == PreferenceValues.TappingInvertMode.NONE) return this
+ fun invert(invertMode: TappingInvertMode): Region {
+ if (invertMode == TappingInvertMode.NONE) return this
return this.copy(
rectF = this.rectF.invert(invertMode),
)
}
}
- private val constantMenuRegion: RectF = RectF(0f, 0f, 1f, 0.05f)
-
protected abstract val originalRegions: List
val regions get() = originalRegions.map { it.invert(invertMode) }
- var invertMode: PreferenceValues.TappingInvertMode = PreferenceValues.TappingInvertMode.NONE
-
- fun getAction(pos: PointF): NavigationRegion {
- val x = pos.x
- val y = pos.y
- val region = regions.find { it.rectF.contains(x, y) }
- return when {
- region != null -> region.type
- constantMenuRegion.contains(x, y) -> NavigationRegion.MENU
- else -> NavigationRegion.MENU
- }
- }
+ var invertMode = TappingInvertMode.NONE
companion object {
fun fromPreference(value: Int, isVertical: Boolean) = when (value) {
diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerConfig.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerConfig.kt
deleted file mode 100644
index 2cf93a60b..000000000
--- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerConfig.kt
+++ /dev/null
@@ -1,135 +0,0 @@
-package eu.kanade.tachiyomi.ui.reader.viewer.pager
-
-import eu.kanade.tachiyomi.data.preference.PreferenceValues.TappingInvertMode
-import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
-import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
-import eu.kanade.tachiyomi.ui.reader.viewer.ViewerConfig
-import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
-import eu.kanade.tachiyomi.ui.reader.viewer.navigation.DisabledNavigation
-import eu.kanade.tachiyomi.ui.reader.viewer.navigation.EdgeNavigation
-import eu.kanade.tachiyomi.ui.reader.viewer.navigation.KindlishNavigation
-import eu.kanade.tachiyomi.ui.reader.viewer.navigation.LNavigation
-import eu.kanade.tachiyomi.ui.reader.viewer.navigation.RightAndLeftNavigation
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.drop
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onEach
-
-/**
- * Configuration used by pager viewers.
- */
-class PagerConfig(
- private val viewer: PagerViewer,
- scope: CoroutineScope,
- readerPreferences: ReaderPreferences = ReaderPreferences,
-) : ViewerConfig(readerPreferences, scope) {
-
- var theme = readerPreferences.readerTheme().get()
- private set
-
- var automaticBackground = false
- private set
-
- var dualPageSplitChangedListener: ((Boolean) -> Unit)? = null
-
- var imageScaleType = 1
- private set
-
- var imageZoomType = ReaderPageImageView.ZoomStartPosition.LEFT
- private set
-
- var imageCropBorders = false
- private set
-
- var navigateToPan = false
- private set
-
- var landscapeZoom = false
- private set
-
- init {
- readerPreferences.readerTheme()
- .register(
- {
- theme = it
- automaticBackground = it == 3
- },
- { imagePropertyChangedListener?.invoke() },
- )
-
- readerPreferences.imageScaleType()
- .register({ imageScaleType = it }, { imagePropertyChangedListener?.invoke() })
-
- readerPreferences.zoomStart()
- .register({ zoomTypeFromPreference(it) }, { imagePropertyChangedListener?.invoke() })
-
- readerPreferences.navigateToPan()
- .register({ navigateToPan = it })
-
- readerPreferences.landscapeZoom()
- .register({ landscapeZoom = it }, { imagePropertyChangedListener?.invoke() })
-
- readerPreferences.navigationModePager()
- .register({ navigationMode = it }, { updateNavigation(navigationMode) })
-
- readerPreferences.pagerNavInverted().changes().map { TappingInvertMode.entries[it] }
- .register({ tappingInverted = it }, { navigator.invertMode = it })
- readerPreferences.pagerNavInverted().changes()
- .drop(1)
- .onEach { navigationModeChangedListener?.invoke() }
- .launchIn(scope)
-
- readerPreferences.dualPageSplitPaged()
- .register(
- { dualPageSplit = it },
- {
- imagePropertyChangedListener?.invoke()
- dualPageSplitChangedListener?.invoke(it)
- },
- )
-
- readerPreferences.dualPageInvertPaged()
- .register({ dualPageInvert = it }, { imagePropertyChangedListener?.invoke() })
- }
-
- private fun zoomTypeFromPreference(value: Int) {
- imageZoomType = when (value) {
- // Auto
- 1 -> when {
- viewer.isRtl -> ReaderPageImageView.ZoomStartPosition.RIGHT
- viewer.isVertical -> ReaderPageImageView.ZoomStartPosition.CENTER
- else -> ReaderPageImageView.ZoomStartPosition.LEFT
- }
- // Left
- 2 -> ReaderPageImageView.ZoomStartPosition.LEFT
- // Right
- 3 -> ReaderPageImageView.ZoomStartPosition.RIGHT
- // Center
- else -> ReaderPageImageView.ZoomStartPosition.CENTER
- }
- }
-
- override var navigator: ViewerNavigation = defaultNavigation()
- set(value) {
- field = value.also { it.invertMode = this.tappingInverted }
- }
-
- override fun defaultNavigation(): ViewerNavigation = when {
- viewer.isVertical -> LNavigation()
- else -> RightAndLeftNavigation()
- }
-
- override fun updateNavigation(navigationMode: Int) {
- navigator = when (navigationMode) {
- 0 -> defaultNavigation()
- 1 -> LNavigation()
- 2 -> KindlishNavigation()
- 3 -> EdgeNavigation()
- 4 -> RightAndLeftNavigation()
- 5 -> DisabledNavigation()
- else -> defaultNavigation()
- }
- navigationModeChangedListener?.invoke()
- }
-}
diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerFrame.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerFrame.kt
deleted file mode 100644
index e9bc6eb7c..000000000
--- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerFrame.kt
+++ /dev/null
@@ -1,95 +0,0 @@
-package eu.kanade.tachiyomi.ui.reader.viewer.pager
-
-import android.annotation.SuppressLint
-import android.content.Context
-import android.view.HapticFeedbackConstants
-import android.view.MotionEvent
-import android.widget.FrameLayout
-import androidx.viewpager2.widget.ViewPager2
-import eu.kanade.tachiyomi.ui.reader.viewer.GestureDetectorWithLongTap
-
-/**
- * Frame layout that wraps a [ViewPager2] to handle touch events because [ViewPager2] is final.
- */
-class PagerFrame(context: Context) : FrameLayout(context) {
-
- /**
- * Tap listener function to execute when a tap is detected.
- */
- var tapListener: ((MotionEvent) -> Unit)? = null
-
- /**
- * Long tap listener function to execute when a long tap is detected.
- */
- var longTapListener: ((MotionEvent) -> Boolean)? = null
-
- /**
- * Gesture listener that implements tap and long tap events.
- */
- private val gestureListener = object : GestureDetectorWithLongTap.Listener() {
- override fun onSingleTapConfirmed(ev: MotionEvent): Boolean {
- tapListener?.invoke(ev)
- return true
- }
-
- override fun onLongTapConfirmed(ev: MotionEvent) {
- val listener = longTapListener
- if (listener != null && listener.invoke(ev)) {
- performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
- }
- }
- }
-
- /**
- * Gesture detector which handles motion events.
- */
- private val gestureDetector = GestureDetectorWithLongTap(context, gestureListener)
-
- /**
- * Whether the gesture detector is currently enabled.
- */
- private var isGestureDetectorEnabled = true
-
- /**
- * Dispatches a touch event.
- */
- override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
- val handled = super.dispatchTouchEvent(ev)
- if (isGestureDetectorEnabled) {
- gestureDetector.onTouchEvent(ev)
- }
- return handled
- }
-
- /**
- * Whether the given [ev] should be intercepted. Only used to prevent crashes when child
- * views manipulate [requestDisallowInterceptTouchEvent].
- */
- override fun onInterceptTouchEvent(ev: MotionEvent): Boolean = try {
- super.onInterceptTouchEvent(ev)
- } catch (e: IllegalArgumentException) {
- false
- }
-
- /**
- * Handles a touch event. Only used to prevent crashes when child views manipulate
- * [requestDisallowInterceptTouchEvent].
- */
- @SuppressLint("ClickableViewAccessibility")
- override fun onTouchEvent(ev: MotionEvent): Boolean = try {
- super.onTouchEvent(ev)
- } catch (e: NullPointerException) {
- false
- } catch (e: IndexOutOfBoundsException) {
- false
- } catch (e: IllegalArgumentException) {
- false
- }
-
- /**
- * Enables or disables the gesture detector.
- */
- fun setGestureDetectorEnabled(enabled: Boolean) {
- isGestureDetectorEnabled = enabled
- }
-}
diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt
deleted file mode 100644
index c6b3105a6..000000000
--- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt
+++ /dev/null
@@ -1,390 +0,0 @@
-package eu.kanade.tachiyomi.ui.reader.viewer.pager
-
-import android.graphics.PointF
-import android.view.InputDevice
-import android.view.KeyEvent
-import android.view.MotionEvent
-import android.view.View
-import android.view.ViewGroup.LayoutParams
-import androidx.core.view.get
-import androidx.core.view.isGone
-import androidx.core.view.isVisible
-import androidx.recyclerview.widget.RecyclerView
-import androidx.viewpager2.widget.ViewPager2
-import com.hippo.ehviewer.R
-import eu.kanade.tachiyomi.ui.reader.ReaderActivity
-import eu.kanade.tachiyomi.ui.reader.loader.PageLoader
-import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
-import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
-import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageAdapter
-import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageHolder
-import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
-import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation.NavigationRegion
-import eu.kanade.tachiyomi.util.system.logcat
-import kotlinx.coroutines.MainScope
-import kotlinx.coroutines.cancel
-
-/**
- * Implementation of a [BaseViewer] to display pages with a [ViewPager2].
- */
-class PagerViewer(
- override val activity: ReaderActivity,
- override val isRtl: Boolean = false,
- val isVertical: Boolean = false,
-) : BaseViewer {
-
- private val scope = MainScope()
-
- /**
- * View pager used by this viewer.
- */
- private val pager = ViewPager2(activity).apply {
- if (isRtl) {
- layoutDirection = ViewPager2.LAYOUT_DIRECTION_RTL
- } else if (isVertical) {
- orientation = ViewPager2.ORIENTATION_VERTICAL
- }
- }
-
- private val frame = PagerFrame(activity)
-
- /**
- * Configuration used by the pager, like allow taps, scale mode on images, page transitions...
- */
- private val config = PagerConfig(this, scope)
-
- /**
- * Adapter of the pager.
- */
- private val adapter = ReaderPageAdapter(this)
-
- /**
- * Currently active item. It can be a chapter page or a chapter transition.
- */
- private var currentPage: ReaderPage? = null
-
- /**
- * Viewer chapters to set when the pager enters idle mode. Otherwise, if the view was settling
- * or dragging, there'd be a noticeable and annoying jump.
- */
- private var awaitingIdleViewerChapters: PageLoader? = null
-
- /**
- * Whether the view pager is currently in idle mode. It sets the awaiting chapters if setting
- * this field to true.
- */
- private var isIdle = true
- set(value) {
- field = value
- if (value) {
- awaitingIdleViewerChapters?.let { viewerChapters ->
- setChaptersInternal(viewerChapters)
- awaitingIdleViewerChapters = null
- }
- }
- }
-
- private var longPressed = false
-
- init {
- pager.isVisible = false // Don't layout the pager yet
- pager.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
- pager.clipToPadding = false
- pager.isFocusable = false
- pager.offscreenPageLimit = 1
- pager.id = R.id.reader_pager
- pager.adapter = adapter
- pager.registerOnPageChangeCallback(
- object : ViewPager2.OnPageChangeCallback() {
- override fun onPageSelected(position: Int) {
- if (activity.isScrollingThroughPages.not()) {
- activity.hideMenu()
- }
-
- onPageChange(position)
- }
-
- override fun onPageScrollStateChanged(state: Int) {
- isIdle = state == ViewPager2.SCROLL_STATE_IDLE
- }
- },
- )
- frame.tapListener = { event ->
- val windowOffset = IntArray(2)
- activity.window.decorView.getLocationOnScreen(windowOffset)
- val pos = PointF(
- (event.rawX - windowOffset[0]) / pager.width,
- (event.rawY - windowOffset[1]) / pager.height,
- )
- val navigator = config.navigator
-
- when (navigator.getAction(pos)) {
- NavigationRegion.MENU -> activity.toggleMenu()
- NavigationRegion.NEXT -> moveToNext()
- NavigationRegion.PREV -> moveToPrevious()
- NavigationRegion.RIGHT -> moveRight()
- NavigationRegion.LEFT -> moveLeft()
- }
- }
- frame.longTapListener = f@{
- if (activity.menuVisible || config.longTapEnabled) {
- val item = adapter.items.getOrNull(pager.currentItem)
- if (item is ReaderPage) {
- activity.onPageLongTap(item)
- return@f true
- }
- }
- false
- }
-
- config.imagePropertyChangedListener = {
- refreshAdapter()
- }
-
- config.navigationModeChangedListener = {
- val showOnStart = config.navigationOverlayOnStart || config.forceNavigationOverlay
- activity.binding.navigationOverlay.setNavigation(config.navigator, showOnStart)
- }
-
- frame.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
- frame.addView(pager)
- }
-
- override val readerConfig
- get() = ReaderPageImageView.Config(
- zoomDuration = config.doubleTapAnimDuration,
- minimumScaleType = config.imageScaleType,
- cropBorders = config.imageCropBorders,
- zoomStartPosition = config.imageZoomType,
- landscapeZoom = config.landscapeZoom,
- )
-
- override fun destroy() {
- scope.cancel()
- }
-
- /**
- * Returns the view this viewer uses.
- */
- override fun getView(): View = frame
-
- /**
- * Returns the [ReaderPageHolder] for the provided page
- */
- private fun getPageHolder(position: Int): ReaderPageHolder? =
- (pager[0] as RecyclerView).findViewHolderForAdapterPosition(position) as ReaderPageHolder?
-
- /**
- * Called when a new [ReaderPage] is marked as active
- */
- private fun onPageChange(position: Int) {
- val page = adapter.items.getOrNull(position)
- if (page != null && currentPage != page) {
- val forward = page.number > (currentPage?.number ?: 0)
- currentPage = page
- onReaderPageSelected(page, forward)
- }
- }
-
- /**
- * Called when a [ReaderPage] is marked as active. It notifies the
- * activity of the change and requests the preload of the next chapter if this is the last page.
- */
- private fun onReaderPageSelected(page: ReaderPage, forward: Boolean) {
- activity.onPageSelected(page)
-
- // Notify holder of page change
- getPageHolder(page.index)?.onPageSelected(forward)
- }
-
- /**
- * Tells this viewer to set the given [provider] as active. If the pager is currently idle,
- * it sets the chapters immediately, otherwise they are saved and set when it becomes idle.
- */
- override fun setGalleryProvider(provider: PageLoader) {
- if (isIdle) {
- setChaptersInternal(provider)
- } else {
- awaitingIdleViewerChapters = provider
- }
- }
-
- /**
- * Sets the active [chapters] on this pager.
- */
- private fun setChaptersInternal(chapters: PageLoader) {
- logcat { "setChaptersInternal" }
- adapter.setChapters(chapters)
- refreshAdapter(0)
-
- // Layout the pager once a chapter is being set
- if (pager.isGone) {
- logcat { "Pager first layout" }
- pager.isVisible = true
- }
- }
-
- /**
- * Tells this viewer to move to the given [page].
- */
- override fun moveToPage(page: ReaderPage) {
- logcat { "moveToPage ${page.number}" }
- val position = adapter.items.indexOf(page)
- if (position != -1) {
- val currentPosition = pager.currentItem
- pager.setCurrentItem(position, true)
- // manually call onPageChange since ViewPager listener is not triggered in this case
- if (currentPosition == position) {
- onPageChange(position)
- }
- } else {
- logcat { "Page $page not found in adapter" }
- }
- }
-
- /**
- * Moves to the next page.
- */
- private fun moveToNext() {
- val currentItem = pager.currentItem
- if (currentItem != adapter.itemCount - 1) {
- val holder = getPageHolder(currentItem)
- if (!config.navigateToPan || holder?.panEnd(isRtl) != true) {
- pager.setCurrentItem(currentItem + 1, config.usePageTransitions)
- }
- }
- }
-
- /**
- * Moves to the previous page.
- */
- private fun moveToPrevious() {
- val currentItem = pager.currentItem
- if (currentItem != 0) {
- val holder = getPageHolder(currentItem)
- if (!config.navigateToPan || holder?.panEnd(!isRtl) != true) {
- pager.setCurrentItem(currentItem - 1, config.usePageTransitions)
- }
- }
- }
-
- /**
- * Moves to the page at the right.
- */
- private fun moveRight() {
- if (isRtl) {
- moveToPrevious()
- } else {
- moveToNext()
- }
- }
-
- /**
- * Moves to the page at the left.
- */
- private fun moveLeft() {
- if (isRtl) {
- moveToNext()
- } else {
- moveToPrevious()
- }
- }
-
- /**
- * Moves to the page at the top (or previous).
- */
- private fun moveUp() {
- moveToPrevious()
- }
-
- /**
- * Moves to the page at the bottom (or next).
- */
- private fun moveDown() {
- moveToNext()
- }
-
- override fun refreshAdapter() {
- refreshAdapter(pager.currentItem)
- }
-
- /**
- * Resets the adapter in order to recreate all the views. Used when a image configuration is
- * changed.
- */
- private fun refreshAdapter(currentItem: Int) {
- adapter.refresh()
- pager.adapter = adapter
- pager.setCurrentItem(currentItem, false)
- }
-
- /**
- * Called from the containing activity when a key [event] is received. It should return true
- * if the event was handled, false otherwise.
- */
- override fun handleKeyEvent(event: KeyEvent): Boolean {
- val isUp = event.action == KeyEvent.ACTION_UP
- val ctrlPressed = event.metaState.and(KeyEvent.META_CTRL_ON) > 0
- val interval = config.volumeKeysInterval
- val movePage = longPressed && (interval == 0 || event.repeatCount % (interval + 1) == 0)
- longPressed = event.isLongPress || !isUp && longPressed
-
- when (event.keyCode) {
- KeyEvent.KEYCODE_VOLUME_DOWN -> {
- if (!config.volumeKeysEnabled || activity.menuVisible) {
- return false
- } else if (isUp != movePage) {
- if (!config.volumeKeysInverted) moveDown() else moveUp()
- }
- }
-
- KeyEvent.KEYCODE_VOLUME_UP -> {
- if (!config.volumeKeysEnabled || activity.menuVisible) {
- return false
- } else if (isUp != movePage) {
- if (!config.volumeKeysInverted) moveUp() else moveDown()
- }
- }
-
- KeyEvent.KEYCODE_DPAD_RIGHT -> {
- if (isUp) {
- if (ctrlPressed) moveToNext() else moveRight()
- }
- }
-
- KeyEvent.KEYCODE_DPAD_LEFT -> {
- if (isUp) {
- if (ctrlPressed) moveToPrevious() else moveLeft()
- }
- }
-
- KeyEvent.KEYCODE_DPAD_DOWN -> if (isUp) moveDown()
- KeyEvent.KEYCODE_DPAD_UP -> if (isUp) moveUp()
- KeyEvent.KEYCODE_PAGE_DOWN -> if (isUp) moveDown()
- KeyEvent.KEYCODE_PAGE_UP -> if (isUp) moveUp()
- KeyEvent.KEYCODE_MENU -> if (isUp) activity.toggleMenu()
- else -> return false
- }
- return true
- }
-
- /**
- * Called from the containing activity when a generic motion [event] is received. It should
- * return true if the event was handled, false otherwise.
- */
- override fun handleGenericMotionEvent(event: MotionEvent): Boolean {
- if (event.source and InputDevice.SOURCE_CLASS_POINTER != 0) {
- when (event.action) {
- MotionEvent.ACTION_SCROLL -> {
- if (event.getAxisValue(MotionEvent.AXIS_VSCROLL) < 0.0f) {
- moveDown()
- } else {
- moveUp()
- }
- return true
- }
- }
- }
- return false
- }
-}
diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonConfig.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonConfig.kt
deleted file mode 100644
index 12b1f2f86..000000000
--- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonConfig.kt
+++ /dev/null
@@ -1,88 +0,0 @@
-package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
-
-import eu.kanade.tachiyomi.data.preference.PreferenceValues.TappingInvertMode
-import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
-import eu.kanade.tachiyomi.ui.reader.viewer.ViewerConfig
-import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
-import eu.kanade.tachiyomi.ui.reader.viewer.navigation.DisabledNavigation
-import eu.kanade.tachiyomi.ui.reader.viewer.navigation.EdgeNavigation
-import eu.kanade.tachiyomi.ui.reader.viewer.navigation.KindlishNavigation
-import eu.kanade.tachiyomi.ui.reader.viewer.navigation.LNavigation
-import eu.kanade.tachiyomi.ui.reader.viewer.navigation.RightAndLeftNavigation
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.drop
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onEach
-
-/**
- * Configuration used by webtoon viewers.
- */
-class WebtoonConfig(
- scope: CoroutineScope,
- readerPreferences: ReaderPreferences = ReaderPreferences,
-) : ViewerConfig(readerPreferences, scope) {
-
- var themeChangedListener: (() -> Unit)? = null
-
- var imageCropBorders = false
- private set
-
- var sidePadding = 0
- private set
-
- var longStripSplit = false
- private set
-
- var longStripSplitChangedListener: ((Boolean) -> Unit)? = null
-
- val theme = readerPreferences.readerTheme().get()
-
- init {
- readerPreferences.webtoonSidePadding()
- .register({ sidePadding = it }, { imagePropertyChangedListener?.invoke() })
-
- readerPreferences.navigationModeWebtoon()
- .register({ navigationMode = it }, { updateNavigation(it) })
-
- readerPreferences.webtoonNavInverted().changes().map { TappingInvertMode.entries[it] }
- .register({ tappingInverted = it }, { navigator.invertMode = it })
- readerPreferences.webtoonNavInverted().changes()
- .drop(1)
- .onEach { navigationModeChangedListener?.invoke() }
- .launchIn(scope)
-
- readerPreferences.dualPageSplitWebtoon()
- .register({ dualPageSplit = it }, { imagePropertyChangedListener?.invoke() })
-
- readerPreferences.dualPageInvertWebtoon()
- .register({ dualPageInvert = it }, { imagePropertyChangedListener?.invoke() })
-
- readerPreferences.readerTheme().changes()
- .drop(1)
- .distinctUntilChanged()
- .onEach { themeChangedListener?.invoke() }
- .launchIn(scope)
- }
-
- override var navigator: ViewerNavigation = defaultNavigation()
- set(value) {
- field = value.also { it.invertMode = tappingInverted }
- }
-
- override fun defaultNavigation(): ViewerNavigation = LNavigation()
-
- override fun updateNavigation(navigationMode: Int) {
- this.navigator = when (navigationMode) {
- 0 -> defaultNavigation()
- 1 -> LNavigation()
- 2 -> KindlishNavigation()
- 3 -> EdgeNavigation()
- 4 -> RightAndLeftNavigation()
- 5 -> DisabledNavigation()
- else -> defaultNavigation()
- }
- navigationModeChangedListener?.invoke()
- }
-}
diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonFrame.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonFrame.kt
deleted file mode 100644
index 698ce3cf9..000000000
--- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonFrame.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
-
-import android.content.Context
-import android.view.GestureDetector
-import android.view.MotionEvent
-import android.view.ScaleGestureDetector
-import android.widget.FrameLayout
-
-/**
- * Frame layout which contains a [WebtoonRecyclerView]. It's needed to handle touch events,
- * because the recyclerview is scaled and its touch events are translated, which breaks the
- * detectors.
- *
- * TODO consider integrating this class into [WebtoonViewer].
- */
-class WebtoonFrame(context: Context) : FrameLayout(context) {
-
- /**
- * Scale detector, either with pinch or quick scale.
- */
- private val scaleDetector = ScaleGestureDetector(context, ScaleListener())
-
- /**
- * Fling detector.
- */
- private val flingDetector = GestureDetector(context, FlingListener())
-
- /**
- * Recycler view added in this frame.
- */
- private val recycler: WebtoonRecyclerView?
- get() = getChildAt(0) as? WebtoonRecyclerView
-
- /**
- * Dispatches a touch event to the detectors.
- */
- override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
- scaleDetector.onTouchEvent(ev)
- flingDetector.onTouchEvent(ev)
- return super.dispatchTouchEvent(ev)
- }
-
- /**
- * Scale listener used to delegate events to the recycler view.
- */
- inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() {
- override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
- recycler?.onScaleBegin()
- return true
- }
-
- override fun onScale(detector: ScaleGestureDetector): Boolean {
- recycler?.onScale(detector.scaleFactor)
- return true
- }
-
- override fun onScaleEnd(detector: ScaleGestureDetector) {
- recycler?.onScaleEnd()
- }
- }
-
- /**
- * Fling listener used to delegate events to the recycler view.
- */
- inner class FlingListener : GestureDetector.SimpleOnGestureListener() {
- override fun onDown(e: MotionEvent): Boolean = true
-
- override fun onFling(
- e1: MotionEvent?,
- e2: MotionEvent,
- velocityX: Float,
- velocityY: Float,
- ): Boolean = recycler?.zoomFling(velocityX.toInt(), velocityY.toInt()) ?: false
- }
-}
diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonLayoutManager.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonLayoutManager.kt
deleted file mode 100644
index 3c87af3e6..000000000
--- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonLayoutManager.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-@file:Suppress("PackageDirectoryMismatch")
-
-package androidx.recyclerview.widget
-
-import androidx.recyclerview.widget.RecyclerView.NO_POSITION
-import eu.kanade.tachiyomi.ui.reader.ReaderActivity
-
-/**
- * Layout manager used by the webtoon viewer. Item prefetch is disabled because the extra layout
- * space feature is used which allows setting the image even if the holder is not visible,
- * avoiding (in most cases) black views when they are visible.
- *
- * This layout manager uses the same package name as the support library in order to use a package
- * protected method.
- */
-class WebtoonLayoutManager(activity: ReaderActivity) : LinearLayoutManager(activity) {
-
- /**
- * Extra layout space is set to half the screen height.
- */
- private val extraLayoutSpace = activity.resources.displayMetrics.heightPixels / 2
-
- init {
- isItemPrefetchEnabled = false
- }
-
- /**
- * Returns the custom extra layout space.
- */
- @Deprecated("Deprecated in Java")
- override fun getExtraLayoutSpace(state: RecyclerView.State): Int = extraLayoutSpace
-
- /**
- * Returns the position of the last item whose end side is visible on screen.
- */
- fun findLastEndVisibleItemPosition(): Int {
- ensureLayoutState()
- val callback = if (mOrientation == HORIZONTAL) {
- mHorizontalBoundCheck
- } else {
- mVerticalBoundCheck
- }.mCallback
- val start = callback.parentStart
- val end = callback.parentEnd
- for (i in childCount - 1 downTo 0) {
- val child = getChildAt(i)!!
- val childStart = callback.getChildStart(child)
- val childEnd = callback.getChildEnd(child)
- if (childEnd <= end || childStart < start) {
- return getPosition(child)
- }
- }
- return NO_POSITION
- }
-}
diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonRecyclerView.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonRecyclerView.kt
deleted file mode 100644
index d01cab53a..000000000
--- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonRecyclerView.kt
+++ /dev/null
@@ -1,335 +0,0 @@
-package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
-
-import android.animation.AnimatorSet
-import android.animation.ValueAnimator
-import android.content.Context
-import android.util.AttributeSet
-import android.view.HapticFeedbackConstants
-import android.view.MotionEvent
-import android.view.ViewConfiguration
-import android.view.animation.DecelerateInterpolator
-import androidx.core.animation.doOnEnd
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
-import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
-import eu.kanade.tachiyomi.ui.reader.viewer.GestureDetectorWithLongTap
-import kotlin.math.abs
-
-/**
- * Implementation of a [RecyclerView] used by the webtoon reader.
- */
-class WebtoonRecyclerView @JvmOverloads constructor(
- context: Context,
- attrs: AttributeSet? = null,
- defStyle: Int = 0,
-) : RecyclerView(context, attrs, defStyle) {
-
- private var isZooming = false
- private var atLastPosition = false
- private var atFirstPosition = false
- private var halfWidth = 0
- private var halfHeight = 0
- var originalHeight = 0
- private set
- private var heightSet = false
- private var firstVisibleItemPosition = 0
- private var lastVisibleItemPosition = 0
- private var currentScale = DEFAULT_RATE
-
- private val listener = GestureListener()
- private val detector = Detector()
-
- var tapListener: ((MotionEvent) -> Unit)? = null
- var longTapListener: ((MotionEvent) -> Boolean)? = null
-
- init {
- isVerticalScrollBarEnabled = false
- isHorizontalScrollBarEnabled = false
- }
-
- override fun onMeasure(widthSpec: Int, heightSpec: Int) {
- halfWidth = MeasureSpec.getSize(widthSpec) / 2
- halfHeight = MeasureSpec.getSize(heightSpec) / 2
- if (!heightSet) {
- originalHeight = MeasureSpec.getSize(heightSpec)
- heightSet = true
- }
- super.onMeasure(widthSpec, heightSpec)
- }
-
- override fun onTouchEvent(e: MotionEvent): Boolean {
- if (scrollState == SCROLL_STATE_IDLE) {
- detector.onTouchEvent(e)
- }
- return super.onTouchEvent(e)
- }
-
- override fun onScrolled(dx: Int, dy: Int) {
- super.onScrolled(dx, dy)
- val layoutManager = layoutManager
- lastVisibleItemPosition =
- (layoutManager as LinearLayoutManager).findLastVisibleItemPosition()
- firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
- }
-
- override fun onScrollStateChanged(state: Int) {
- super.onScrollStateChanged(state)
- val layoutManager = layoutManager
- val visibleItemCount = layoutManager?.childCount ?: 0
- val totalItemCount = layoutManager?.itemCount ?: 0
- atLastPosition = visibleItemCount > 0 && lastVisibleItemPosition == totalItemCount - 1
- atFirstPosition = firstVisibleItemPosition == 0
- }
-
- private fun getPositionX(positionX: Float): Float {
- if (currentScale < 1) {
- return 0f
- }
- val maxPositionX = halfWidth * (currentScale - 1)
- return positionX.coerceIn(-maxPositionX, maxPositionX)
- }
-
- private fun getPositionY(positionY: Float): Float {
- if (currentScale < 1) {
- return (originalHeight / 2 - halfHeight).toFloat()
- }
- val maxPositionY = halfHeight * (currentScale - 1)
- return positionY.coerceIn(-maxPositionY, maxPositionY)
- }
-
- private fun zoom(
- fromRate: Float,
- toRate: Float,
- fromX: Float,
- toX: Float,
- fromY: Float,
- toY: Float,
- ) {
- isZooming = true
- val animatorSet = AnimatorSet()
- val translationXAnimator = ValueAnimator.ofFloat(fromX, toX)
- translationXAnimator.addUpdateListener { animation -> x = animation.animatedValue as Float }
-
- val translationYAnimator = ValueAnimator.ofFloat(fromY, toY)
- translationYAnimator.addUpdateListener { animation -> y = animation.animatedValue as Float }
-
- val scaleAnimator = ValueAnimator.ofFloat(fromRate, toRate)
- scaleAnimator.addUpdateListener { animation ->
- setScaleRate(animation.animatedValue as Float)
- }
- animatorSet.playTogether(translationXAnimator, translationYAnimator, scaleAnimator)
- animatorSet.duration = ANIMATOR_DURATION_TIME.toLong()
- animatorSet.interpolator = DecelerateInterpolator()
- animatorSet.start()
- animatorSet.doOnEnd {
- isZooming = false
- currentScale = toRate
- }
- }
-
- fun zoomFling(velocityX: Int, velocityY: Int): Boolean {
- if (currentScale <= 1f) return false
-
- val distanceTimeFactor = 0.4f
- var newX: Float? = null
- var newY: Float? = null
-
- if (velocityX != 0) {
- val dx = (distanceTimeFactor * velocityX / 2)
- newX = getPositionX(x + dx)
- }
- if (velocityY != 0 && (atFirstPosition || atLastPosition)) {
- val dy = (distanceTimeFactor * velocityY / 2)
- newY = getPositionY(y + dy)
- }
-
- animate()
- .apply {
- newX?.let { x(it) }
- newY?.let { y(it) }
- }
- .setInterpolator(DecelerateInterpolator())
- .setDuration(400)
- .start()
-
- return true
- }
-
- private fun zoomScrollBy(dx: Int, dy: Int) {
- if (dx != 0) {
- x = getPositionX(x + dx)
- }
- if (dy != 0) {
- y = getPositionY(y + dy)
- }
- }
-
- private fun setScaleRate(rate: Float) {
- scaleX = rate
- scaleY = rate
- }
-
- fun onScale(scaleFactor: Float) {
- currentScale *= scaleFactor
- currentScale = currentScale.coerceIn(
- MIN_RATE,
- MAX_SCALE_RATE,
- )
-
- setScaleRate(currentScale)
-
- layoutParams.height = if (currentScale < 1) {
- (originalHeight / currentScale).toInt()
- } else {
- originalHeight
- }
- halfHeight = layoutParams.height / 2
-
- if (currentScale != DEFAULT_RATE) {
- x = getPositionX(x)
- y = getPositionY(y)
- } else {
- x = 0f
- y = 0f
- }
-
- requestLayout()
- }
-
- fun onScaleBegin() {
- if (detector.isDoubleTapping) {
- detector.isQuickScaling = true
- }
- }
-
- fun onScaleEnd() {
- if (scaleX < MIN_RATE) {
- zoom(currentScale, MIN_RATE, x, 0f, y, 0f)
- }
- }
-
- inner class GestureListener : GestureDetectorWithLongTap.Listener() {
-
- override fun onSingleTapConfirmed(ev: MotionEvent): Boolean {
- tapListener?.invoke(ev)
- return false
- }
-
- override fun onDoubleTap(ev: MotionEvent): Boolean {
- if (ReaderPreferences.doubleTapToZoom().get()) detector.isDoubleTapping = true
- return false
- }
-
- fun onDoubleTapConfirmed(ev: MotionEvent) {
- if (!isZooming) {
- if (scaleX != DEFAULT_RATE) {
- zoom(currentScale, DEFAULT_RATE, x, 0f, y, 0f)
- } else {
- val toScale = 2f
- val toX = (halfWidth - ev.x) * (toScale - 1)
- val toY = (halfHeight - ev.y) * (toScale - 1)
- zoom(DEFAULT_RATE, toScale, 0f, toX, 0f, toY)
- }
- }
- }
-
- override fun onLongTapConfirmed(ev: MotionEvent) {
- val listener = longTapListener
- if (listener != null && listener.invoke(ev)) {
- performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
- }
- }
- }
-
- inner class Detector : GestureDetectorWithLongTap(context, listener) {
-
- private var scrollPointerId = 0
- private var downX = 0
- private var downY = 0
- private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
- private var isZoomDragging = false
- var isDoubleTapping = false
- var isQuickScaling = false
-
- override fun onTouchEvent(ev: MotionEvent): Boolean {
- val action = ev.actionMasked
- val actionIndex = ev.actionIndex
-
- when (action) {
- MotionEvent.ACTION_DOWN -> {
- scrollPointerId = ev.getPointerId(0)
- downX = (ev.x + 0.5f).toInt()
- downY = (ev.y + 0.5f).toInt()
- }
- MotionEvent.ACTION_POINTER_DOWN -> {
- scrollPointerId = ev.getPointerId(actionIndex)
- downX = (ev.getX(actionIndex) + 0.5f).toInt()
- downY = (ev.getY(actionIndex) + 0.5f).toInt()
- }
- MotionEvent.ACTION_MOVE -> {
- if (isDoubleTapping && isQuickScaling) {
- return true
- }
-
- val index = ev.findPointerIndex(scrollPointerId)
- if (index < 0) {
- return false
- }
-
- val x = (ev.getX(index) + 0.5f).toInt()
- val y = (ev.getY(index) + 0.5f).toInt()
- var dx = x - downX
- var dy = if (atFirstPosition || atLastPosition) y - downY else 0
-
- if (!isZoomDragging && currentScale > 1f) {
- var startScroll = false
-
- if (abs(dx) > touchSlop) {
- if (dx < 0) {
- dx += touchSlop
- } else {
- dx -= touchSlop
- }
- startScroll = true
- }
- if (abs(dy) > touchSlop) {
- if (dy < 0) {
- dy += touchSlop
- } else {
- dy -= touchSlop
- }
- startScroll = true
- }
-
- if (startScroll) {
- isZoomDragging = true
- }
- }
-
- if (isZoomDragging) {
- zoomScrollBy(dx, dy)
- }
- }
- MotionEvent.ACTION_UP -> {
- if (isDoubleTapping && !isQuickScaling) {
- listener.onDoubleTapConfirmed(ev)
- }
- isZoomDragging = false
- isDoubleTapping = false
- isQuickScaling = false
- }
- MotionEvent.ACTION_CANCEL -> {
- isZoomDragging = false
- isDoubleTapping = false
- isQuickScaling = false
- }
- }
- return super.onTouchEvent(ev)
- }
- }
-}
-
-private const val ANIMATOR_DURATION_TIME = 200
-private const val MIN_RATE = 0.5f
-private const val DEFAULT_RATE = 1f
-private const val MAX_SCALE_RATE = 3f
diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonSubsamplingImageView.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonSubsamplingImageView.kt
deleted file mode 100644
index cab4bca2b..000000000
--- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonSubsamplingImageView.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
-
-import android.content.Context
-import android.util.AttributeSet
-import android.view.MotionEvent
-import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
-
-/**
- * Implementation of subsampling scale image view that ignores all touch events, because the
- * webtoon viewer handles all the gestures.
- */
-class WebtoonSubsamplingImageView @JvmOverloads constructor(
- context: Context,
- attrs: AttributeSet? = null,
-) : SubsamplingScaleImageView(context, attrs) {
-
- override fun onTouchEvent(event: MotionEvent): Boolean = false
-}
diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt
deleted file mode 100644
index bce0167f5..000000000
--- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt
+++ /dev/null
@@ -1,291 +0,0 @@
-package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
-
-import android.graphics.PointF
-import android.view.KeyEvent
-import android.view.MotionEvent
-import android.view.View
-import android.view.ViewGroup
-import android.view.ViewGroup.LayoutParams.MATCH_PARENT
-import androidx.core.app.ActivityCompat
-import androidx.core.view.isGone
-import androidx.core.view.isVisible
-import androidx.recyclerview.widget.RecyclerView
-import androidx.recyclerview.widget.WebtoonLayoutManager
-import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
-import eu.kanade.tachiyomi.ui.reader.ReaderActivity
-import eu.kanade.tachiyomi.ui.reader.loader.PageLoader
-import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
-import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
-import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageAdapter
-import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
-import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation.NavigationRegion
-import eu.kanade.tachiyomi.util.system.logcat
-import kotlinx.coroutines.MainScope
-import kotlinx.coroutines.cancel
-
-/**
- * Implementation of a [BaseViewer] to display pages with a [RecyclerView].
- */
-class WebtoonViewer(override val activity: ReaderActivity, val isContinuous: Boolean = true) : BaseViewer {
-
- override val isRtl: Boolean = false
-
- private val scope = MainScope()
-
- /**
- * Recycler view used by this viewer.
- */
- val recycler = WebtoonRecyclerView(activity)
-
- /**
- * Frame containing the recycler view.
- */
- private val frame = WebtoonFrame(activity)
-
- /**
- * Layout manager of the recycler view.
- */
- private val layoutManager = WebtoonLayoutManager(activity)
-
- /**
- * Configuration used by this viewer, like allow taps, or crop image borders.
- */
- val config = WebtoonConfig(scope)
-
- /**
- * Adapter of the recycler view.
- */
- private val adapter = ReaderPageAdapter(this, true)
-
- /**
- * Distance to scroll when the user taps on one side of the recycler view.
- */
- private var scrollDistance = activity.resources.displayMetrics.heightPixels * 3 / 4
-
- /**
- * Currently active item. It can be a chapter page or a chapter transition.
- */
- private var currentPage: Any? = null
-
- private val threshold: Int = 13
-
- private var longPressed = false
-
- init {
- recycler.isVisible = false // Don't let the recycler layout yet
- recycler.layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
- recycler.clipToPadding = false
- recycler.isFocusable = false
- recycler.itemAnimator = null
- recycler.layoutManager = layoutManager
- recycler.adapter = adapter
- recycler.addOnScrollListener(
- object : RecyclerView.OnScrollListener() {
- override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
- onScrolled()
-
- if ((dy > threshold || dy < -threshold) && activity.menuVisible) {
- activity.hideMenu()
- }
- }
- },
- )
- recycler.tapListener = { event ->
- val windowOffset = IntArray(2)
- activity.window.decorView.getLocationOnScreen(windowOffset)
- val pos = PointF(
- (event.rawX - windowOffset[0]) / recycler.width,
- (event.rawY - windowOffset[1]) / recycler.originalHeight,
- )
- val navigator = config.navigator
-
- when (navigator.getAction(pos)) {
- NavigationRegion.MENU -> activity.toggleMenu()
- NavigationRegion.NEXT, NavigationRegion.RIGHT -> scrollDown()
- NavigationRegion.PREV, NavigationRegion.LEFT -> scrollUp()
- }
- }
- recycler.longTapListener = f@{ event ->
- if (activity.menuVisible || config.longTapEnabled) {
- val child = recycler.findChildViewUnder(event.x, event.y)
- if (child != null) {
- val position = recycler.getChildAdapterPosition(child)
- val item = adapter.items.getOrNull(position)
- if (item is ReaderPage) {
- activity.onPageLongTap(item)
- return@f true
- }
- }
- }
- false
- }
-
- config.imagePropertyChangedListener = {
- refreshAdapter()
- }
-
- config.themeChangedListener = {
- ActivityCompat.recreate(activity)
- }
-
- config.navigationModeChangedListener = {
- val showOnStart = config.navigationOverlayOnStart || config.forceNavigationOverlay
- activity.binding.navigationOverlay.setNavigation(config.navigator, showOnStart)
- }
-
- frame.layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
- frame.addView(recycler)
- }
-
- override val readerConfig
- get() = ReaderPageImageView.Config(
- zoomDuration = config.doubleTapAnimDuration,
- minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_FIT_WIDTH,
- cropBorders = config.imageCropBorders,
- )
-
- override fun getViewForInsets(): View = recycler
-
- /**
- * Returns the view this viewer uses.
- */
- override fun getView(): View = frame
-
- /**
- * Destroys this viewer. Called when leaving the reader or swapping viewers.
- */
- override fun destroy() {
- scope.cancel()
- }
-
- /**
- * Called from the RecyclerView listener when a [page] is marked as active. It notifies the
- * activity of the change and requests the preload of the next chapter if this is the last page.
- */
- private fun onPageSelected(page: ReaderPage) {
- activity.onPageSelected(page)
- }
-
- /**
- * Tells this viewer to set the given [provider] as active.
- */
- override fun setGalleryProvider(provider: PageLoader) {
- adapter.setChapters(provider)
- refreshAdapter(0)
-
- if (recycler.isGone) {
- logcat { "Recycler first layout" }
- recycler.isVisible = true
- }
- }
-
- /**
- * Tells this viewer to move to the given [page].
- */
- override fun moveToPage(page: ReaderPage) {
- logcat { "moveToPage" }
- val position = adapter.items.indexOf(page)
- if (position != -1) {
- layoutManager.scrollToPositionWithOffset(position, 0)
- onScrolled(pos = position)
- } else {
- logcat { "Page $page not found in adapter" }
- }
- }
-
- fun onScrolled(pos: Int? = null) {
- val position = pos ?: layoutManager.findLastEndVisibleItemPosition()
- val item = adapter.items.getOrNull(position)
- if (item != null && currentPage != item) {
- currentPage = item
- onPageSelected(item)
- }
- }
-
- /**
- * Scrolls up by [scrollDistance].
- */
- private fun scrollUp() {
- if (config.usePageTransitions) {
- recycler.smoothScrollBy(0, -scrollDistance)
- } else {
- recycler.scrollBy(0, -scrollDistance)
- }
- }
-
- /**
- * Scrolls down by [scrollDistance].
- */
- private fun scrollDown() {
- if (config.usePageTransitions) {
- recycler.smoothScrollBy(0, scrollDistance)
- } else {
- recycler.scrollBy(0, scrollDistance)
- }
- }
-
- /**
- * Called from the containing activity when a key [event] is received. It should return true
- * if the event was handled, false otherwise.
- */
- override fun handleKeyEvent(event: KeyEvent): Boolean {
- val isUp = event.action == KeyEvent.ACTION_UP
- val interval = config.volumeKeysInterval
- val movePage = longPressed && (interval == 0 || event.repeatCount % (interval + 1) == 0)
- longPressed = event.isLongPress || !isUp && longPressed
-
- when (event.keyCode) {
- KeyEvent.KEYCODE_VOLUME_DOWN -> {
- if (!config.volumeKeysEnabled || activity.menuVisible) {
- return false
- } else if (isUp != movePage) {
- if (!config.volumeKeysInverted) scrollDown() else scrollUp()
- }
- }
-
- KeyEvent.KEYCODE_VOLUME_UP -> {
- if (!config.volumeKeysEnabled || activity.menuVisible) {
- return false
- } else if (isUp != movePage) {
- if (!config.volumeKeysInverted) scrollUp() else scrollDown()
- }
- }
-
- KeyEvent.KEYCODE_MENU -> if (isUp) activity.toggleMenu()
-
- KeyEvent.KEYCODE_DPAD_LEFT,
- KeyEvent.KEYCODE_DPAD_UP,
- KeyEvent.KEYCODE_PAGE_UP,
- -> if (isUp) scrollUp()
-
- KeyEvent.KEYCODE_DPAD_RIGHT,
- KeyEvent.KEYCODE_DPAD_DOWN,
- KeyEvent.KEYCODE_PAGE_DOWN,
- -> if (isUp) scrollDown()
-
- else -> return false
- }
- return true
- }
-
- /**
- * Called from the containing activity when a generic motion [event] is received. It should
- * return true if the event was handled, false otherwise.
- */
- override fun handleGenericMotionEvent(event: MotionEvent): Boolean = false
-
- override fun refreshAdapter() {
- refreshAdapter(layoutManager.findLastEndVisibleItemPosition())
- }
-
- /**
- * Notifies adapter of changes around the current page to trigger a relayout in the recycler.
- * Used when an image configuration is changed.
- */
- private fun refreshAdapter(position: Int) {
- val positionStart = maxOf(0, position - 3)
- val positionEnd = minOf(position + 3, adapter.itemCount - 1)
- adapter.refresh()
- adapter.notifyItemRangeChanged(positionStart, positionEnd - positionStart + 1)
- }
-}
diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/util/lang/RectFExtensions.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/util/lang/RectFExtensions.kt
index d1ad0b5b9..fded1950c 100644
--- a/app/src/main/kotlin/eu/kanade/tachiyomi/util/lang/RectFExtensions.kt
+++ b/app/src/main/kotlin/eu/kanade/tachiyomi/util/lang/RectFExtensions.kt
@@ -1,9 +1,9 @@
package eu.kanade.tachiyomi.util.lang
import android.graphics.RectF
-import eu.kanade.tachiyomi.data.preference.PreferenceValues
+import eu.kanade.tachiyomi.ui.reader.setting.TappingInvertMode
-fun RectF.invert(invertMode: PreferenceValues.TappingInvertMode): RectF {
+fun RectF.invert(invertMode: TappingInvertMode): RectF {
val horizontal = invertMode.shouldInvertHorizontal
val vertical = invertMode.shouldInvertVertical
return when {
diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/util/system/ContextExtensions.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/util/system/ContextExtensions.kt
deleted file mode 100644
index 44a64e856..000000000
--- a/app/src/main/kotlin/eu/kanade/tachiyomi/util/system/ContextExtensions.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-package eu.kanade.tachiyomi.util.system
-
-import android.content.Context
-import android.content.res.Configuration
-import android.content.res.Resources
-import android.provider.Settings
-import androidx.appcompat.view.ContextThemeWrapper
-import com.hippo.ehviewer.R
-import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
-
-/**
- * Converts to px.
- */
-val Int.dpToPx: Int
- get() = (this * Resources.getSystem().displayMetrics.density).toInt()
-
-/** Gets the duration multiplier for general animations on the device
- * @see Settings.Global.ANIMATOR_DURATION_SCALE
- */
-val Context.animatorDurationScale: Float
- get() = Settings.Global.getFloat(this.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f)
-
-private const val TABLET_UI_REQUIRED_SCREEN_WIDTH_DP = 720
-
-fun Configuration.isTabletUi(): Boolean = smallestScreenWidthDp >= TABLET_UI_REQUIRED_SCREEN_WIDTH_DP
-
-/**
- * Returns true if current context is in night mode
- */
-fun Context.isNightMode(): Boolean = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
-
-/**
- * Creates night mode Context depending on reader theme/background
- *
- * Context wrapping method obtained from AppCompatDelegateImpl
- * https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImpl.java;l=348;drc=e28752c96fc3fb4d3354781469a1af3dbded4898
- */
-fun Context.createReaderThemeContext(): Context {
- val isDarkBackground = when (ReaderPreferences.readerTheme().get()) {
- 1, 2 -> true // Black, Gray
- 3 -> applicationContext.isNightMode() // Automatic bg uses activity background by default
- else -> false // White
- }
- val expected = if (isDarkBackground) Configuration.UI_MODE_NIGHT_YES else Configuration.UI_MODE_NIGHT_NO
- if (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK != expected) {
- val overrideConf = Configuration()
- overrideConf.setTo(resources.configuration)
- overrideConf.uiMode = overrideConf.uiMode and Configuration.UI_MODE_NIGHT_MASK.inv() or expected
-
- val wrappedContext = ContextThemeWrapper(this, R.style.AppTheme)
- wrappedContext.applyOverrideConfiguration(overrideConf)
- return wrappedContext
- }
- return this
-}
diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/util/view/ViewExtensions.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/util/view/ViewExtensions.kt
deleted file mode 100644
index e147065de..000000000
--- a/app/src/main/kotlin/eu/kanade/tachiyomi/util/view/ViewExtensions.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package eu.kanade.tachiyomi.util.view
-
-import android.content.res.Resources
-import android.graphics.Rect
-import android.view.View
-
-fun View?.isVisibleOnScreen(): Boolean {
- if (this == null) {
- return false
- }
- if (!this.isShown) {
- return false
- }
- val actualPosition = Rect()
- this.getGlobalVisibleRect(actualPosition)
- val screen = Rect(0, 0, Resources.getSystem().displayMetrics.widthPixels, Resources.getSystem().displayMetrics.heightPixels)
- return actualPosition.intersect(screen)
-}
diff --git a/app/src/main/res/layout/reader_activity.xml b/app/src/main/res/layout/reader_activity.xml
deleted file mode 100644
index cd96cb0c2..000000000
--- a/app/src/main/res/layout/reader_activity.xml
+++ /dev/null
@@ -1,38 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/docs/README/zh-cn.md b/docs/README/zh-cn.md
index 00be87dc0..6b7a2b254 100644
--- a/docs/README/zh-cn.md
+++ b/docs/README/zh-cn.md
@@ -93,8 +93,6 @@
- [libarchive](https://www.libarchive.org/)
- [jsoup](https://jsoup.org/)
-特别感谢[Tachiyomi](https://tachiyomi.org/)的阅读器实现
-
# 许可证
Copyright 2014-2019 Hippo Seven
diff --git a/docs/README/zh-tw.md b/docs/README/zh-tw.md
index c6a1d24d0..102bc7750 100644
--- a/docs/README/zh-tw.md
+++ b/docs/README/zh-tw.md
@@ -93,8 +93,6 @@
- [libarchive](https://www.libarchive.org/)
- [jsoup](https://jsoup.org/)
-特別感謝[Tachiyomi](https://tachiyomi.org/)的閱讀器實現
-
# 許可證書
Copyright 2014-2019 Hippo Seven
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 88a574ab7..36e2e53da 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -13,9 +13,7 @@ androidx-datastore = "1.1.1"
androidx-graphics-path = "1.0.1"
androidx-lifecycle = "2.8.4"
androidx-paging = "3.3.1"
-androidx-recyclerview = "1.4.0-alpha01"
androidx-room = "2.7.0-alpha05"
-androidx-viewpager2 = "1.1.0"
androidx-work = "2.10.0-alpha02"
arrow = "1.2.4"
coil = "3.0.0-alpha09"
@@ -55,13 +53,9 @@ androidx-lifecycle-compose = { module = "androidx.lifecycle:lifecycle-runtime-co
androidx-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "androidx-paging" }
-androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "androidx-recyclerview" }
-
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "androidx-room" }
androidx-room-paging = { module = "androidx.room:room-paging", version.ref = "androidx-room" }
-androidx-viewpager2 = { module = "androidx.viewpager2:viewpager2", version.ref = "androidx-viewpager2" }
-
androidx-work-runtime = { module = "androidx.work:work-runtime", version.ref = "androidx-work" }
arrow-fx-coroutines = { module = "io.arrow-kt:arrow-fx-coroutines" }
@@ -92,7 +86,6 @@ cronet-embedded = "org.chromium.net:cronet-embedded:119.6045.31"
desugar = "com.android.tools:desugar_jdk_libs_nio:2.0.4"
diff = "io.github.petertrr:kotlin-multiplatform-diff:0.7.0"
-insetter = "dev.chrisbanes.insetter:insetter:0.6.1"
jsoup = "org.jsoup:jsoup:1.18.1"
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
@@ -106,8 +99,6 @@ logcat = "com.squareup.logcat:logcat:0.1"
okio-jvm = { module = "com.squareup.okio:okio-jvm", version.ref = "okio" }
-photoview = "io.getstream:photoview:1.0.2"
-
reorderable = "sh.calvin.reorderable:reorderable:2.2.0"
serialization-cbor = { module = "org.jetbrains.kotlinx:kotlinx-serialization-cbor", version.ref = "kotlinx-serialization" }