Handle reader input and state in the activity (#4192)

This commit is contained in:
AntsyLich
2026-10-26 23:15:38 +00:00
committed by GitHub
parent 00335b2a70
commit 9079ad8c9a
18 changed files with 251 additions and 426 deletions
@@ -49,10 +49,10 @@ fun ReaderNavigationOverlay(
Canvas(modifier = Modifier.fillMaxSize()) {
regions.forEachIndexed { index, region ->
val rect = region.rectF
val rect = region.rect
val topLeft = Offset(size.width * rect.left, size.height * rect.top)
val regionSize = Size(size.width * rect.width(), size.height * rect.height())
drawRect(color = Color(region.type.color), topLeft = topLeft, size = regionSize)
val regionSize = Size(size.width * rect.width, size.height * rect.height)
drawRect(color = region.type.color, topLeft = topLeft, size = regionSize)
val text = textMeasurer.measure(labels[index], textStyle)
val textTopLeft = Offset(
@@ -8,13 +8,12 @@ import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.WindowManager
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.compose.foundation.background
import androidx.compose.foundation.focusable
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.layout.Arrangement
@@ -31,6 +30,7 @@ import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
@@ -39,9 +39,19 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.isCtrlPressed
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.keepScreenOn
@@ -84,6 +94,8 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsViewModel
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
import eu.kanade.tachiyomi.ui.reader.viewer.LocalPageColorFilter
import eu.kanade.tachiyomi.ui.reader.viewer.Viewer
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation.Action
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation.NavigationRegion
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.util.system.readerBackgroundColor
@@ -184,16 +196,33 @@ class ReaderActivity : BaseActivity() {
val colorOverlayEnabled by readerPreferences.colorFilter.collectAsState()
val colorOverlay by readerPreferences.colorFilterValue.collectAsState()
val colorOverlayMode by readerPreferences.colorFilterMode.collectAsState()
val brightness by settingsViewModel.brightness.collectAsState()
val navigation = settingsViewModel.navigation.collectAsState().value
val backgroundColor = remember(general.theme) { Color(baseContext.readerBackgroundColor(general.theme)) }
val colorOverlayBlendMode = remember(colorOverlayMode) {
ReaderPreferences.ColorFilterMode.getOrNull(colorOverlayMode)?.second
}
LaunchedEffect(brightness) { setWindowBrightness(brightness) }
val forceNavigationOverlay = remember { settingsViewModel.takeNavigationOverlayForNewUser() }
LaunchedEffect(navigation) {
if (navigation == null) return@LaunchedEffect
val showOnStart = forceNavigationOverlay || settingsViewModel.general.value.navigationOverlayOnStart
navigationOverlayState.setNavigation(navigation, showOnStart)
}
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { focusRequester.requestFocus() }
Box(
modifier = Modifier
.fillMaxSize()
.background(backgroundColor)
.onPreviewKeyEvent(::onKeyEvent)
.focusRequester(focusRequester)
.focusable()
.pointerInput(Unit) {
awaitEachGesture {
awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
@@ -210,7 +239,7 @@ class ReaderActivity : BaseActivity() {
}
ReaderContentOverlay(
brightness = state.brightnessOverlayValue,
brightness = brightness,
color = colorOverlay.takeIf { colorOverlayEnabled },
colorBlendMode = colorOverlayBlendMode,
)
@@ -234,23 +263,24 @@ class ReaderActivity : BaseActivity() {
AppBars(state = state)
}
if (state.isLoadingAdjacentChapter) {
AlertDialog(
onDismissRequest = {},
confirmButton = {},
text = {
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
CircularProgressIndicator()
Text(stringResource(MR.strings.loading))
}
},
)
}
val onDismissRequest = viewModel::closeDialog
when (state.dialog) {
is ReaderViewModel.Dialog.Loading -> {
AlertDialog(
onDismissRequest = {},
confirmButton = {},
text = {
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
CircularProgressIndicator()
Text(stringResource(MR.strings.loading))
}
},
)
}
is ReaderViewModel.Dialog.Settings -> {
ReaderSettingsDialog(
onDismissRequest = onDismissRequest,
@@ -304,17 +334,6 @@ class ReaderActivity : BaseActivity() {
Notifications.ID_NEW_CHAPTERS,
)
settingsViewModel.brightness
.onEach(::setCustomBrightnessValue)
.launchIn(lifecycleScope)
val forceNavigationOverlay = settingsViewModel.takeNavigationOverlayForNewUser()
settingsViewModel.navigation
.filterNotNull()
.onEach { navigation ->
val showOnStart = forceNavigationOverlay || settingsViewModel.general.value.navigationOverlayOnStart
navigationOverlayState.setNavigation(navigation, showOnStart)
}
.launchIn(lifecycleScope)
setMenuVisibility(viewModel.state.value.menuVisible)
// Finish when incognito mode is disabled
@@ -330,12 +349,6 @@ class ReaderActivity : BaseActivity() {
.onEach(::setInitialChapterError)
.launchIn(lifecycleScope)
viewModel.state
.map { it.isLoadingAdjacentChapter }
.distinctUntilChanged()
.onEach(::setProgressDialog)
.launchIn(lifecycleScope)
viewModel.state
.map { it.manga }
.distinctUntilChanged()
@@ -464,29 +477,66 @@ class ReaderActivity : BaseActivity() {
}
}
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
if (keyCode == KeyEvent.KEYCODE_N) {
loadNextChapter()
return true
} else if (keyCode == KeyEvent.KEYCODE_P) {
loadPreviousChapter()
return true
/**
* Handles a key [event] before anything else in the reader. Movement happens on key up.
*/
private fun onKeyEvent(event: KeyEvent): Boolean {
val viewer = viewer ?: return false
val navigation = settingsViewModel.navigation.value ?: return false
val isUp = event.type == KeyEventType.KeyUp
val action = when (event.key) {
Key.VolumeDown, Key.VolumeUp -> {
val general = settingsViewModel.general.value
if (!general.volumeKeysEnabled || viewModel.state.value.menuVisible) return false
if ((event.key == Key.VolumeDown) != general.volumeKeysInverted) Action.FORWARD else Action.BACKWARD
}
Key.DirectionRight -> if (event.isCtrlPressed) {
Action.FORWARD
} else {
navigation.actionOf(
NavigationRegion.RIGHT,
)
}
Key.DirectionLeft -> if (event.isCtrlPressed) {
Action.BACKWARD
} else {
navigation.actionOf(
NavigationRegion.LEFT,
)
}
Key.DirectionDown, Key.PageDown -> Action.FORWARD
Key.DirectionUp, Key.PageUp -> Action.BACKWARD
Key.Menu -> Action.MENU
Key.N -> {
if (isUp) loadNextChapter()
return true
}
Key.P -> {
if (isUp) loadPreviousChapter()
return true
}
else -> return false
}
return super.onKeyUp(keyCode, event)
}
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
val handled = viewer?.handleKeyEvent(event) ?: false
return handled || super.dispatchKeyEvent(event)
if (isUp) {
when (action) {
Action.MENU -> toggleMenu()
Action.FORWARD -> viewer.moveForward()
Action.BACKWARD -> viewer.moveBackward()
}
}
return true
}
/**
* Dispatches a generic motion event. If the viewer doesn't handle it, call the default
* implementation.
* Called from the viewer when it's tapped at [pos], given as a fraction of its size.
*/
override fun dispatchGenericMotionEvent(event: MotionEvent): Boolean {
val handled = viewer?.handleGenericMotionEvent(event) ?: false
return handled || super.dispatchGenericMotionEvent(event)
fun onViewerTap(pos: Offset) {
val action = settingsViewModel.navigation.value?.getAction(pos) ?: Action.MENU
when (action) {
Action.MENU -> toggleMenu()
Action.FORWARD -> viewer?.moveForward()
Action.BACKWARD -> viewer?.moveBackward()
}
}
@Composable
@@ -666,20 +716,6 @@ class ReaderActivity : BaseActivity() {
toast(error.message)
}
/**
* Called from the presenter whenever it's loading the next or previous chapter. It shows or
* dismisses a non-cancellable dialog to prevent user interaction according to the value of
* [show]. This is only used when the next/previous buttons on the toolbar are clicked; the
* other cases are handled with chapter transitions on the viewers and chapter preloading.
*/
private fun setProgressDialog(show: Boolean) {
if (show) {
viewModel.showLoadingDialog()
} else {
viewModel.closeDialog()
}
}
/**
* Moves the viewer to the given page [index]. It does nothing if the viewer is null or the
* page is not found.
@@ -825,11 +861,11 @@ class ReaderActivity : BaseActivity() {
/**
* Sets the brightness of the screen. Range is [-75, 100].
* From -75 to -1 a semi-transparent black view is overlaid with the minimum brightness.
* From -75 to -1 the minimum brightness is used, and the overlay darkens further.
* From 1 to 100 it sets that value as brightness.
* 0 sets system brightness and hides the overlay.
* 0 sets system brightness.
*/
private fun setCustomBrightnessValue(value: Int) {
private fun setWindowBrightness(value: Int) {
// Calculate and set reader brightness.
val readerBrightness = when {
value > 0 -> {
@@ -841,8 +877,6 @@ class ReaderActivity : BaseActivity() {
else -> WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
}
window.attributes = window.attributes.apply { screenBrightness = readerBrightness }
viewModel.setBrightnessOverlayValue(value)
}
}
@@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.reader
import android.content.Context
import android.net.Uri
import androidx.annotation.IntRange
import androidx.compose.runtime.Immutable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
@@ -913,10 +912,6 @@ class ReaderViewModel(
mutableState.update { it.copy(menuVisible = visible) }
}
fun showLoadingDialog() {
mutableState.update { it.copy(dialog = Dialog.Loading) }
}
fun openReadingModeSelectDialog() {
mutableState.update { it.copy(dialog = Dialog.ReadingModeSelect) }
}
@@ -937,10 +932,6 @@ class ReaderViewModel(
mutableState.update { it.copy(dialog = null) }
}
fun setBrightnessOverlayValue(value: Int) {
mutableState.update { it.copy(brightnessOverlayValue = value) }
}
/**
* Saves the image of the selected page on the pictures directory and notifies the UI of the result.
* There's also a notification to allow sharing the image somewhere else or deleting it.
@@ -1105,7 +1096,6 @@ class ReaderViewModel(
val currentPage: Int = -1,
val dialog: Dialog? = null,
val menuVisible: Boolean = false,
@IntRange(from = -100, to = 100) val brightnessOverlayValue: Int = 0,
) {
val currentChapter: ReaderChapter?
get() = viewerChapters?.currChapter
@@ -1115,7 +1105,6 @@ class ReaderViewModel(
}
sealed interface Dialog {
data object Loading : Dialog
data object Settings : Dialog
data object ReadingModeSelect : Dialog
data object OrientationModeSelect : Dialog
@@ -267,13 +267,14 @@ class ReaderSettingsViewModel(
} else {
paged.map { it.navigation }
}
val defaultNavigation: (ReaderPreferences.TappingInvertMode) -> ViewerNavigation =
val isReversed = viewerMode.readingMode == ReadingMode.RIGHT_TO_LEFT
val defaultNavigation: (ReaderPreferences.TappingInvertMode, Boolean) -> ViewerNavigation =
if (viewerMode.readingMode.direction == ReadingMode.Direction.Vertical) {
::LNavigation
} else {
::RightAndLeftNavigation
}
settings.distinctUntilChanged().map { it.navigator(defaultNavigation) }
settings.distinctUntilChanged().map { it.navigator(isReversed, defaultNavigation) }
}
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
@@ -1,11 +1,12 @@
package eu.kanade.tachiyomi.ui.reader.viewer
import android.view.KeyEvent
import android.view.MotionEvent
import androidx.compose.runtime.Composable
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.pointerInput
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
@@ -36,16 +37,14 @@ interface Viewer {
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.
* Moves forward through the pages.
*/
fun handleKeyEvent(event: KeyEvent): Boolean
fun moveForward()
/**
* Called from the containing activity when a generic motion [event] is received. It should
* return true if the event was handled, false otherwise.
* Moves backward through the pages.
*/
fun handleGenericMotionEvent(event: MotionEvent): Boolean
fun moveBackward()
}
/**
@@ -53,3 +52,22 @@ interface Viewer {
* theirs. Null when both are off.
*/
val LocalPageColorFilter = compositionLocalOf<ColorFilter?> { null }
/**
* Turns pages with the mouse wheel instead of letting the content scroll.
*/
fun Modifier.mouseWheelPaging(viewer: Viewer): Modifier = pointerInput(viewer) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent(PointerEventPass.Initial)
if (event.type != PointerEventType.Scroll) continue
val delta = event.changes.first().scrollDelta.y
when {
delta > 0 -> viewer.moveForward()
delta < 0 -> viewer.moveBackward()
else -> continue
}
event.changes.forEach { it.consume() }
}
}
}
@@ -1,37 +1,50 @@
package eu.kanade.tachiyomi.ui.reader.viewer
import android.graphics.Color
import android.graphics.PointF
import android.graphics.RectF
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.util.lang.invert
import tachiyomi.i18n.MR
abstract class ViewerNavigation(val invertMode: ReaderPreferences.TappingInvertMode) {
/**
* Tap zones of a viewer. [isReversed] is set for right to left books, where the right side of the
* screen goes back.
*/
abstract class ViewerNavigation(
val invertMode: ReaderPreferences.TappingInvertMode,
val isReversed: Boolean,
) {
sealed class NavigationRegion(val nameRes: StringResource, val color: Int) {
data object MENU : NavigationRegion(MR.strings.action_menu, Color.argb(0xCC, 0x95, 0x81, 0x8D))
data object PREV : NavigationRegion(MR.strings.nav_zone_prev, Color.argb(0xCC, 0xFF, 0x77, 0x33))
data object NEXT : NavigationRegion(MR.strings.nav_zone_next, Color.argb(0xCC, 0x84, 0xE2, 0x96))
data object LEFT : NavigationRegion(MR.strings.nav_zone_left, Color.argb(0xCC, 0x7D, 0x11, 0x28))
data object RIGHT : NavigationRegion(MR.strings.nav_zone_right, Color.argb(0xCC, 0xA6, 0xCF, 0xD5))
enum class Action { MENU, FORWARD, BACKWARD }
sealed class NavigationRegion(val nameRes: StringResource, val color: Color) {
data object MENU : NavigationRegion(MR.strings.action_menu, Color(0xCC95818D))
data object PREV : NavigationRegion(MR.strings.nav_zone_prev, Color(0xCCFF7733))
data object NEXT : NavigationRegion(MR.strings.nav_zone_next, Color(0xCC84E296))
data object LEFT : NavigationRegion(MR.strings.nav_zone_left, Color(0xCC7D1128))
data object RIGHT : NavigationRegion(MR.strings.nav_zone_right, Color(0xCCA6CFD5))
}
data class Region(
val rectF: RectF,
val rect: Rect,
val type: NavigationRegion,
) {
fun invert(invertMode: ReaderPreferences.TappingInvertMode): Region {
if (invertMode == ReaderPreferences.TappingInvertMode.NONE) return this
return this.copy(
rectF = this.rectF.invert(invertMode),
val horizontal = invertMode.shouldInvertHorizontal
val vertical = invertMode.shouldInvertVertical
if (!horizontal && !vertical) return this
return copy(
rect = Rect(
left = if (horizontal) 1f - rect.right else rect.left,
top = if (vertical) 1f - rect.bottom else rect.top,
right = if (horizontal) 1f - rect.left else rect.right,
bottom = if (vertical) 1f - rect.top else rect.bottom,
),
)
}
}
private var constantMenuRegion: RectF = RectF(0f, 0f, 1f, 0.05f)
protected abstract val regionList: List<Region>
/** Returns regions with applied inversion. */
@@ -39,19 +52,24 @@ abstract class ViewerNavigation(val invertMode: ReaderPreferences.TappingInvertM
return regionList.map { it.invert(invertMode) }
}
fun getAction(pos: PointF): NavigationRegion {
val x = pos.x
val y = pos.y
val region = getRegions().find { it.rectF.contains(x, y) }
return when {
region != null -> region.type
constantMenuRegion.contains(x, y) -> NavigationRegion.MENU
else -> NavigationRegion.MENU
}
fun getAction(pos: Offset): Action {
val region = getRegions().find { pos in it.rect } ?: return Action.MENU
return actionOf(region.type)
}
fun actionOf(region: NavigationRegion): Action = when (region) {
NavigationRegion.MENU -> Action.MENU
NavigationRegion.NEXT -> Action.FORWARD
NavigationRegion.PREV -> Action.BACKWARD
NavigationRegion.RIGHT -> if (isReversed) Action.BACKWARD else Action.FORWARD
NavigationRegion.LEFT -> if (isReversed) Action.FORWARD else Action.BACKWARD
}
override fun equals(other: Any?): Boolean =
other is ViewerNavigation && other.javaClass == javaClass && other.invertMode == invertMode
other is ViewerNavigation &&
other.javaClass == javaClass &&
other.invertMode == invertMode &&
other.isReversed == isReversed
override fun hashCode(): Int = 31 * javaClass.hashCode() + invertMode.hashCode()
override fun hashCode(): Int = (31 * javaClass.hashCode() + invertMode.hashCode()) * 31 + isReversed.hashCode()
}
@@ -19,14 +19,17 @@ data class NavigationSettings(
val inverted: ReaderPreferences.TappingInvertMode,
) {
fun navigator(default: (ReaderPreferences.TappingInvertMode) -> ViewerNavigation): ViewerNavigation {
fun navigator(
isReversed: Boolean,
default: (ReaderPreferences.TappingInvertMode, Boolean) -> ViewerNavigation,
): ViewerNavigation {
return when (mode) {
ReaderPreferences.NavigationMode.DEFAULT -> default(inverted)
ReaderPreferences.NavigationMode.L_SHAPED -> LNavigation(inverted)
ReaderPreferences.NavigationMode.KINDLISH -> KindlishNavigation(inverted)
ReaderPreferences.NavigationMode.EDGE -> EdgeNavigation(inverted)
ReaderPreferences.NavigationMode.RIGHT_AND_LEFT -> RightAndLeftNavigation(inverted)
ReaderPreferences.NavigationMode.DISABLED -> DisabledNavigation(inverted)
ReaderPreferences.NavigationMode.DEFAULT -> default(inverted, isReversed)
ReaderPreferences.NavigationMode.L_SHAPED -> LNavigation(inverted, isReversed)
ReaderPreferences.NavigationMode.KINDLISH -> KindlishNavigation(inverted, isReversed)
ReaderPreferences.NavigationMode.EDGE -> EdgeNavigation(inverted, isReversed)
ReaderPreferences.NavigationMode.RIGHT_AND_LEFT -> RightAndLeftNavigation(inverted, isReversed)
ReaderPreferences.NavigationMode.DISABLED -> DisabledNavigation(inverted, isReversed)
}
}
}
@@ -13,7 +13,10 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
* | M | M | M | N: Next
* +---+---+---+
*/
class DisabledNavigation(invertMode: TappingInvertMode) : ViewerNavigation(invertMode) {
class DisabledNavigation(
invertMode: TappingInvertMode,
isReversed: Boolean,
) : ViewerNavigation(invertMode, isReversed) {
override val regionList: List<Region> = emptyList()
}
@@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.ui.reader.viewer.navigation
import android.graphics.RectF
import androidx.compose.ui.geometry.Rect
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences.TappingInvertMode
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
@@ -14,19 +14,19 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
* | N | P | N | N: Next
* +---+---+---+
*/
class EdgeNavigation(invertMode: TappingInvertMode) : ViewerNavigation(invertMode) {
class EdgeNavigation(invertMode: TappingInvertMode, isReversed: Boolean) : ViewerNavigation(invertMode, isReversed) {
override val regionList: List<Region> = listOf(
Region(
rectF = RectF(0f, 0f, 0.33f, 1f),
rect = Rect(0f, 0f, 0.33f, 1f),
type = NavigationRegion.NEXT,
),
Region(
rectF = RectF(0.33f, 0.66f, 0.66f, 1f),
rect = Rect(0.33f, 0.66f, 0.66f, 1f),
type = NavigationRegion.PREV,
),
Region(
rectF = RectF(0.66f, 0f, 1f, 1f),
rect = Rect(0.66f, 0f, 1f, 1f),
type = NavigationRegion.NEXT,
),
)
@@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.ui.reader.viewer.navigation
import android.graphics.RectF
import androidx.compose.ui.geometry.Rect
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences.TappingInvertMode
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
@@ -14,15 +14,18 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
* | P | N | N | N: Next
* +---+---+---+
*/
class KindlishNavigation(invertMode: TappingInvertMode) : ViewerNavigation(invertMode) {
class KindlishNavigation(
invertMode: TappingInvertMode,
isReversed: Boolean,
) : ViewerNavigation(invertMode, isReversed) {
override val regionList: List<Region> = listOf(
Region(
rectF = RectF(0.33f, 0.33f, 1f, 1f),
rect = Rect(0.33f, 0.33f, 1f, 1f),
type = NavigationRegion.NEXT,
),
Region(
rectF = RectF(0f, 0.33f, 0.33f, 1f),
rect = Rect(0f, 0.33f, 0.33f, 1f),
type = NavigationRegion.PREV,
),
)
@@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.ui.reader.viewer.navigation
import android.graphics.RectF
import androidx.compose.ui.geometry.Rect
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences.TappingInvertMode
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
@@ -14,23 +14,23 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
* | N | N | N | N: Next
* +---+---+---+
*/
open class LNavigation(invertMode: TappingInvertMode) : ViewerNavigation(invertMode) {
open class LNavigation(invertMode: TappingInvertMode, isReversed: Boolean) : ViewerNavigation(invertMode, isReversed) {
override val regionList: List<Region> = listOf(
Region(
rectF = RectF(0f, 0.33f, 0.33f, 0.66f),
rect = Rect(0f, 0.33f, 0.33f, 0.66f),
type = NavigationRegion.PREV,
),
Region(
rectF = RectF(0f, 0f, 1f, 0.33f),
rect = Rect(0f, 0f, 1f, 0.33f),
type = NavigationRegion.PREV,
),
Region(
rectF = RectF(0.66f, 0.33f, 1f, 0.66f),
rect = Rect(0.66f, 0.33f, 1f, 0.66f),
type = NavigationRegion.NEXT,
),
Region(
rectF = RectF(0f, 0.66f, 1f, 1f),
rect = Rect(0f, 0.66f, 1f, 1f),
type = NavigationRegion.NEXT,
),
)
@@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.ui.reader.viewer.navigation
import android.graphics.RectF
import androidx.compose.ui.geometry.Rect
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences.TappingInvertMode
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
@@ -14,15 +14,18 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
* | N | M | P | N: Move Left
* +---+---+---+
*/
class RightAndLeftNavigation(invertMode: TappingInvertMode) : ViewerNavigation(invertMode) {
class RightAndLeftNavigation(
invertMode: TappingInvertMode,
isReversed: Boolean,
) : ViewerNavigation(invertMode, isReversed) {
override val regionList: List<Region> = listOf(
Region(
rectF = RectF(0f, 0f, 0.33f, 1f),
rect = Rect(0f, 0f, 0.33f, 1f),
type = NavigationRegion.LEFT,
),
Region(
rectF = RectF(0.66f, 0f, 1f, 1f),
rect = Rect(0.66f, 0f, 1f, 1f),
type = NavigationRegion.RIGHT,
),
)
@@ -2,16 +2,14 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager
import android.content.Context
import android.graphics.ColorFilter
import android.graphics.PointF
import android.view.InputDevice
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams
import android.widget.FrameLayout
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.asAndroidColorFilter
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.children
@@ -29,8 +27,8 @@ import eu.kanade.tachiyomi.ui.reader.viewer.LocalPageColorFilter
import eu.kanade.tachiyomi.ui.reader.viewer.PagedSettings
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView.ZoomStartPosition
import eu.kanade.tachiyomi.ui.reader.viewer.Viewer
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation.NavigationRegion
import eu.kanade.tachiyomi.ui.reader.viewer.collectChanges
import eu.kanade.tachiyomi.ui.reader.viewer.mouseWheelPaging
import eu.kanade.tachiyomi.ui.reader.viewer.position
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
@@ -135,6 +133,7 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer {
pager.isVisible = false // Don't layout the pager yet
pager.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
pager.isFocusable = false
pager.descendantFocusability = ViewGroup.FOCUS_BLOCK_DESCENDANTS
pager.offscreenPageLimit = 1
pager.id = R.id.reader_pager
pager.adapter = adapter
@@ -144,17 +143,11 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer {
pager.getLocationOnScreen(viewPosition)
val viewPositionRelativeToWindow = IntArray(2)
pager.getLocationInWindow(viewPositionRelativeToWindow)
val pos = PointF(
val pos = Offset(
(event.rawX - viewPosition[0] + viewPositionRelativeToWindow[0]) / pager.width,
(event.rawY - viewPosition[1] + viewPositionRelativeToWindow[1]) / pager.height,
)
when (activity.settingsViewModel.navigation.value?.getAction(pos) ?: NavigationRegion.MENU) {
NavigationRegion.MENU -> activity.toggleMenu()
NavigationRegion.NEXT -> moveToNext()
NavigationRegion.PREV -> moveToPrevious()
NavigationRegion.RIGHT -> moveRight()
NavigationRegion.LEFT -> moveLeft()
}
activity.onViewerTap(pos)
}
pager.longTapListener = f@{
if (activity.viewModel.state.value.menuVisible || general.longTapEnabled) {
@@ -212,7 +205,7 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer {
}
AndroidView(
factory = { context -> DisallowInterceptBarrier(context).apply { addView(pager) } },
modifier = modifier,
modifier = modifier.mouseWheelPaging(this),
)
}
@@ -367,23 +360,18 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer {
}
/**
* Moves to the next page.
* Whether pages run right to left, so moving forward goes left.
*/
open fun moveToNext() {
moveRight()
}
protected open val isReversed = false
/**
* Moves to the previous page.
*/
open fun moveToPrevious() {
moveLeft()
}
override fun moveForward() = if (isReversed) moveLeft() else moveRight()
override fun moveBackward() = if (isReversed) moveRight() else moveLeft()
/**
* Moves to the page at the right.
*/
protected open fun moveRight() {
private fun moveRight() {
if (pager.currentItem != adapter.count - 1) {
val holder = (currentPage as? ReaderPage)?.let(::getPageHolder)
if (holder != null && paged.navigateToPan && holder.canPanRight()) {
@@ -397,7 +385,7 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer {
/**
* Moves to the page at the left.
*/
protected open fun moveLeft() {
private fun moveLeft() {
if (pager.currentItem != 0) {
val holder = (currentPage as? ReaderPage)?.let(::getPageHolder)
if (holder != null && paged.navigateToPan && holder.canPanLeft()) {
@@ -408,20 +396,6 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer {
}
}
/**
* Moves to the page at the top (or previous).
*/
protected open fun moveUp() {
moveToPrevious()
}
/**
* Moves to the page at the bottom (or next).
*/
protected open fun moveDown() {
moveToNext()
}
/**
* Resets the adapter in order to recreate all the views. Used when a image configuration is
* changed.
@@ -433,69 +407,6 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer {
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
when (event.keyCode) {
KeyEvent.KEYCODE_VOLUME_DOWN -> {
if (!general.volumeKeysEnabled || activity.viewModel.state.value.menuVisible) {
return false
} else if (isUp) {
if (!general.volumeKeysInverted) moveDown() else moveUp()
}
}
KeyEvent.KEYCODE_VOLUME_UP -> {
if (!general.volumeKeysEnabled || activity.viewModel.state.value.menuVisible) {
return false
} else if (isUp) {
if (!general.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
}
fun onPageSplit(currentPage: ReaderPage, newPage: InsertPage) {
activity.runOnUiThread {
// Need to insert on UI thread else images will go blank
@@ -25,19 +25,7 @@ class R2LPagerViewer(activity: ReaderActivity) : PagerViewer(activity) {
return Pager(activity)
}
/**
* Moves to the next page. On a R2L pager the next page is the one at the left.
*/
override fun moveToNext() {
moveLeft()
}
/**
* Moves to the previous page. On a R2L pager the previous page is the one at the right.
*/
override fun moveToPrevious() {
moveRight()
}
override val isReversed = true
}
/**
@@ -1,12 +1,8 @@
package eu.kanade.tachiyomi.ui.reader.viewer.webgpu
import android.content.Context
import android.graphics.PointF
import android.graphics.Rect
import android.os.SystemClock
import android.view.InputDevice
import android.view.KeyEvent
import android.view.MotionEvent
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import ca.mpreg.imagedecoder.ImageDecoder
@@ -46,8 +42,8 @@ import eu.kanade.tachiyomi.ui.reader.viewer.LongStripSettings
import eu.kanade.tachiyomi.ui.reader.viewer.PagedSettings
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView.ZoomStartPosition
import eu.kanade.tachiyomi.ui.reader.viewer.Viewer
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation.NavigationRegion
import eu.kanade.tachiyomi.ui.reader.viewer.collectChanges
import eu.kanade.tachiyomi.ui.reader.viewer.mouseWheelPaging
import eu.kanade.tachiyomi.ui.reader.viewer.position
import eu.kanade.tachiyomi.util.system.createReaderThemeContext
import eu.kanade.tachiyomi.util.system.readerBackgroundColor
@@ -813,14 +809,7 @@ open class WebGpuViewer(
}
onTap = { offset ->
val region = activity.settingsViewModel.navigation.value?.getAction(PointF(offset.x, offset.y))
when (region ?: NavigationRegion.MENU) {
NavigationRegion.MENU -> activity.toggleMenu()
NavigationRegion.NEXT -> moveToNext()
NavigationRegion.PREV -> moveToPrevious()
NavigationRegion.RIGHT -> moveRight()
NavigationRegion.LEFT -> moveLeft()
}
activity.onViewerTap(offset)
}
onLongTap = { _ ->
@@ -1005,7 +994,7 @@ open class WebGpuViewer(
@Composable
override fun Content(modifier: Modifier) {
ImageViewer(modifier = modifier, state = viewerState)
ImageViewer(modifier = modifier.mouseWheelPaging(this), state = viewerState)
}
private val cropBorders: Boolean
@@ -1881,14 +1870,9 @@ open class WebGpuViewer(
viewerState.animatePageTurn(if (isReversed) direction else -direction)
}
// moveRight/moveLeft are screen directions, so a right-to-left book's next is to the left.
fun moveToNext() {
if (isReversed) moveLeft() else moveRight()
}
override fun moveForward() = if (isReversed) moveLeft() else moveRight()
fun moveToPrevious() {
if (isReversed) moveRight() else moveLeft()
}
override fun moveBackward() = if (isReversed) moveRight() else moveLeft()
protected open fun moveRight() {
viewerState.getPage(0)?.let { page ->
@@ -1952,68 +1936,4 @@ open class WebGpuViewer(
val target = currentPage?.let { nextPage(it, direction) } ?: return
moveToPage(target)
}
protected fun moveUp() {
moveToPrevious()
}
protected fun moveDown() {
moveToNext()
}
/**
* 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
when (event.keyCode) {
KeyEvent.KEYCODE_VOLUME_DOWN -> {
if (!general.volumeKeysEnabled || activity.viewModel.state.value.menuVisible) {
return false
} else if (isUp) {
if (!general.volumeKeysInverted) moveDown() else moveUp()
}
}
KeyEvent.KEYCODE_VOLUME_UP -> {
if (!general.volumeKeysEnabled || activity.viewModel.state.value.menuVisible) {
return false
} else if (isUp) {
if (!general.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
}
}
@@ -7,6 +7,7 @@ import ca.mpreg.webgpuviewer.viewer.ImageViewerContinuous
import ca.mpreg.webgpuviewer.viewer.ImageViewerContinuousState
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.viewer.mouseWheelPaging
import kotlinx.coroutines.launch
import kotlin.math.max
@@ -26,7 +27,7 @@ class WebGpuViewerContinuous(activity: ReaderActivity, val useGap: Boolean = fal
@Composable
override fun Content(modifier: Modifier) {
ImageViewerContinuous(modifier = modifier, state = state)
ImageViewerContinuous(modifier = modifier.mouseWheelPaging(this), state = state)
}
init {
@@ -1,15 +1,13 @@
package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
import android.graphics.ColorFilter
import android.graphics.PointF
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.asAndroidColorFilter
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.app.ActivityCompat
@@ -26,7 +24,6 @@ import eu.kanade.tachiyomi.ui.reader.viewer.LocalPageColorFilter
import eu.kanade.tachiyomi.ui.reader.viewer.LongStripSettings
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
import eu.kanade.tachiyomi.ui.reader.viewer.Viewer
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation.NavigationRegion
import eu.kanade.tachiyomi.ui.reader.viewer.collectChanges
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
@@ -139,15 +136,11 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
recycler.getLocationOnScreen(viewPosition)
val viewPositionRelativeToWindow = IntArray(2)
recycler.getLocationInWindow(viewPositionRelativeToWindow)
val pos = PointF(
val pos = Offset(
(event.rawX - viewPosition[0] + viewPositionRelativeToWindow[0]) / recycler.width,
(event.rawY - viewPosition[1] + viewPositionRelativeToWindow[1]) / recycler.originalHeight,
)
when (activity.settingsViewModel.navigation.value?.getAction(pos) ?: NavigationRegion.MENU) {
NavigationRegion.MENU -> activity.toggleMenu()
NavigationRegion.NEXT, NavigationRegion.RIGHT -> scrollDown()
NavigationRegion.PREV, NavigationRegion.LEFT -> scrollUp()
}
activity.onViewerTap(pos)
}
recycler.longTapListener = f@{ event ->
if (activity.viewModel.state.value.menuVisible || general.longTapEnabled) {
@@ -188,6 +181,7 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
}
frame.layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
frame.descendantFocusability = ViewGroup.FOCUS_BLOCK_DESCENDANTS
frame.addView(recycler)
}
@@ -322,7 +316,7 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
/**
* Scrolls up by [scrollDistance].
*/
private fun scrollUp() {
override fun moveBackward() {
if (general.usePageTransitions) {
recycler.smoothScrollBy(0, -scrollDistance)
} else {
@@ -333,7 +327,7 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
/**
* Scrolls down by [scrollDistance].
*/
private fun scrollDown() {
override fun moveForward() {
if (general.usePageTransitions) {
recycler.smoothScrollBy(0, scrollDistance)
} else {
@@ -341,52 +335,6 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
}
}
/**
* 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
when (event.keyCode) {
KeyEvent.KEYCODE_VOLUME_DOWN -> {
if (!general.volumeKeysEnabled || activity.viewModel.state.value.menuVisible) {
return false
} else if (isUp) {
if (!general.volumeKeysInverted) scrollDown() else scrollUp()
}
}
KeyEvent.KEYCODE_VOLUME_UP -> {
if (!general.volumeKeysEnabled || activity.viewModel.state.value.menuVisible) {
return false
} else if (isUp) {
if (!general.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 {
return false
}
/**
* Notifies adapter of changes around the current page to trigger a relayout in the recycler.
* Used when an image configuration is changed.
@@ -1,15 +0,0 @@
package eu.kanade.tachiyomi.util.lang
import android.graphics.RectF
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
fun RectF.invert(invertMode: ReaderPreferences.TappingInvertMode): RectF {
val horizontal = invertMode.shouldInvertHorizontal
val vertical = invertMode.shouldInvertVertical
return when {
horizontal && vertical -> RectF(1f - this.right, 1f - this.bottom, 1f - this.left, 1f - this.top)
vertical -> RectF(this.left, 1f - this.bottom, this.right, 1f - this.top)
horizontal -> RectF(1f - this.right, this.top, 1f - this.left, this.bottom)
else -> this
}
}