Improve reader and library UI controls

This commit is contained in:
Sergey Chernov 2026-05-17 23:41:23 +03:00
parent 1239f15836
commit 06b4614cbe
7 changed files with 131 additions and 58 deletions

View File

@ -21,7 +21,6 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@ -58,9 +57,7 @@ internal fun BookInfoScreen(
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back to reader")
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface,
),
colors = themedTopAppBarColors(),
)
},
) {

View File

@ -24,7 +24,6 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -105,7 +104,7 @@ internal fun ImageViewer(
contentWindowInsets = WindowInsets(0, 0, 0, 0),
snackbarHost = { SnackbarHost(snackbarHostState) },
topBar = {
Surface(color = MaterialTheme.colorScheme.surface) {
ThemedTopBarSurface {
Row(
modifier = Modifier.fillMaxWidth().height(48.dp),
verticalAlignment = Alignment.CenterVertically,

View File

@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Close
@ -36,12 +37,9 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@ -53,6 +51,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
@ -205,9 +204,7 @@ internal fun LibraryScreen(
modifier = Modifier.fillMaxWidth(),
)
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface,
),
colors = themedTopAppBarColors(),
actions = {
IconButton(onClick = { refresh() }, enabled = !busy && !loadingPage) {
Icon(Icons.Filled.Refresh, contentDescription = "Refresh library")
@ -412,14 +409,13 @@ private fun LibrarySearchField(
onClear: () -> Unit,
modifier: Modifier = Modifier,
) {
val shape = RoundedCornerShape(18.dp)
OutlinedTextField(
value = value,
onValueChange = onValueChange,
singleLine = true,
textStyle = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurface),
val shape = RoundedCornerShape(16.dp)
Row(
modifier = modifier
.height(56.dp)
.height(42.dp)
.clip(shape)
.background(MaterialTheme.colorScheme.surface)
.padding(horizontal = 12.dp)
.onPreviewKeyEvent { event ->
if (event.type == KeyEventType.KeyDown && event.key == Key.Escape && value.isNotBlank()) {
onClear()
@ -428,38 +424,43 @@ private fun LibrarySearchField(
false
}
},
placeholder = {
Text(
"Search library",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.72f),
maxLines = 1,
)
},
leadingIcon = {
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
Icons.Filled.Search,
contentDescription = "Search library",
modifier = Modifier.size(20.dp),
modifier = Modifier.size(18.dp),
tint = MaterialTheme.colorScheme.outline,
)
},
trailingIcon = {
if (value.isNotBlank()) {
IconButton(onClick = onClear, modifier = Modifier.size(34.dp)) {
Icon(Icons.Filled.Close, contentDescription = "Clear search", modifier = Modifier.size(18.dp))
}
}
},
shape = shape,
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.primary,
unfocusedBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.55f),
focusedContainerColor = MaterialTheme.colorScheme.surface,
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
cursorColor = MaterialTheme.colorScheme.primary,
),
Box(
modifier = Modifier
.weight(1f)
.padding(start = 8.dp, end = 6.dp),
contentAlignment = Alignment.CenterStart,
) {
BasicTextField(
value = value,
onValueChange = onValueChange,
singleLine = true,
textStyle = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onSurface),
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
modifier = Modifier.fillMaxWidth(),
)
if (value.isBlank()) {
Text(
"Search library",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.72f),
maxLines = 1,
)
}
}
if (value.isNotBlank()) {
IconButton(onClick = onClear, modifier = Modifier.size(30.dp)) {
Icon(Icons.Filled.Close, contentDescription = "Clear search", modifier = Modifier.size(17.dp))
}
}
}
}
@Composable

View File

@ -3,6 +3,10 @@ package net.sergeych.toread
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.animateScrollBy
import androidx.compose.foundation.gestures.awaitEachGesture
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.Column
@ -30,6 +34,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -38,6 +43,9 @@ import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
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.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
@ -67,6 +75,7 @@ import net.sergeych.toread.fb2.Fb2TextSpan
import net.sergeych.toread.fb2.Fb2TextStyle
import net.sergeych.toread.text.HyphenationRegistry
import net.sergeych.toread.text.SoftHyphen
import kotlinx.coroutines.launch
import kotlin.math.max
import kotlin.math.min
@ -79,6 +88,7 @@ internal fun ContinuousBookReader(
onImageOpen: (ViewedBookImage) -> Unit = {},
) {
val hyphenation = remember { HyphenationRegistry() }
val scope = rememberCoroutineScope()
val contentPadding = if (isAndroidPlatform()) {
PaddingValues(start = 6.dp, top = 6.dp, end = 0.dp, bottom = 6.dp)
} else {
@ -88,7 +98,19 @@ internal fun ContinuousBookReader(
LazyColumn(
state = listState,
modifier = modifier
.background(MaterialTheme.colorScheme.surface),
.background(MaterialTheme.colorScheme.surface)
.pageTurnOnTouchTap(
onPageDown = {
scope.launch {
listState.animateScrollBy(listState.pageScrollDistance())
}
},
onPageUp = {
scope.launch {
listState.animateScrollBy(-listState.pageScrollDistance())
}
},
),
contentPadding = contentPadding,
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
@ -116,6 +138,34 @@ internal fun ContinuousBookReader(
}
}
private fun Modifier.pageTurnOnTouchTap(
onPageDown: () -> Unit,
onPageUp: () -> Unit,
): Modifier = pointerInput(onPageDown, onPageUp) {
awaitEachGesture {
val down = awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Final)
if (down.type != PointerType.Touch) {
waitForUpOrCancellation(pass = PointerEventPass.Final)
return@awaitEachGesture
}
val up = waitForUpOrCancellation(pass = PointerEventPass.Final) ?: return@awaitEachGesture
if (up.isConsumed) return@awaitEachGesture
if (down.position.x < size.width / 2f) {
onPageDown()
} else {
onPageUp()
}
}
}
private fun LazyListState.pageScrollDistance(): Float {
val layoutInfo = layoutInfo
val viewportHeight = layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset
return viewportHeight.toFloat().coerceAtLeast(0f)
}
private fun LazyListScope.sectionItems(
book: Fb2Book,
section: Fb2Section,
@ -411,8 +461,9 @@ private fun ReaderText(
@Composable
private fun readerParagraphTextStyle(language: String?): TextStyle =
MaterialTheme.typography.bodyLarge.copy(
fontSize = 18.sp,
lineHeight = 27.sp,
fontWeight = FontWeight(350),
fontSize = 21.sp,
lineHeight = 28.sp,
hyphens = if (isAndroidPlatform()) Hyphens.Auto else Hyphens.Unspecified,
lineBreak = if (isAndroidPlatform()) LineBreak.Paragraph else LineBreak.Unspecified,
localeList = language?.takeIf(String::isNotBlank)?.let { LocaleList(Locale(it)) },

View File

@ -19,7 +19,6 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -142,7 +141,7 @@ private fun CompactReaderTopBar(
onBookInfo: () -> Unit,
onBack: () -> Unit,
) {
Surface(color = MaterialTheme.colorScheme.surface) {
ThemedTopBarSurface {
Row(
modifier = Modifier.fillMaxWidth().height(48.dp),
verticalAlignment = Alignment.CenterVertically,

View File

@ -28,7 +28,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -62,9 +61,7 @@ internal fun ScanScreen(
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back to library")
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface,
),
colors = themedTopAppBarColors(),
)
},
) {

View File

@ -3,16 +3,21 @@ package net.sergeych.toread
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarColors
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -75,6 +80,30 @@ internal fun quietCardColors() = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.92f),
)
@Composable
internal fun ThemedTopBarSurface(content: @Composable ColumnScope.() -> Unit) {
Surface(
color = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
) {
Column {
content()
HorizontalDivider(color = MaterialTheme.colorScheme.outline.copy(alpha = 0.38f))
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun themedTopAppBarColors(): TopAppBarColors =
TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
scrolledContainerColor = MaterialTheme.colorScheme.primaryContainer,
navigationIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
actionIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
)
@Composable
internal fun readerBackground(): Brush = SolidColor(MaterialTheme.colorScheme.background)