DocumentScreen: Landscape mode

This commit is contained in:
Pierre-Yves Nicolas
2025-07-16 13:42:05 +02:00
parent d0a77cfd3d
commit 6e30861dbf
5 changed files with 190 additions and 152 deletions

View File

@@ -74,8 +74,8 @@ fun SecondaryActionButton(
}
@Composable
fun BackButton(onClick: () -> Unit) {
IconButton(onClick = onClick) {
fun BackButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
IconButton(onClick = onClick, modifier = modifier) {
Icon(
Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.back)

View File

@@ -29,29 +29,21 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Article
import androidx.compose.material3.BottomAppBar
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
@@ -193,37 +185,31 @@ private fun CameraScreenScaffold(
thumbnailCoords: MutableState<Offset>,
toAboutScreen: () -> Unit,
) {
val documentBar : @Composable () -> Unit = {
DocumentBar(
pageListState = pageListState,
pageCount = cameraUiState.pageCount,
onFinalizePressed = onFinalizePressed,
onDebugModeSwitched = onDebugModeSwitched,
isLandscape = cameraUiState.isLandscape
)
}
Box {
if (!cameraUiState.isLandscape) {
Scaffold(
bottomBar = documentBar
) { padding ->
val modifier = Modifier.padding(bottom = padding.calculateBottomPadding()).fillMaxSize()
CameraPreviewBox(cameraPreview, cameraUiState, onCapture, modifier)
var tapCount by remember { mutableStateOf(0) }
var lastTapTime by remember { mutableStateOf(0L) }
val tapThreshold = 500L
val onPageCountClick = {
val currentTime = System.currentTimeMillis()
if (currentTime - lastTapTime < tapThreshold) {
tapCount++
if (tapCount >= 3) {
onDebugModeSwitched()
tapCount = 0
}
} else {
Scaffold { innerPadding ->
Row(
modifier = Modifier.padding(innerPadding).fillMaxSize()
) {
CameraPreviewBox(cameraPreview, cameraUiState, onCapture, Modifier)
documentBar()
}
}
tapCount = 1
}
lastTapTime = currentTime
}
Box {
MyScaffold(
toAboutScreen = toAboutScreen,
pageListState = pageListState,
bottomBar = { Bar(cameraUiState.pageCount, onPageCountClick, onFinalizePressed) }
) {
modifier -> CameraPreviewBox(cameraPreview, cameraUiState, onCapture, modifier)
}
AboutScreenNavButton(
onClick = toAboutScreen,
modifier = Modifier.align(Alignment.TopEnd).windowInsetsPadding(WindowInsets.safeDrawing)
)
if (cameraUiState.captureState is CaptureState.CapturePreview) {
CapturedImage(cameraUiState.captureState.processed.asImageBitmap(), thumbnailCoords)
}
@@ -410,61 +396,6 @@ fun MessageBox(inferenceTime: Long) {
)
}
@Composable
fun DocumentBar(
pageListState: CommonPageListState,
pageCount: Int,
onFinalizePressed: () -> Unit,
onDebugModeSwitched: () -> Unit,
isLandscape: Boolean,
) {
var tapCount by remember { mutableStateOf(0) }
var lastTapTime by remember { mutableStateOf(0L) }
val tapThreshold = 500L
val onPageCountClick = {
val currentTime = System.currentTimeMillis()
if (currentTime - lastTapTime < tapThreshold) {
tapCount++
if (tapCount >= 3) {
onDebugModeSwitched()
tapCount = 0
}
} else {
tapCount = 1
}
lastTapTime = currentTime
}
Column (
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.background(MaterialTheme.colorScheme.surfaceContainer)
) {
CommonPageList(pageListState, Modifier.weight(1f))
BottomAppBar(
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
) {
if (isLandscape) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
Bar(pageCount, onPageCountClick, onFinalizePressed)
}
} else {
Row(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 1.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Bar(pageCount, onPageCountClick, onFinalizePressed)
}
}
}
}
}
@Composable
private fun Bar(
pageCount: Int,
@@ -479,7 +410,7 @@ private fun Bar(
MainActionButton(
onClick = onFinalizePressed,
enabled = pageCount > 0,
text = "Document",
text = stringResource(R.string.document),
icon = Icons.AutoMirrored.Filled.Article,
)
}

View File

@@ -22,7 +22,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@@ -40,11 +39,8 @@ import androidx.compose.material3.AlertDialog
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableIntState
@@ -95,28 +91,29 @@ fun DocumentScreen(
BackHandler {
navigation.back()
}
Scaffold (
topBar = {
TopAppBar(
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
titleContentColor = MaterialTheme.colorScheme.onSurface,
),
title = { Text(stringResource(R.string.document)) },
navigationIcon = { BackButton(navigation.back) },
actions = {
AboutScreenNavButton(onClick = navigation.toAboutScreen)
}
MyScaffold(
toAboutScreen = navigation.toAboutScreen,
pageListState = CommonPageListState(
pageIds,
imageLoader,
onPageClick = { index -> currentPageIndex.intValue = index },
currentPageIndex = currentPageIndex.intValue,
listState = rememberLazyListState(),
),
onBack = navigation.back,
bottomBar = {
BottomBar(showPdfDialog, showNewDocDialog)
},
pageListButton = {
SecondaryActionButton(
icon = Icons.Default.Add,
onClick = navigation.toCameraScreen,
contentDescription = stringResource(R.string.add_page),
)
},
bottomBar = {
Column {
PageList(pageIds, imageLoader, currentPageIndex, navigation.toCameraScreen)
BottomBar(showPdfDialog, showNewDocDialog)
}
}
) { padding ->
DocumentPreview(pageIds, imageLoader, currentPageIndex, onDeleteImage, padding)
) { modifier ->
DocumentPreview(pageIds, imageLoader, currentPageIndex, onDeleteImage, modifier)
if (showNewDocDialog.value) {
NewDocumentDialog(onConfirm = onStartNew, showNewDocDialog)
}
@@ -135,14 +132,12 @@ private fun DocumentPreview(
imageLoader: (String) -> Bitmap?,
currentPageIndex: MutableIntState,
onDeleteImage: (String) -> Unit,
padding: PaddingValues,
modifier: Modifier,
) {
val imageId = pageIds[currentPageIndex.intValue]
Column (
modifier = Modifier
.fillMaxSize()
modifier = modifier
.background(MaterialTheme.colorScheme.surfaceContainerLow)
.padding(padding)
) {
Box (
modifier = Modifier.fillMaxSize()
@@ -175,13 +170,13 @@ private fun DocumentPreview(
contentDescription = stringResource(R.string.delete_page),
onClick = { onDeleteImage(imageId) },
modifier = Modifier
.align(Alignment.TopEnd)
.align(Alignment.BottomEnd)
.padding(8.dp)
)
Text("${currentPageIndex.intValue + 1} / ${pageIds.size}",
color = MaterialTheme.colorScheme.inverseOnSurface,
modifier = Modifier
.align(Alignment.TopStart)
.align(Alignment.BottomStart)
.padding(all = 16.dp)
.background(
color = MaterialTheme.colorScheme.inverseSurface.copy(alpha = 0.5f),
@@ -193,34 +188,6 @@ private fun DocumentPreview(
}
}
@Composable
private fun PageList(
pageIds: List<String>,
imageLoader: (String) -> Bitmap?,
currentPageIndex: MutableState<Int>,
toCameraScreen: () -> Unit
) {
Box {
CommonPageList(
CommonPageListState(
pageIds,
imageLoader,
onPageClick = { index -> currentPageIndex.value = index },
currentPageIndex = currentPageIndex.value,
listState = rememberLazyListState()
)
)
SecondaryActionButton(
icon = Icons.Default.Add,
onClick = toCameraScreen,
contentDescription = stringResource(R.string.add_page),
modifier = Modifier
.align(Alignment.CenterEnd)
.padding(8.dp)
)
}
}
@Composable
private fun BottomBar(
showPdfDialog: MutableState<Boolean>,
@@ -296,3 +263,10 @@ fun DocumentScreenPreview() {
)
}
}
@Preview(showBackground = true, widthDp = 640, heightDp = 320)
@Composable
fun DocumentScreenPreviewInLandscapeMode() {
DocumentScreenPreview()
}

View File

@@ -67,6 +67,7 @@ fun CommonPageList(
val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE
if (isLandscape) {
LazyColumn (
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
) {
itemsIndexed(state.pageIds) { index, id ->

View File

@@ -0,0 +1,132 @@
/*
* Copyright 2025 Pierre-Yves Nicolas
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option)
* any later version.
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.mydomain.myscan.view
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.dp
@Composable
fun MyScaffold(
toAboutScreen: () -> Unit,
pageListState: CommonPageListState,
pageListButton: (@Composable () -> Unit)? = null,
bottomBar: @Composable () -> Unit,
onBack: (() -> Unit)? = null,
content: @Composable (Modifier) -> Unit,
) {
Box {
if (!isLandscape(LocalConfiguration.current)) {
Scaffold(
bottomBar = { DocumentBar(pageListState, bottomBar, Modifier, pageListButton) }
) { innerPadding ->
content(Modifier.padding(innerPadding).fillMaxSize())
}
} else {
Scaffold { innerPadding ->
Row(
modifier = Modifier.padding(innerPadding).fillMaxSize()
) {
content(Modifier.weight(2f))
DocumentBar(pageListState, bottomBar, Modifier.weight(1f), pageListButton)
}
}
}
if (onBack != null) {
BackButton(
onBack,
modifier = Modifier.align(Alignment.TopStart).windowInsetsPadding(WindowInsets.safeDrawing)
)
}
AboutScreenNavButton(
onClick = toAboutScreen,
modifier = Modifier.align(Alignment.TopEnd).windowInsetsPadding(WindowInsets.safeDrawing)
)
}
}
@Composable
fun DocumentBar(
pageListState: CommonPageListState,
buttonBar: @Composable () -> Unit,
modifier: Modifier = Modifier,
pageListButton: (@Composable () -> Unit)? = null,
) {
Column (
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier.background(MaterialTheme.colorScheme.surfaceContainer)
) {
var modifier: Modifier = Modifier
if (isLandscape(LocalConfiguration.current)) {
modifier = modifier.weight(1f).fillMaxWidth()
}
Box (modifier) {
CommonPageList(pageListState, modifier = Modifier.fillMaxWidth())
if (pageListButton != null) {
Box (Modifier
.align(Alignment.BottomEnd)
.padding(horizontal = 8.dp, vertical = 16.dp)
) {
pageListButton()
}
}
}
BottomAppBar(
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
) {
if (isLandscape(LocalConfiguration.current)) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
buttonBar()
}
} else {
Row(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 1.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
buttonBar()
}
}
}
}
}
fun isLandscape(configuration: Configuration): Boolean {
return configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
}