diff --git a/app/src/main/kotlin/com/hippo/ehviewer/Settings.kt b/app/src/main/kotlin/com/hippo/ehviewer/Settings.kt index 9dfb22186..5ea5f076f 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/Settings.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/Settings.kt @@ -70,14 +70,6 @@ inline fun PrefDelegate.collectAsState(crossinline transform: @Disallo return flow.collectAsState(transform(init)) } -@Stable -@Composable -inline fun PrefDelegate.collectAsState(key1: Any?, crossinline transform: @DisallowComposableCalls (T) -> R): State { - val flow = remember(key1) { valueFlow().map { transform(it) } } - val init = getValue() - return flow.collectAsState(transform(init)) -} - @Stable @Composable fun PrefDelegate.collectAsState(): State { @@ -217,6 +209,8 @@ object Settings : DataStorePreferences(null) { val imageScaleType = intPref("pref_image_scale_type_key", 1) val landscapeZoom = boolPref("landscape_zoom", false) val zoomStart = intPref("pref_zoom_start_key", 1) + val showNavigationOverlayNewUser = boolPref("reader_navigation_overlay_new_user", true) + val showNavigationOverlayOnStart = boolPref("reader_navigation_overlay_on_start", false) init { if ("CN" == Locale.getDefault().country) { 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 41a8478b5..8c45a0c5f 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 @@ -1,6 +1,5 @@ package com.hippo.ehviewer.ui.reader -import android.graphics.PointF import androidx.compose.foundation.gestures.animateScrollBy import androidx.compose.foundation.gestures.scrollBy import androidx.compose.foundation.layout.Arrangement @@ -44,8 +43,6 @@ 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.DEFAULT -import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType.LEFT_TO_RIGHT 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 @@ -75,143 +72,154 @@ fun GalleryPager( ) { val scope = rememberCoroutineScope() val items = pageLoader.pages - when (type) { - DEFAULT, LEFT_TO_RIGHT, RIGHT_TO_LEFT, VERTICAL -> { - val isVertical = type == VERTICAL - val scaleType by Settings.imageScaleType.collectAsState() - val landscapeZoom by Settings.landscapeZoom.collectAsState() - val alignment by Settings.zoomStart.collectAsState(type) { - Alignment.fromPreferences(it, type) - } - val navigatorState = Settings.readerPagerNav.collectAsState(isVertical) { - ViewerNavigation.fromPreference(it, isVertical) - } - val invertMode by Settings.readerPagerNavInverted.collectAsState { - TappingInvertMode.entries[it] - } - LaunchedEffect(navigatorState.value, invertMode) { - navigatorState.value.invertMode = invertMode - } - 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, - ) - } - } 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, - ) - } + val isPagerType = !ReadingModeType.isWebtoon(type) + val pagerNavigation by Settings.readerPagerNav.collectAsState() + val pagerInvertMode by Settings.readerPagerNavInverted.collectAsState { + TappingInvertMode.entries[it] + } + val webtoonNavigation by Settings.readerWebtoonNav.collectAsState() + val webtoonInvertMode by Settings.readerWebtoonNavInverted.collectAsState { + TappingInvertMode.entries[it] + } + 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 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() } } - WEBTOON, CONTINUOUS_VERTICAL -> { - val zoomableState = rememberZoomableState(zoomSpec = ZoomSpec) - val navigator by Settings.readerWebtoonNav.collectAsState { - ViewerNavigation.fromPreference(it, true) + 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 invertMode by Settings.readerWebtoonNavInverted.collectAsState { - TappingInvertMode.entries[it] + } 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, + ) } - LaunchedEffect(navigator, invertMode) { - navigator.invertMode = invertMode - } - 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() - } + } + } 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 pos = PointF(it.x / size.width, it.y / size.height) - val distance = size.height * 0.75f - when (navigator.getAction(pos)) { - NavigationRegion.MENU -> onMenuRegionClick() - NavigationRegion.NEXT, NavigationRegion.RIGHT -> performScrollBy(distance) - NavigationRegion.PREV, NavigationRegion.LEFT -> performScrollBy(-distance) - } + } + }.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 + val distance = size.height * 0.75f + when (navigatorState.value.getAction(x, y)) { + NavigationRegion.MENU -> onMenuRegionClick() + NavigationRegion.NEXT, NavigationRegion.RIGHT -> performScrollBy(distance) + NavigationRegion.PREV, NavigationRegion.LEFT -> performScrollBy(-distance) } } - }, - 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, - ) - } + } + }, + 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, + ) } } } + val showOnStart = remember { + Settings.showNavigationOverlayNewUser.value || Settings.showNavigationOverlayOnStart.value + } + NavigationOverlay( + regions, + showOnStart, + onDismiss = { Settings.showNavigationOverlayNewUser.value = false }, + ) } @Composable @@ -223,7 +231,7 @@ private fun PageContainer( landscapeZoom: Boolean, alignment: Alignment.Horizontal, layoutSize: Size, - navigatorState: State, + navigatorState: State, pagerState: PagerState, onSelectPage: (ReaderPage) -> Unit, onMenuRegionClick: () -> Unit, @@ -284,9 +292,10 @@ private fun PageContainer( scope.launch { with(pagerState) { with(zoomableState) { - val pos = PointF(it.x / layoutSize.width, it.y / layoutSize.height) + val x = it.x / layoutSize.width + val y = it.y / layoutSize.height val distance = layoutSize.width - when (navigatorState.value.getAction(pos)) { + when (navigatorState.value.getAction(x, y)) { NavigationRegion.MENU -> onMenuRegionClick() NavigationRegion.NEXT -> { val canPan = if (isRtl) panRight(distance) else panLeft(distance) @@ -325,6 +334,9 @@ private fun PageContainer( } } +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) { diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/NavigationOverlay.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/NavigationOverlay.kt new file mode 100644 index 000000000..64bc55ca8 --- /dev/null +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/NavigationOverlay.kt @@ -0,0 +1,88 @@ +package com.hippo.ehviewer.ui.reader + +import androidx.compose.animation.AnimatedVisibility +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 +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 + +@Composable +fun NavigationOverlay( + 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 { + val fontSize = MaterialTheme.typography.headlineSmall.fontSize + val style = TextStyle( + color = Color.White, + fontSize = fontSize, + ) + val strokeStyle = TextStyle( + color = Color.Black, + fontSize = fontSize, + drawStyle = Stroke(8f), + ) + val scaleX = maxWidth + val scaleY = maxHeight + regions.forEach { region -> + val rect = region.rectF + val color = colorResource(region.type.colorRes) + val text = stringResource(region.type.nameRes) + Box( + modifier = Modifier.size(scaleX * rect.width(), scaleY * rect.height()) + .offset(scaleX * rect.left, scaleY * rect.top).background(color), + contentAlignment = Alignment.Center, + ) { + Text(text, style = strokeStyle) + Text(text, style = style) + } + } + } + } +} + +typealias NavigationRegions = List + +private val AnimationSpec = tween(1000) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/Utils.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/Utils.kt index 217b3f37b..d29912cdd 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/Utils.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/Utils.kt @@ -61,6 +61,7 @@ fun Activity.setCustomBrightnessValue(value: Int) { window.attributes = window.attributes.apply { screenBrightness = readerBrightness } } +@Stable fun Alignment.Companion.fromPreferences(value: Int, mode: ReadingModeType) = when (value) { 1 -> when (mode) { VERTICAL -> CenterHorizontally 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 a86451156..39d931d8f 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 @@ -37,6 +37,11 @@ enum class ReadingModeType( WEBTOON, CONTINUOUS_VERTICAL -> true } + fun isVertical(type: ReadingModeType) = when (type) { + DEFAULT, LEFT_TO_RIGHT, RIGHT_TO_LEFT -> false + VERTICAL, WEBTOON, CONTINUOUS_VERTICAL -> true + } + const val MASK = 0x00000007 fun fromPreference(preference: Int?): ReadingModeType = entries.find { it.flagValue == preference } ?: DEFAULT