diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b3da8a31..23b1ca12 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -137,7 +137,7 @@ dependencies { implementation("ru.noties.markwon:core:3.1.0") implementation("xyz.quaver:documentfilex:0.7.1") - implementation("xyz.quaver:subsampledimage:0.0.1-alpha14-SNAPSHOT-DEV01") + implementation("xyz.quaver:subsampledimage:0.0.1-alpha15-SNAPSHOT") implementation("com.google.guava:guava:31.0.1-jre") diff --git a/app/src/main/java/xyz/quaver/pupil/sources/composable/ReaderBase.kt b/app/src/main/java/xyz/quaver/pupil/sources/composable/ReaderBase.kt index 9258fdae..d55de41c 100644 --- a/app/src/main/java/xyz/quaver/pupil/sources/composable/ReaderBase.kt +++ b/app/src/main/java/xyz/quaver/pupil/sources/composable/ReaderBase.kt @@ -20,10 +20,14 @@ package xyz.quaver.pupil.sources.composable import android.app.Application import android.net.Uri +import android.os.Parcelable +import android.util.Log +import android.view.MotionEvent import androidx.compose.animation.core.* import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.border import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.* import androidx.compose.material.* @@ -34,16 +38,21 @@ import androidx.compose.material.icons.materialIcon import androidx.compose.material.icons.materialPath import androidx.compose.runtime.* import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.composed import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.draw.rotate import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.input.pointer.pointerInteropFilter import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity @@ -70,12 +79,15 @@ import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable import org.kodein.di.DIAware import org.kodein.di.android.closestDI import org.kodein.di.instance import org.kodein.log.LoggerFactory import org.kodein.log.newLogger import xyz.quaver.graphics.subsampledimage.* +import xyz.quaver.graphics.subsampledimage.ScaleTypes.CENTER_INSIDE import xyz.quaver.io.FileX import xyz.quaver.pupil.R import xyz.quaver.pupil.db.AppDatabase @@ -472,25 +484,11 @@ fun BoxScope.ReaderLazyList( } } -@Composable -fun ReaderLayoutItem( - modifier: Modifier = Modifier, - isVertical: Boolean, - content: @Composable (Modifier) -> Unit -) { - if (isVertical) - Row( - modifier = Modifier.fillMaxWidth() - ) { - content(modifier.weight(1f)) - } - else - Column( - modifier = Modifier.fillMaxHeight() - ) { - content(modifier.weight(1f)) - } -} +data class ReaderItemData( + val index: Int, + val size: Size?, + val imageSource: ImageSource? +) @ExperimentalFoundationApi @Composable @@ -498,37 +496,35 @@ fun ReaderItem( model: ReaderBaseViewModel, readerOptions: ReaderOptions, listSize: Size, - imageSources: List, + images: List, + onTap: () -> Unit = { } ) { - val context = LocalContext.current - val state = rememberSubSampledImageState( - when { - readerOptions.padding -> ScaleTypes.CENTER_INSIDE - readerOptions.orientation.isVertical -> ScaleTypes.FIT_WIDTH - else -> ScaleTypes.FIT_HEIGHT - } - ) + val (widthDp, heightDp) = LocalDensity.current.run { listSize.width.toDp() to listSize.height.toDp() } - val listSizeDp = LocalDensity.current.run { listSize.width.toDp() to listSize.height.toDp() } + Row( + modifier = when { + readerOptions.padding -> Modifier.size(widthDp, heightDp) + readerOptions.orientation.isVertical -> Modifier.fillMaxWidth() + else -> Modifier.fillMaxHeight() + }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + images.let { if (readerOptions.orientation.isReverse) it.reversed() else it }.forEach { (index, imageSize, imageSource) -> + val state = rememberSubSampledImageState() + + val modifier = when { + imageSize == null -> Modifier.weight(1f).height(heightDp) + readerOptions.padding -> Modifier.fillMaxHeight().widthIn(0.dp, widthDp/images.size).aspectRatio(imageSize.width/imageSize.height) + else -> Modifier.aspectRatio(imageSize.width/imageSize.height).weight(1f) + } - val modifier = when { - readerOptions.padding -> Modifier.size(listSizeDp.first, listSizeDp.second) - readerOptions.orientation.isVertical -> Modifier - .wrapContentHeight(state, listSizeDp.second) - .fillMaxWidth() - else -> Modifier - .wrapContentWidth(state, listSizeDp.first) - .fillMaxHeight() - } - ReaderLayoutItem(modifier, readerOptions.orientation.isVertical) { modifier -> - indices.forEach { index -> Box( - modifier.border(1.dp, Color.Gray), + modifier, contentAlignment = Alignment.Center ) { val progress = model.progressList.getOrNull(index) ?: 0f - val uri = model.imageList.getOrNull(index) if (progress == Float.NEGATIVE_INFINITY) Icon(Icons.Filled.BrokenImage, null, tint = Orange500) @@ -539,33 +535,28 @@ fun ReaderItem( LinearProgressIndicator(progress) Text((index + 1).toString()) } - else if (uri != null && progress == Float.POSITIVE_INFINITY) { - val imageSource = kotlin.runCatching { - rememberFileXImageSource(FileX(context, uri)) - }.getOrNull() + else if (progress == Float.POSITIVE_INFINITY) { + SubSampledImage( + modifier = Modifier + .fillMaxSize() + .run { + if (model.fullscreen) + doubleClickCycleZoom(state, 2f, onTap = onTap) + else + combinedClickable( + onLongClick = { - if (imageSource != null) - SubSampledImage( - modifier = Modifier - .fillMaxSize() - .run { - if (model.fullscreen) - doubleClickCycleZoom(state, 2f) - else - combinedClickable( - onLongClick = { - - } - ) { - model.fullscreen = true } - }, - imageSource = imageSource, - state = state, - onError = { - model.error(index) - } - ) + ) { + model.fullscreen = true + } + }, + imageSource = imageSource, + state = state, + onError = { + model.error(index) + } + ) } } } @@ -578,24 +569,51 @@ fun LazyListScope.ReaderLazyListContent( listSize: Size, imageSources: List, imageSizes: List, - readerOptions: ReaderOptions + readerOptions: ReaderOptions, + onTap: () -> Unit = { } ) { - when { - readerOptions.layout == ReaderOptions.Layout.SINGLE_PAGE -> - items(imageSources) { source -> - ReaderItem(model, readerOptions, listSize, listOf(source)) + when (readerOptions.layout) { + ReaderOptions.Layout.SINGLE_PAGE -> + itemsIndexed(imageSources) { index, source -> + ReaderItem(model, readerOptions, listSize, listOf(ReaderItemData(index, imageSizes[index], source))) } - readerOptions.layout == ReaderOptions.Layout.DOUBLE_PAGE -> - items(imageSources.size/2 + (imageSources.size and 0x1)) { i -> - ReaderItem(model, readerOptions, listSize, imageSources.subList(2*i, 2*i+2)) + ReaderOptions.Layout.DOUBLE_PAGE -> + itemsIndexed(imageSources.chunked(2), key = { i, _ -> i*2 }) { chunkIndex, sourceList -> + ReaderItem(model, readerOptions, listSize, sourceList.mapIndexed { i, it -> + val index = chunkIndex*2+i + ReaderItemData(index, imageSizes[index], it) + }, onTap) } - else -> - items(imageSources) { source -> - ReaderItem(model, readerOptions, listSize, listOf(source)) + ReaderOptions.Layout.AUTO -> { + val images = mutableListOf>() + + var i = 0 + while (i < imageSizes.size) { + val list = mutableListOf(i) + + if ( + imageSizes[i] != null && + imageSizes.getOrNull(i+1) != null && + listSize != Size.Zero && + imageSizes[i]!!.width*listSize.height/imageSizes[i]!!.height + + imageSizes[i+1]!!.width*listSize.height/imageSizes[i+1]!!.height < listSize.width + ) list.add(++i) + + images.add(list) + i++ } + + items(images, key = { it.first() }) { images -> + ReaderItem(model, readerOptions, listSize, images.map { ReaderItemData(it, imageSizes[it], imageSources[it]) }, onTap) + } + } + else -> itemsIndexed(imageSources) { index, source -> + ReaderItem(model, readerOptions, listSize, listOf(ReaderItemData(index, imageSizes[index], source)), onTap) + } } } +@ExperimentalComposeUiApi @ExperimentalMaterialApi @ExperimentalFoundationApi @Composable @@ -666,7 +684,10 @@ fun ReaderBase( val nestedScrollConnection = remember { object: NestedScrollConnection { override suspend fun onPreFling(available: Velocity): Velocity { - return if (mainReaderOptions.snap) { + return if ( + mainReaderOptions.snap && + listState.layoutInfo.visibleItemsInfo.size > 1 + ) { val velocity = when (mainReaderOptions.orientation) { ReaderOptions.Orientation.VERTICAL_DOWN -> available.y ReaderOptions.Orientation.VERTICAL_UP -> -(available.y) @@ -692,19 +713,23 @@ fun ReaderBase( val imageSources = remember { mutableStateListOf() } val imageSizes = remember { mutableStateListOf() } - LaunchedEffect(model.imageList.count { it != null }) { - if (imageSources.size != model.imageList.size) - imageSources.addAll(List (model.imageList.size-imageSources.size) { null }) + LaunchedEffect(model.progressList.count { it.isFinite() }) { + val size = model.progressList.size - if (imageSizes.size != model.imageList.size) - imageSizes.addAll(List (model.imageList.size-imageSources.size) { null }) + if (imageSources.size != size) + imageSources.addAll(List (size-imageSources.size) { null }) + + if (imageSizes.size != size) + imageSizes.addAll(List (size-imageSizes.size) { null }) coroutineScope.launch { - model.imageList.forEachIndexed { i, uri -> + repeat(size) { i -> + val uri = model.imageList[i] + if (imageSources[i] == null && uri != null) imageSources[i] = FileXImageSource(FileX(context, uri)) - if (imageSizes[i] == null) + if (imageSizes[i] == null && model.progressList[i] == Float.POSITIVE_INFINITY) imageSources[i]?.let { imageSizes[i] = it.imageSize } @@ -726,7 +751,11 @@ fun ReaderBase( imageSources, imageSizes, mainReaderOptions - ) + ) { + coroutineScope.launch { + listState.scrollToItem(listState.firstVisibleItemIndex + 1) + } + } } if (model.progressList.any { it.isFinite() }) @@ -745,4 +774,44 @@ fun ReaderBase( ) } } -} \ No newline at end of file +} + +fun Modifier.doubleClickCycleZoom( + state: SubSampledImageState, + scale: Float = 2f, + animationSpec: AnimationSpec = spring(), + onTap: () -> Unit = { }, +) = composed { + val initialImageRect by produceState(null, state.canvasSize, state.imageSize) { + state.canvasSize?.let { canvasSize -> + state.imageSize?.let { imageSize -> + value = state.bound(state.scaleType(canvasSize, imageSize), canvasSize) + } } + } + + val coroutineScope = rememberCoroutineScope() + + pointerInput(Unit) { + detectTapGestures( + onTap = { onTap() }, + onDoubleTap = { centroid -> + val imageRect = state.imageRect + coroutineScope.launch { + if (imageRect == null || imageRect != initialImageRect) + state.resetImageRect(animationSpec) + else { + state.setImageRectWithBound( + Rect( + Offset( + centroid.x - (centroid.x - imageRect.left) * scale, + centroid.y - (centroid.y - imageRect.top) * scale + ), + imageRect.size * scale + ), animationSpec + ) + } + } + } + ) + } +} diff --git a/app/src/main/java/xyz/quaver/pupil/sources/hitomi/Hitomi.kt b/app/src/main/java/xyz/quaver/pupil/sources/hitomi/Hitomi.kt index e8b7883c..6f30a53c 100644 --- a/app/src/main/java/xyz/quaver/pupil/sources/hitomi/Hitomi.kt +++ b/app/src/main/java/xyz/quaver/pupil/sources/hitomi/Hitomi.kt @@ -32,6 +32,7 @@ import androidx.compose.material.icons.filled.* import androidx.compose.runtime.* import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource @@ -67,7 +68,7 @@ import xyz.quaver.pupil.sources.hitomi.lib.getReferer import xyz.quaver.pupil.sources.hitomi.lib.imageUrlFromImage import xyz.quaver.pupil.ui.theme.Orange500 -@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class) +@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class) class Hitomi(app: Application) : Source(), DIAware { override val di by closestDI(app) diff --git a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/Manatoki.kt b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/Manatoki.kt index 96952191..1f43aaca 100644 --- a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/Manatoki.kt +++ b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/Manatoki.kt @@ -41,6 +41,7 @@ import androidx.compose.runtime.* import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.geometry.Offset @@ -93,7 +94,12 @@ import kotlin.math.sign private val imageUserAgent = "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Mobile Safari/537.36" -@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class, ExperimentalAnimationApi::class) +@OptIn( + ExperimentalMaterialApi::class, + ExperimentalFoundationApi::class, + ExperimentalAnimationApi::class, + ExperimentalComposeUiApi::class +) class Manatoki(app: Application) : Source(), DIAware { override val di by closestDI(app)