diff --git a/composeApp/src/commonMain/kotlin/net/sergeych/toread/ImageViewer.kt b/composeApp/src/commonMain/kotlin/net/sergeych/toread/ImageViewer.kt index d41e437..be20f20 100644 --- a/composeApp/src/commonMain/kotlin/net/sergeych/toread/ImageViewer.kt +++ b/composeApp/src/commonMain/kotlin/net/sergeych/toread/ImageViewer.kt @@ -6,8 +6,11 @@ import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.gestures.rememberTransformableState import androidx.compose.foundation.gestures.transformable import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -35,7 +38,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester @@ -66,6 +68,8 @@ internal fun ImageViewer( val focusRequester = remember { FocusRequester() } val snackbarHostState = remember { SnackbarHostState() } val scope = rememberCoroutineScope() + val imageBackgroundColor = readerImageBackgroundColor() + val imageAspectRatio = image.bitmap.width.toFloat() / image.bitmap.height.toFloat() fun setScale(next: Float) { scale = next.coerceIn(MinImageScale, MaxImageScale) @@ -133,11 +137,11 @@ internal fun ImageViewer( } }, ) { padding -> - Box( + BoxWithConstraints( modifier = Modifier .fillMaxSize() .padding(padding) - .background(Color.Black) + .background(readerBackground()) .focusRequester(focusRequester) .onPreviewKeyEvent { event -> if (event.type != KeyEventType.KeyDown) return@onPreviewKeyEvent false @@ -171,19 +175,28 @@ internal fun ImageViewer( }, contentAlignment = Alignment.Center, ) { - Image( - bitmap = image.bitmap, - contentDescription = image.title, - modifier = Modifier - .fillMaxSize() + val imageModifier = if (maxWidth / maxHeight > imageAspectRatio) { + Modifier.fillMaxHeight().aspectRatio(imageAspectRatio) + } else { + Modifier.fillMaxWidth().aspectRatio(imageAspectRatio) + } + Box( + modifier = imageModifier .graphicsLayer { scaleX = scale scaleY = scale translationX = offset.x translationY = offset.y - }, - contentScale = ContentScale.Fit, - ) + } + .background(imageBackgroundColor), + ) { + Image( + bitmap = image.bitmap, + contentDescription = image.title, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Fit, + ) + } } } } diff --git a/composeApp/src/commonMain/kotlin/net/sergeych/toread/ReaderContent.kt b/composeApp/src/commonMain/kotlin/net/sergeych/toread/ReaderContent.kt index f184565..81282c6 100644 --- a/composeApp/src/commonMain/kotlin/net/sergeych/toread/ReaderContent.kt +++ b/composeApp/src/commonMain/kotlin/net/sergeych/toread/ReaderContent.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.gestures.awaitFirstDown import androidx.compose.foundation.gestures.waitForUpOrCancellation import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow @@ -16,6 +17,7 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -47,6 +49,7 @@ import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.PointerType import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.nestedScroll @@ -368,6 +371,7 @@ internal fun CoverAndTitle(book: Fb2Book, onImageOpen: (ViewedBookImage) -> Unit image = book.coverImages.firstOrNull() ?: book.bodyImages.firstOrNull(), modifier = Modifier.width(112.dp).aspectRatio(0.68f), contentScale = ContentScale.Crop, + fitBackgroundToBitmapBounds = false, onOpen = onImageOpen, ) Column(verticalArrangement = Arrangement.spacedBy(6.dp), modifier = Modifier.weight(1f)) { @@ -564,6 +568,7 @@ private fun BookImage( image: Fb2ImageRef?, modifier: Modifier = Modifier, contentScale: ContentScale = ContentScale.Fit, + fitBackgroundToBitmapBounds: Boolean = true, onOpen: (ViewedBookImage) -> Unit = {}, ) { val binary = remember(book, image) { @@ -573,10 +578,12 @@ private fun BookImage( binary?.let { decodeBookImage(it) } } val imageTitle = image?.alt?.ifBlank { null } ?: book.title - Box( + val imageBackgroundColor = readerImageBackgroundColor() + val density = LocalDensity.current + BoxWithConstraints( modifier = modifier .clip(RoundedCornerShape(8.dp)) - .background(MaterialTheme.colorScheme.surfaceVariant) + .background(MaterialTheme.colorScheme.surface) .then( if (bitmap != null && binary != null) { Modifier.clickable { @@ -596,12 +603,23 @@ private fun BookImage( contentAlignment = Alignment.Center, ) { if (bitmap != null) { - Image( - bitmap = bitmap, - contentDescription = imageTitle, - modifier = Modifier.fillMaxWidth(), - contentScale = contentScale, - ) + val imageAspectRatio = bitmap.width.toFloat() / bitmap.height.coerceAtLeast(1).toFloat() + val imageModifier = if (fitBackgroundToBitmapBounds) { + val bitmapWidth = with(density) { bitmap.width.toDp() } + Modifier + .width(if (bitmapWidth < maxWidth) bitmapWidth else maxWidth) + .aspectRatio(imageAspectRatio) + } else { + Modifier.fillMaxSize() + } + Box(imageModifier.background(imageBackgroundColor)) { + Image( + bitmap = bitmap, + contentDescription = imageTitle, + modifier = Modifier.fillMaxSize(), + contentScale = contentScale, + ) + } } else { Text(strings.noImage, style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.outline) } diff --git a/composeApp/src/commonMain/kotlin/net/sergeych/toread/SharedUi.kt b/composeApp/src/commonMain/kotlin/net/sergeych/toread/SharedUi.kt index a540d99..53313dd 100644 --- a/composeApp/src/commonMain/kotlin/net/sergeych/toread/SharedUi.kt +++ b/composeApp/src/commonMain/kotlin/net/sergeych/toread/SharedUi.kt @@ -22,7 +22,9 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.lerp import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import kotlin.math.roundToInt @@ -107,6 +109,16 @@ internal fun themedTopAppBarColors(): TopAppBarColors = @Composable internal fun readerBackground(): Brush = SolidColor(MaterialTheme.colorScheme.background) +@Composable +internal fun readerImageBackgroundColor(): Color { + val surface = MaterialTheme.colorScheme.surface + return if (surface.isVisuallyDark()) lerp(surface, Color.White, 0.5f) else surface +} + +private fun Color.isVisuallyDark(): Boolean { + val luminance = 0.2126f * red + 0.7152f * green + 0.0722f * blue + return luminance < 0.5f +} internal fun Int.formatCompact(): String = if (this >= 10_000) "${(this / 1000.0).roundToInt()}k" else toString()