Don't let navigation overlay consume click event
So it won't take two actions to dismiss overlay & app bar.
This commit is contained in:
1 parent
94b138f254
commit
d23a453f71
7 files changed
+436
-353
No files matched your search
@@ -0,0 +1,23 @@
|
||||
package com.hippo.ehviewer.ui.reader
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import com.hippo.ehviewer.Settings
|
||||
import me.saket.telephoto.zoomable.DoubleClickToZoomListener
|
||||
import me.saket.telephoto.zoomable.ZoomableState
|
||||
|
||||
object DoubleTapZoom : DoubleClickToZoomListener {
|
||||
override suspend fun onDoubleClick(state: ZoomableState, centroid: Offset) {
|
||||
if (Settings.doubleTapToZoom.value) {
|
||||
val zoomFraction = state.zoomFraction ?: return // Content isn't ready yet
|
||||
if (zoomFraction > 0.05f) {
|
||||
state.resetZoom()
|
||||
} else {
|
||||
// Workaround for https://github.com/saket/telephoto/issues/45
|
||||
state.zoomTo(
|
||||
zoomFactor = state.contentTransformation.scaleMetadata.initialScale.scaleX * 2f,
|
||||
centroid = centroid,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,61 +1,28 @@
|
||||
package com.hippo.ehviewer.ui.reader
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.pager.VerticalPager
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.FixedScale
|
||||
import androidx.compose.ui.layout.times
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.toSize
|
||||
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.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||
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
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import me.saket.telephoto.zoomable.DoubleClickToZoomListener
|
||||
import me.saket.telephoto.zoomable.ZoomSpec
|
||||
import me.saket.telephoto.zoomable.ZoomableContentLocation
|
||||
import me.saket.telephoto.zoomable.ZoomableState
|
||||
import me.saket.telephoto.zoomable.rememberZoomableState
|
||||
import me.saket.telephoto.zoomable.zoomable
|
||||
|
||||
@Composable
|
||||
fun GalleryPager(
|
||||
@@ -68,304 +35,66 @@ fun GalleryPager(
|
||||
modifier: Modifier = Modifier,
|
||||
contentPadding: PaddingValues = PaddingValues(0.dp),
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val items = pageLoader.pages
|
||||
val isPagerType = !ReadingModeType.isWebtoon(type)
|
||||
val pagerNavigation by Settings.readerPagerNav.collectAsState()
|
||||
val pagerInvertMode by Settings.readerPagerNavInverted.collectAsState {
|
||||
TappingInvertMode.entries[it]
|
||||
}
|
||||
val pagerInvertMode by Settings.readerPagerNavInverted.collectAsState()
|
||||
val webtoonNavigation by Settings.readerWebtoonNav.collectAsState()
|
||||
val webtoonInvertMode by Settings.readerWebtoonNavInverted.collectAsState {
|
||||
TappingInvertMode.entries[it]
|
||||
}
|
||||
val navigationType = if (isPagerType) {
|
||||
pagerNavigation
|
||||
} else {
|
||||
webtoonNavigation
|
||||
}
|
||||
val webtoonInvertMode by Settings.readerWebtoonNavInverted.collectAsState()
|
||||
val navigationType = if (isPagerType) pagerNavigation else webtoonNavigation
|
||||
val navigation = remember(navigationType, type) {
|
||||
ViewerNavigation.fromPreference(navigationType, ReadingModeType.isVertical(type))
|
||||
}
|
||||
navigation.invertMode = if (isPagerType) {
|
||||
pagerInvertMode
|
||||
} else {
|
||||
webtoonInvertMode
|
||||
val invertMode = if (isPagerType) pagerInvertMode else webtoonInvertMode
|
||||
var firstLaunch by remember { mutableStateOf(true) }
|
||||
var showNavigationOverlay by remember {
|
||||
val showOnStart = Settings.showNavigationOverlayNewUser.value || Settings.showNavigationOverlayOnStart.value
|
||||
Settings.showNavigationOverlayNewUser.value = false
|
||||
mutableStateOf(showOnStart)
|
||||
}
|
||||
val regions = remember(navigation, navigation.invertMode) { navigation.regions }
|
||||
val navigatorState = rememberUpdatedState(regions)
|
||||
if (isPagerType) {
|
||||
val isVertical = type == VERTICAL
|
||||
val scaleType by Settings.imageScaleType.collectAsState()
|
||||
val landscapeZoom by Settings.landscapeZoom.collectAsState()
|
||||
val zoomStart by Settings.zoomStart.collectAsState()
|
||||
val alignment = Alignment.fromPreferences(zoomStart, type)
|
||||
val layoutSize by remember(pagerState) {
|
||||
derivedStateOf {
|
||||
pagerState.layoutInfo.viewportSize.toSize()
|
||||
}
|
||||
}
|
||||
if (isVertical) {
|
||||
VerticalPager(
|
||||
state = pagerState,
|
||||
modifier = modifier,
|
||||
contentPadding = contentPadding,
|
||||
key = { it },
|
||||
) { index ->
|
||||
val page = items[index]
|
||||
PageContainer(
|
||||
page = page,
|
||||
pageLoader = pageLoader,
|
||||
isRtl = false,
|
||||
scaleType = scaleType,
|
||||
landscapeZoom = landscapeZoom,
|
||||
alignment = alignment,
|
||||
layoutSize = layoutSize,
|
||||
navigatorState = navigatorState,
|
||||
pagerState = pagerState,
|
||||
onSelectPage = onSelectPage,
|
||||
onMenuRegionClick = onMenuRegionClick,
|
||||
scope = scope,
|
||||
)
|
||||
}
|
||||
val regions = remember(navigation, invertMode) {
|
||||
if (firstLaunch) {
|
||||
firstLaunch = false
|
||||
} else {
|
||||
val isRtl = type == RIGHT_TO_LEFT
|
||||
val isRtlLayout = LocalLayoutDirection.current == LayoutDirection.Rtl
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = modifier,
|
||||
contentPadding = contentPadding,
|
||||
reverseLayout = isRtl xor isRtlLayout,
|
||||
key = { it },
|
||||
) { index ->
|
||||
val page = items[index]
|
||||
PageContainer(
|
||||
page = page,
|
||||
pageLoader = pageLoader,
|
||||
isRtl = isRtl,
|
||||
scaleType = scaleType,
|
||||
landscapeZoom = landscapeZoom,
|
||||
alignment = alignment,
|
||||
layoutSize = layoutSize,
|
||||
navigatorState = navigatorState,
|
||||
pagerState = pagerState,
|
||||
onSelectPage = onSelectPage,
|
||||
onMenuRegionClick = onMenuRegionClick,
|
||||
scope = scope,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val zoomableState = rememberZoomableState(zoomSpec = ZoomSpec)
|
||||
val density = LocalDensity.current
|
||||
val paddingPercent by Settings.webtoonSidePadding.collectAsState()
|
||||
val sidePadding by remember(density) {
|
||||
snapshotFlow {
|
||||
with(density) {
|
||||
(lazyListState.layoutInfo.viewportSize.width * paddingPercent / 100f).toDp()
|
||||
}
|
||||
}
|
||||
}.collectAsState(0.dp)
|
||||
LazyColumn(
|
||||
modifier = modifier.zoomable(
|
||||
state = zoomableState,
|
||||
onClick = {
|
||||
scope.launch {
|
||||
with(lazyListState) {
|
||||
val size = lazyListState.layoutInfo.viewportSize
|
||||
val x = it.x / size.width
|
||||
val y = it.y / size.height
|
||||
when (navigatorState.value.getAction(x, y)) {
|
||||
NavigationRegion.MENU -> onMenuRegionClick()
|
||||
NavigationRegion.NEXT, NavigationRegion.RIGHT -> scrollDown()
|
||||
NavigationRegion.PREV, NavigationRegion.LEFT -> scrollUp()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onLongClick = { ofs ->
|
||||
val info = lazyListState.layoutInfo.visibleItemsInfo.find { info ->
|
||||
info.offset <= ofs.y && info.offset + info.size > ofs.y
|
||||
}
|
||||
if (info == null && type == CONTINUOUS_VERTICAL) {
|
||||
// Maybe user long-click on gaps? ¯\_(ツ)_/¯
|
||||
return@zoomable
|
||||
}
|
||||
info ?: error("Internal error finding long click item!!! offset:$ofs")
|
||||
onSelectPage(items[info.index])
|
||||
},
|
||||
onDoubleClick = DoubleTapZoom,
|
||||
),
|
||||
state = lazyListState,
|
||||
contentPadding = contentPadding + PaddingValues(horizontal = sidePadding),
|
||||
verticalArrangement = Arrangement.spacedBy(if (type != WEBTOON) 15.dp else 0.dp),
|
||||
) {
|
||||
items(items, key = { it.index }) { page ->
|
||||
PagerItem(
|
||||
page = page,
|
||||
pageLoader = pageLoader,
|
||||
contentScale = ContentScale.FillWidth,
|
||||
)
|
||||
}
|
||||
showNavigationOverlay = true
|
||||
}
|
||||
navigation.invertMode = TappingInvertMode.entries[invertMode]
|
||||
navigation.regions
|
||||
}
|
||||
val showOnStart = remember {
|
||||
Settings.showNavigationOverlayNewUser.value || Settings.showNavigationOverlayOnStart.value
|
||||
}
|
||||
NavigationOverlay(
|
||||
regions,
|
||||
showOnStart,
|
||||
onDismiss = { Settings.showNavigationOverlayNewUser.value = false },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PageContainer(
|
||||
page: ReaderPage,
|
||||
pageLoader: PageLoader2,
|
||||
isRtl: Boolean,
|
||||
scaleType: Int,
|
||||
landscapeZoom: Boolean,
|
||||
alignment: Alignment.Horizontal,
|
||||
layoutSize: Size,
|
||||
navigatorState: State<NavigationRegions>,
|
||||
pagerState: PagerState,
|
||||
onSelectPage: (ReaderPage) -> Unit,
|
||||
onMenuRegionClick: () -> Unit,
|
||||
scope: CoroutineScope,
|
||||
) {
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val isRtl by rememberUpdatedState(isRtl)
|
||||
val zoomableState = rememberZoomableState(zoomSpec = PagerZoomSpec)
|
||||
val status by page.status.collectAsState()
|
||||
if (status == Page.State.READY && layoutSize != Size.Zero) {
|
||||
val size = page.image!!.rect.size.toSize()
|
||||
val contentScale = ContentScale.fromPreferences(scaleType, size, layoutSize)
|
||||
zoomableState.contentScale = contentScale
|
||||
LaunchedEffect(size, contentScale, alignment) {
|
||||
val contentSize = if (contentScale is FixedScale) { // Original
|
||||
size
|
||||
} else {
|
||||
size * contentScale.computeScaleFactor(size, layoutSize)
|
||||
}
|
||||
val horizontalAlignment = if (contentSize.width > layoutSize.width) {
|
||||
alignment
|
||||
} else {
|
||||
Alignment.CenterHorizontally
|
||||
}
|
||||
val verticalAlignment = if (contentSize.height > layoutSize.height) {
|
||||
Alignment.Top
|
||||
} else {
|
||||
Alignment.CenterVertically
|
||||
}
|
||||
zoomableState.contentAlignment = horizontalAlignment + verticalAlignment
|
||||
}
|
||||
LaunchedEffect(size) {
|
||||
val contentLocation = ZoomableContentLocation.scaledInsideAndCenterAligned(size)
|
||||
zoomableState.setContentLocation(contentLocation)
|
||||
}
|
||||
if (landscapeZoom && contentScale == ContentScale.Fit && size.width > size.height) {
|
||||
LaunchedEffect(alignment) {
|
||||
val zoomFraction = snapshotFlow { zoomableState.zoomFraction }.first { it != null }
|
||||
if (zoomFraction == 0f) {
|
||||
delay(500)
|
||||
val contentSize = zoomableState.transformedContentBounds.size
|
||||
val scale = ContentScale.FillHeight.computeScaleFactor(contentSize, layoutSize)
|
||||
val targetScale = scale.scaleX.coerceAtMost(zoomableState.zoomSpec.maxZoomFactor)
|
||||
val offset = alignment.align(0, layoutSize.width.toInt(), LayoutDirection.Ltr)
|
||||
zoomableState.zoomTo(targetScale, Offset(offset.toFloat(), 0f))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
PagerItem(
|
||||
page = page,
|
||||
val navigator by rememberUpdatedState(regions)
|
||||
if (isPagerType) {
|
||||
PagerViewer(
|
||||
pagerState = pagerState,
|
||||
isRtl = type == RIGHT_TO_LEFT,
|
||||
isVertical = type == VERTICAL,
|
||||
pageLoader = pageLoader,
|
||||
contentScale = ContentScale.Inside,
|
||||
modifier = Modifier.zoomable(
|
||||
state = zoomableState,
|
||||
onClick = {
|
||||
scope.launch {
|
||||
with(pagerState) {
|
||||
with(zoomableState) {
|
||||
val x = it.x / layoutSize.width
|
||||
val y = it.y / layoutSize.height
|
||||
val distance = layoutSize.width
|
||||
when (navigatorState.value.getAction(x, y)) {
|
||||
NavigationRegion.MENU -> onMenuRegionClick()
|
||||
NavigationRegion.NEXT -> {
|
||||
val canPan = if (isRtl) panRight(distance) else panLeft(distance)
|
||||
if (!canPan) {
|
||||
moveToNext()
|
||||
}
|
||||
}
|
||||
|
||||
NavigationRegion.PREV -> {
|
||||
val canPan = if (isRtl) panLeft(distance) else panRight(distance)
|
||||
if (!canPan) {
|
||||
moveToPrevious()
|
||||
}
|
||||
}
|
||||
|
||||
NavigationRegion.RIGHT -> {
|
||||
if (!panLeft(distance)) {
|
||||
if (isRtl) moveToPrevious() else moveToNext()
|
||||
}
|
||||
}
|
||||
|
||||
NavigationRegion.LEFT -> {
|
||||
if (!panRight(distance)) {
|
||||
if (isRtl) moveToNext() else moveToPrevious()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onLongClick = { onSelectPage(page) },
|
||||
onDoubleClick = DoubleTapZoom,
|
||||
),
|
||||
navigator = { navigator },
|
||||
onClick = { showNavigationOverlay = false },
|
||||
onSelectPage = onSelectPage,
|
||||
onMenuRegionClick = onMenuRegionClick,
|
||||
contentPadding = contentPadding,
|
||||
modifier = modifier,
|
||||
)
|
||||
} else {
|
||||
WebtoonViewer(
|
||||
lazyListState = lazyListState,
|
||||
withGaps = type == CONTINUOUS_VERTICAL,
|
||||
pageLoader = pageLoader,
|
||||
navigator = { navigator },
|
||||
onClick = { showNavigationOverlay = false },
|
||||
onSelectPage = onSelectPage,
|
||||
onMenuRegionClick = onMenuRegionClick,
|
||||
contentPadding = contentPadding,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun NavigationRegions.getAction(x: Float, y: Float) =
|
||||
find { it.rectF.contains(x, y) }?.type ?: NavigationRegion.MENU
|
||||
|
||||
suspend fun ZoomableState.panLeft(distance: Float): Boolean {
|
||||
val canPan = Settings.navigateToPan.value && transformedContentBounds.right > distance
|
||||
if (canPan) {
|
||||
panBy(Offset(-distance, 0f))
|
||||
}
|
||||
return canPan
|
||||
}
|
||||
|
||||
suspend fun ZoomableState.panRight(distance: Float): Boolean {
|
||||
val canPan = Settings.navigateToPan.value && transformedContentBounds.left < 0
|
||||
if (canPan) {
|
||||
panBy(Offset(distance, 0f))
|
||||
}
|
||||
return canPan
|
||||
}
|
||||
|
||||
object DoubleTapZoom : DoubleClickToZoomListener {
|
||||
override suspend fun onDoubleClick(state: ZoomableState, centroid: Offset) {
|
||||
if (Settings.doubleTapToZoom.value) {
|
||||
val zoomFraction = state.zoomFraction ?: return // Content isn't ready yet
|
||||
if (zoomFraction > 0.05f) {
|
||||
state.resetZoom()
|
||||
} else {
|
||||
// Workaround for https://github.com/saket/telephoto/issues/45
|
||||
state.zoomTo(
|
||||
zoomFactor = state.contentTransformation.scaleMetadata.initialScale.scaleX * 2f,
|
||||
centroid = centroid,
|
||||
)
|
||||
}
|
||||
LaunchedEffect(isPagerType) {
|
||||
if (isPagerType) {
|
||||
pagerState.interactionSource
|
||||
} else {
|
||||
lazyListState.interactionSource
|
||||
}.interactions.collect {
|
||||
showNavigationOverlay = false
|
||||
}
|
||||
}
|
||||
NavigationOverlay(showNavigationOverlay, regions, modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
|
||||
private val PagerZoomSpec = ZoomSpec(maxZoomFactor = 5f)
|
||||
private val ZoomSpec = ZoomSpec(maxZoomFactor = 3f)
|
||||
@@ -5,20 +5,13 @@ import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -26,34 +19,20 @@ import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.NavigationRegions
|
||||
|
||||
@Composable
|
||||
fun NavigationOverlay(
|
||||
visible: Boolean,
|
||||
regions: NavigationRegions,
|
||||
showOnStart: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var firstLaunch by remember { mutableStateOf(true) }
|
||||
var visible by remember { mutableStateOf(showOnStart) }
|
||||
LaunchedEffect(regions) {
|
||||
if (firstLaunch) {
|
||||
firstLaunch = false
|
||||
} else {
|
||||
visible = true
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = visible && regions.isNotEmpty(),
|
||||
modifier = modifier.clickable(interactionSource = null, indication = null) {
|
||||
onDismiss()
|
||||
visible = false
|
||||
}.fillMaxSize(),
|
||||
enter = fadeIn(AnimationSpec),
|
||||
exit = fadeOut(AnimationSpec),
|
||||
) {
|
||||
BoxWithConstraints {
|
||||
BoxWithConstraints(modifier) {
|
||||
val fontSize = MaterialTheme.typography.headlineSmall.fontSize
|
||||
val style = TextStyle(
|
||||
color = Color.White,
|
||||
@@ -83,6 +62,4 @@ fun NavigationOverlay(
|
||||
}
|
||||
}
|
||||
|
||||
typealias NavigationRegions = List<ViewerNavigation.Region>
|
||||
|
||||
private val AnimationSpec = tween<Float>(1000)
|
||||
@@ -0,0 +1,252 @@
|
||||
package com.hippo.ehviewer.ui.reader
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.pager.VerticalPager
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.FixedScale
|
||||
import androidx.compose.ui.layout.times
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.toSize
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.collectAsState
|
||||
import com.hippo.ehviewer.gallery.PageLoader2
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.NavigationRegions
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation.NavigationRegion
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.getAction
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import me.saket.telephoto.zoomable.ZoomableContentLocation
|
||||
import me.saket.telephoto.zoomable.ZoomableState
|
||||
import me.saket.telephoto.zoomable.rememberZoomableState
|
||||
import me.saket.telephoto.zoomable.zoomable
|
||||
|
||||
@Composable
|
||||
fun PagerViewer(
|
||||
pagerState: PagerState,
|
||||
isRtl: Boolean,
|
||||
isVertical: Boolean,
|
||||
pageLoader: PageLoader2,
|
||||
navigator: () -> NavigationRegions,
|
||||
onClick: () -> Unit,
|
||||
onSelectPage: (ReaderPage) -> Unit,
|
||||
onMenuRegionClick: () -> Unit,
|
||||
contentPadding: PaddingValues,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val items = pageLoader.pages
|
||||
val scaleType by Settings.imageScaleType.collectAsState()
|
||||
val landscapeZoom by Settings.landscapeZoom.collectAsState()
|
||||
val zoomStart by Settings.zoomStart.collectAsState()
|
||||
val alignment = Alignment.fromPreferences(zoomStart, isRtl, isVertical)
|
||||
val layoutSize by remember(pagerState) {
|
||||
derivedStateOf {
|
||||
pagerState.layoutInfo.viewportSize.toSize()
|
||||
}
|
||||
}
|
||||
if (isVertical) {
|
||||
VerticalPager(
|
||||
state = pagerState,
|
||||
modifier = modifier,
|
||||
contentPadding = contentPadding,
|
||||
key = { it },
|
||||
) { index ->
|
||||
val page = items[index]
|
||||
PageContainer(
|
||||
page = page,
|
||||
pageLoader = pageLoader,
|
||||
isRtl = false,
|
||||
scaleType = scaleType,
|
||||
landscapeZoom = landscapeZoom,
|
||||
alignment = alignment,
|
||||
layoutSize = layoutSize,
|
||||
navigator = navigator,
|
||||
pagerState = pagerState,
|
||||
onSelectPage = onSelectPage,
|
||||
onClick = onClick,
|
||||
onMenuRegionClick = onMenuRegionClick,
|
||||
scope = scope,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val isRtlLayout = LocalLayoutDirection.current == LayoutDirection.Rtl
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = modifier,
|
||||
contentPadding = contentPadding,
|
||||
reverseLayout = isRtl xor isRtlLayout,
|
||||
key = { it },
|
||||
) { index ->
|
||||
val page = items[index]
|
||||
PageContainer(
|
||||
page = page,
|
||||
pageLoader = pageLoader,
|
||||
isRtl = isRtl,
|
||||
scaleType = scaleType,
|
||||
landscapeZoom = landscapeZoom,
|
||||
alignment = alignment,
|
||||
layoutSize = layoutSize,
|
||||
navigator = navigator,
|
||||
pagerState = pagerState,
|
||||
onSelectPage = onSelectPage,
|
||||
onClick = onClick,
|
||||
onMenuRegionClick = onMenuRegionClick,
|
||||
scope = scope,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PageContainer(
|
||||
page: ReaderPage,
|
||||
pageLoader: PageLoader2,
|
||||
isRtl: Boolean,
|
||||
scaleType: Int,
|
||||
landscapeZoom: Boolean,
|
||||
alignment: Alignment.Horizontal,
|
||||
layoutSize: Size,
|
||||
navigator: () -> NavigationRegions,
|
||||
pagerState: PagerState,
|
||||
onClick: () -> Unit,
|
||||
onSelectPage: (ReaderPage) -> Unit,
|
||||
onMenuRegionClick: () -> Unit,
|
||||
scope: CoroutineScope,
|
||||
) {
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val isRtl by rememberUpdatedState(isRtl)
|
||||
val zoomableState = rememberZoomableState(zoomSpec = zoomSpec)
|
||||
val status by page.status.collectAsState()
|
||||
if (status == Page.State.READY && layoutSize != Size.Zero) {
|
||||
val size = page.image!!.rect.size.toSize()
|
||||
val contentScale = ContentScale.fromPreferences(scaleType, size, layoutSize)
|
||||
zoomableState.contentScale = contentScale
|
||||
LaunchedEffect(size, contentScale, alignment) {
|
||||
val contentSize = if (contentScale is FixedScale) { // Original
|
||||
size
|
||||
} else {
|
||||
size * contentScale.computeScaleFactor(size, layoutSize)
|
||||
}
|
||||
val horizontalAlignment = if (contentSize.width > layoutSize.width) {
|
||||
alignment
|
||||
} else {
|
||||
Alignment.CenterHorizontally
|
||||
}
|
||||
val verticalAlignment = if (contentSize.height > layoutSize.height) {
|
||||
Alignment.Top
|
||||
} else {
|
||||
Alignment.CenterVertically
|
||||
}
|
||||
zoomableState.contentAlignment = horizontalAlignment + verticalAlignment
|
||||
}
|
||||
LaunchedEffect(size) {
|
||||
val contentLocation = ZoomableContentLocation.scaledInsideAndCenterAligned(size)
|
||||
zoomableState.setContentLocation(contentLocation)
|
||||
}
|
||||
if (landscapeZoom && contentScale == ContentScale.Fit && size.width > size.height) {
|
||||
LaunchedEffect(alignment) {
|
||||
val zoomFraction = snapshotFlow { zoomableState.zoomFraction }.first { it != null }
|
||||
if (zoomFraction == 0f) {
|
||||
delay(500)
|
||||
val contentSize = zoomableState.transformedContentBounds.size
|
||||
val scale = ContentScale.FillHeight.computeScaleFactor(contentSize, layoutSize)
|
||||
val targetScale = scale.scaleX.coerceAtMost(zoomableState.zoomSpec.maxZoomFactor)
|
||||
val offset = alignment.align(0, layoutSize.width.toInt(), LayoutDirection.Ltr)
|
||||
zoomableState.zoomTo(targetScale, Offset(offset.toFloat(), 0f))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
PagerItem(
|
||||
page = page,
|
||||
pageLoader = pageLoader,
|
||||
contentScale = ContentScale.Inside,
|
||||
modifier = Modifier.zoomable(
|
||||
state = zoomableState,
|
||||
onClick = {
|
||||
scope.launch {
|
||||
onClick()
|
||||
with(pagerState) {
|
||||
with(zoomableState) {
|
||||
val x = it.x / layoutSize.width
|
||||
val y = it.y / layoutSize.height
|
||||
val distance = layoutSize.width
|
||||
when (navigator().getAction(x, y)) {
|
||||
NavigationRegion.MENU -> onMenuRegionClick()
|
||||
NavigationRegion.NEXT -> {
|
||||
val canPan = if (isRtl) panRight(distance) else panLeft(distance)
|
||||
if (!canPan) {
|
||||
moveToNext()
|
||||
}
|
||||
}
|
||||
|
||||
NavigationRegion.PREV -> {
|
||||
val canPan = if (isRtl) panLeft(distance) else panRight(distance)
|
||||
if (!canPan) {
|
||||
moveToPrevious()
|
||||
}
|
||||
}
|
||||
|
||||
NavigationRegion.RIGHT -> {
|
||||
if (!panLeft(distance)) {
|
||||
if (isRtl) moveToPrevious() else moveToNext()
|
||||
}
|
||||
}
|
||||
|
||||
NavigationRegion.LEFT -> {
|
||||
if (!panRight(distance)) {
|
||||
if (isRtl) moveToNext() else moveToPrevious()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onLongClick = { onSelectPage(page) },
|
||||
onDoubleClick = DoubleTapZoom,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun ZoomableState.panLeft(distance: Float): Boolean {
|
||||
val canPan = Settings.navigateToPan.value && transformedContentBounds.right > distance
|
||||
if (canPan) {
|
||||
panBy(Offset(-distance, 0f))
|
||||
}
|
||||
return canPan
|
||||
}
|
||||
|
||||
private suspend fun ZoomableState.panRight(distance: Float): Boolean {
|
||||
val canPan = Settings.navigateToPan.value && transformedContentBounds.left < 0
|
||||
if (canPan) {
|
||||
panBy(Offset(distance, 0f))
|
||||
}
|
||||
return canPan
|
||||
}
|
||||
|
||||
private val zoomSpec = me.saket.telephoto.zoomable.ZoomSpec(maxZoomFactor = 5f)
|
||||
@@ -17,9 +17,6 @@ import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.FixedScale
|
||||
import com.hippo.ehviewer.Settings
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType.RIGHT_TO_LEFT
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType.VERTICAL
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
|
||||
fun Window.updateKeepScreenOn(enabled: Boolean) {
|
||||
@@ -66,10 +63,10 @@ fun Activity.setCustomBrightnessValue(value: Int) {
|
||||
}
|
||||
|
||||
@Stable
|
||||
fun Alignment.Companion.fromPreferences(value: Int, mode: ReadingModeType) = when (value) {
|
||||
1 -> when (mode) {
|
||||
VERTICAL -> CenterHorizontally
|
||||
RIGHT_TO_LEFT -> AbsoluteAlignment.Right
|
||||
fun Alignment.Companion.fromPreferences(value: Int, isRtl: Boolean, isVertical: Boolean) = when (value) {
|
||||
1 -> when {
|
||||
isVertical -> CenterHorizontally
|
||||
isRtl -> AbsoluteAlignment.Right
|
||||
else -> AbsoluteAlignment.Left
|
||||
}
|
||||
2 -> AbsoluteAlignment.Left
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.hippo.ehviewer.ui.reader
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.NavigationRegions
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation.NavigationRegion
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.getAction
|
||||
import kotlinx.coroutines.launch
|
||||
import me.saket.telephoto.zoomable.ZoomSpec
|
||||
import me.saket.telephoto.zoomable.rememberZoomableState
|
||||
import me.saket.telephoto.zoomable.zoomable
|
||||
|
||||
@Composable
|
||||
fun WebtoonViewer(
|
||||
lazyListState: LazyListState,
|
||||
withGaps: Boolean,
|
||||
pageLoader: PageLoader2,
|
||||
navigator: () -> NavigationRegions,
|
||||
onClick: () -> Unit,
|
||||
onSelectPage: (ReaderPage) -> Unit,
|
||||
onMenuRegionClick: () -> Unit,
|
||||
contentPadding: PaddingValues,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val items = pageLoader.pages
|
||||
val zoomableState = rememberZoomableState(zoomSpec = ZoomSpec)
|
||||
val density = LocalDensity.current
|
||||
val paddingPercent by Settings.webtoonSidePadding.collectAsState()
|
||||
val sidePadding by remember(density) {
|
||||
snapshotFlow {
|
||||
with(density) {
|
||||
(lazyListState.layoutInfo.viewportSize.width * paddingPercent / 100f).toDp()
|
||||
}
|
||||
}
|
||||
}.collectAsState(0.dp)
|
||||
LazyColumn(
|
||||
modifier = modifier.zoomable(
|
||||
state = zoomableState,
|
||||
onClick = {
|
||||
onClick()
|
||||
scope.launch {
|
||||
with(lazyListState) {
|
||||
val size = lazyListState.layoutInfo.viewportSize
|
||||
val x = it.x / size.width
|
||||
val y = it.y / size.height
|
||||
when (navigator().getAction(x, y)) {
|
||||
NavigationRegion.MENU -> onMenuRegionClick()
|
||||
NavigationRegion.NEXT, NavigationRegion.RIGHT -> scrollDown()
|
||||
NavigationRegion.PREV, NavigationRegion.LEFT -> scrollUp()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onLongClick = { ofs ->
|
||||
val info = lazyListState.layoutInfo.visibleItemsInfo.find { info ->
|
||||
info.offset <= ofs.y && info.offset + info.size > ofs.y
|
||||
}
|
||||
if (info == null && withGaps) {
|
||||
// Maybe user long-click on gaps? ¯\_(ツ)_/¯
|
||||
return@zoomable
|
||||
}
|
||||
info ?: error("Internal error finding long click item!!! offset:$ofs")
|
||||
onSelectPage(items[info.index])
|
||||
},
|
||||
onDoubleClick = DoubleTapZoom,
|
||||
),
|
||||
state = lazyListState,
|
||||
contentPadding = contentPadding + PaddingValues(horizontal = sidePadding),
|
||||
verticalArrangement = Arrangement.spacedBy(if (withGaps) 15.dp else 0.dp),
|
||||
) {
|
||||
items(items, key = { it.index }) { page ->
|
||||
PagerItem(
|
||||
page = page,
|
||||
pageLoader = pageLoader,
|
||||
contentScale = ContentScale.FillWidth,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val ZoomSpec = ZoomSpec(maxZoomFactor = 3f)
|
||||
@@ -50,3 +50,8 @@ abstract class ViewerNavigation {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typealias NavigationRegions = List<ViewerNavigation.Region>
|
||||
|
||||
fun NavigationRegions.getAction(x: Float, y: Float) =
|
||||
find { it.rectF.contains(x, y) }?.type ?: ViewerNavigation.NavigationRegion.MENU
|
||||
Reference in new issue
Block a user