From 31f66fd05b34e1ebc56f8005f2efa6e0f1cf4d2b Mon Sep 17 00:00:00 2001 From: Vladimir Dostoyevskiy <99822064+VDostoyevskiy@users.noreply.github.com> Date: Tue, 27 Feb 2024 23:14:36 +0800 Subject: [PATCH] Reader updates (#800) * Refine * Bars and sliders * Zoomable * Refine * Refine Slider & LazyList state sync * Refine * Cleanup * Fixes * Fix initial state * Fixes * Fixes * Fixup initial state --------- Co-authored-by: GG Bond --- app/build.gradle.kts | 3 + .../hippo/ehviewer/ui/reader/ReaderScreen.kt | 104 ++++++++++++------ .../ui/reader/SliderPagerDoubleSync.kt | 46 ++++++++ .../tachiyomi/ui/reader/ReaderAppBars.kt | 3 +- gradle/libs.versions.toml | 5 + 5 files changed, 124 insertions(+), 37 deletions(-) create mode 100644 app/src/main/kotlin/com/hippo/ehviewer/ui/reader/SliderPagerDoubleSync.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 934ecc442..77cf1983e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -245,6 +245,7 @@ dependencies { implementation(libs.aboutlibraries.core) implementation(libs.aboutlibraries.compose.m3) + implementation(libs.accompanist.drawable.painter) implementation(libs.insetter) // Dead Dependency @@ -257,6 +258,8 @@ dependencies { implementation(platform(libs.coil.bom)) implementation(libs.bundles.coil) + implementation(libs.telephoto.zoomable) + implementation(libs.bundles.ktor) implementation(libs.bundles.kotlinx.serialization) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/ReaderScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/ReaderScreen.kt index 882b12b14..fd237e46c 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/ReaderScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/ReaderScreen.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text @@ -22,11 +23,18 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.LayoutDirection.Rtl +import androidx.compose.ui.zIndex import coil3.BitmapImage +import coil3.DrawableImage import coil3.Image as CoilImage +import com.google.accompanist.drawablepainter.DrawablePainter import com.hippo.ehviewer.R +import com.hippo.ehviewer.Settings import com.hippo.ehviewer.client.data.BaseGalleryInfo +import com.hippo.ehviewer.collectAsState import com.hippo.ehviewer.download.DownloadManager import com.hippo.ehviewer.download.archiveFile import com.hippo.ehviewer.gallery.ArchivePageLoader @@ -39,6 +47,11 @@ import eu.kanade.tachiyomi.source.model.Page.State.ERROR import eu.kanade.tachiyomi.source.model.Page.State.LOAD_PAGE import eu.kanade.tachiyomi.source.model.Page.State.QUEUE import eu.kanade.tachiyomi.source.model.Page.State.READY +import eu.kanade.tachiyomi.ui.reader.ReaderAppBars +import me.saket.telephoto.zoomable.ZoomSpec +import me.saket.telephoto.zoomable.rememberZoomableState +import me.saket.telephoto.zoomable.zoomable +import moe.tarsin.kt.unreachable @Destination @Composable @@ -54,45 +67,63 @@ fun ReaderScreen(info: BaseGalleryInfo, page: Int = -1) { pageLoader.stop() } } + val showSeekbar by Settings.showReaderSeekbar.collectAsState() + val lazyListState = rememberLazyListState() + val zoomableState = rememberZoomableState(zoomSpec = ZoomSpec(maxZoomFactor = 3f)) + val sync = remember { SliderPagerDoubleSync(lazyListState) } + sync.Sync() + val isRtl = LocalLayoutDirection.current == Rtl Deferred({ pageLoader.awaitReady() }) { - LazyColumn(modifier = Modifier.fillMaxSize()) { - items(pageLoader.mPages) { page -> - pageLoader.request(page.index) - val state by page.status.collectAsState() - when (state) { - QUEUE, LOAD_PAGE -> { - Box(modifier = Modifier.fillMaxWidth().aspectRatio(DEFAULT_ASPECT)) { - CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) + Box { + ReaderAppBars( + visible = true, + isRtl = isRtl, + showSeekBar = showSeekbar, + currentPage = sync.sliderValue, + totalPages = pageLoader.size, + onSliderValueChange = { sync.sliderScrollTo(it + 1) }, + onClickSettings = { }, + modifier = Modifier.zIndex(1f), + ) + LazyColumn(modifier = Modifier.fillMaxSize().zoomable(zoomableState), state = lazyListState) { + items(pageLoader.mPages) { page -> + pageLoader.request(page.index) + val state by page.status.collectAsState() + when (state) { + QUEUE, LOAD_PAGE -> { + Box(modifier = Modifier.fillMaxWidth().aspectRatio(DEFAULT_ASPECT)) { + CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) + } } - } - DOWNLOAD_IMAGE -> { - Box(modifier = Modifier.fillMaxWidth().aspectRatio(DEFAULT_ASPECT)) { - val progress by page.progressFlow.collectAsState() - CircularProgressIndicator( - progress = { progress / 100f }, - modifier = Modifier.align(Alignment.Center), + DOWNLOAD_IMAGE -> { + Box(modifier = Modifier.fillMaxWidth().aspectRatio(DEFAULT_ASPECT)) { + val progress by page.progressFlow.collectAsState() + CircularProgressIndicator( + progress = { progress / 100f }, + modifier = Modifier.align(Alignment.Center), + ) + } + } + READY -> { + val image = page.image!!.innerImage!! + val painter = remember(image) { image.toPainter() } + Image( + painter = painter, + contentDescription = null, + modifier = Modifier.fillMaxWidth(), + contentScale = ContentScale.FillWidth, ) } - } - READY -> { - val image = page.image!!.innerImage!! - val painter = remember(image) { image.toPainter() } - Image( - painter = painter, - contentDescription = null, - modifier = Modifier.fillMaxWidth(), - contentScale = ContentScale.FillWidth, - ) - } - ERROR -> { - Box(modifier = Modifier.fillMaxWidth().aspectRatio(DEFAULT_ASPECT)) { - Column( - modifier = Modifier.align(Companion.Center), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text(text = page.errorMsg.orEmpty()) - Button(onClick = { }) { - Text(text = stringResource(id = R.string.action_retry)) + ERROR -> { + Box(modifier = Modifier.fillMaxWidth().aspectRatio(DEFAULT_ASPECT)) { + Column( + modifier = Modifier.align(Companion.Center), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text(text = page.errorMsg.orEmpty()) + Button(onClick = { }) { + Text(text = stringResource(id = R.string.action_retry)) + } } } } @@ -105,7 +136,8 @@ fun ReaderScreen(info: BaseGalleryInfo, page: Int = -1) { private fun CoilImage.toPainter() = when (this) { is BitmapImage -> BitmapPainter(image = bitmap.asImageBitmap()) - else -> TODO() + is DrawableImage -> DrawablePainter(drawable) + else -> unreachable() } private const val DEFAULT_ASPECT = 1 / 1.4125f diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/SliderPagerDoubleSync.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/SliderPagerDoubleSync.kt new file mode 100644 index 000000000..7e4c4249e --- /dev/null +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/SliderPagerDoubleSync.kt @@ -0,0 +1,46 @@ +package com.hippo.ehviewer.ui.reader + +import androidx.compose.foundation.interaction.collectIsDraggedAsState +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.mapNotNull + +class SliderPagerDoubleSync(private val lazyListState: LazyListState) { + private var sliderFollowPager by mutableStateOf(true) + var sliderValue by mutableIntStateOf(1) + + fun sliderScrollTo(index: Int) { + sliderFollowPager = false + sliderValue = index + } + + @Composable + fun Sync() { + val fling by lazyListState.interactionSource.collectIsDraggedAsState() + if (fling) sliderFollowPager = true + if (sliderFollowPager) { + LaunchedEffect(Unit) { + snapshotFlow { + lazyListState.layoutInfo + }.mapNotNull { info -> + info.visibleItemsInfo.lastOrNull() + }.collect { info -> + sliderValue = info.index + 1 + } + } + } else { + LaunchedEffect(Unit) { + snapshotFlow { sliderValue - 1 }.collectLatest { index -> + lazyListState.animateScrollToItem(index) + } + } + } + } +} diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ReaderAppBars.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ReaderAppBars.kt index 46bcd7190..d1af762f9 100644 --- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ReaderAppBars.kt +++ b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ReaderAppBars.kt @@ -24,7 +24,8 @@ fun ReaderAppBars( totalPages: Int, onSliderValueChange: (Int) -> Unit, onClickSettings: () -> Unit, -) = Column(modifier = Modifier.fillMaxHeight(), verticalArrangement = Arrangement.SpaceBetween) { + modifier: Modifier = Modifier, +) = Column(modifier = modifier.fillMaxHeight(), verticalArrangement = Arrangement.SpaceBetween) { Spacer(modifier = Modifier.weight(1f)) AnimatedVisibility( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 645c748b8..d239db94c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,6 @@ [versions] aboutlibraries = "10.10.0" +accompanist = "0.34.0" agp = "8.4.0-alpha11" androidx-activity = "1.8.2" androidx-appcompat = "1.6.1" @@ -29,11 +30,13 @@ ktor = "3.0.0-beta-1" okhttp3 = "5.0.0-alpha.12" okio = "3.8.0" splitties = "3.0.0" +telephoto = "0.8.0" xmlutil = "0.86.3" [libraries] aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutlibraries" } aboutlibraries-compose-m3 = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "aboutlibraries" } +accompanist-drawable-painter = { module = "com.google.accompanist:accompanist-drawablepainter", version.ref = "accompanist" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" } androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } @@ -117,6 +120,8 @@ splitties-arch-room = { module = "com.louiscad.splitties:splitties-arch-room", v splitties-preferences = { module = "com.louiscad.splitties:splitties-preferences", version.ref = "splitties" } splitties-systemservices = { module = "com.louiscad.splitties:splitties-systemservices", version.ref = "splitties" } +telephoto-zoomable = { module = "me.saket.telephoto:zoomable", version.ref = "telephoto" } + [bundles] coil = ["coil-compose", "coil-gif", "coil-network"] compose = ["compose-foundation", "compose-material-icons-extended", "compose-material3", "compose-material3-windowsizeclass", "compose-ui-util"]