From 6e30861dbf91292c9fef5ce0b46a6726c05d745e Mon Sep 17 00:00:00 2001 From: Pierre-Yves Nicolas <6371790+pynicolas@users.noreply.github.com> Date: Wed, 16 Jul 2025 13:42:05 +0200 Subject: [PATCH] DocumentScreen: Landscape mode --- .../java/org/mydomain/myscan/view/Buttons.kt | 4 +- .../org/mydomain/myscan/view/CameraScreen.kt | 115 +++------------ .../mydomain/myscan/view/DocumentScreen.kt | 90 +++++------- .../java/org/mydomain/myscan/view/PageList.kt | 1 + .../java/org/mydomain/myscan/view/Scaffold.kt | 132 ++++++++++++++++++ 5 files changed, 190 insertions(+), 152 deletions(-) create mode 100644 app/src/main/java/org/mydomain/myscan/view/Scaffold.kt diff --git a/app/src/main/java/org/mydomain/myscan/view/Buttons.kt b/app/src/main/java/org/mydomain/myscan/view/Buttons.kt index 105f829..63b4937 100644 --- a/app/src/main/java/org/mydomain/myscan/view/Buttons.kt +++ b/app/src/main/java/org/mydomain/myscan/view/Buttons.kt @@ -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) diff --git a/app/src/main/java/org/mydomain/myscan/view/CameraScreen.kt b/app/src/main/java/org/mydomain/myscan/view/CameraScreen.kt index 04cfae9..c0cacee 100644 --- a/app/src/main/java/org/mydomain/myscan/view/CameraScreen.kt +++ b/app/src/main/java/org/mydomain/myscan/view/CameraScreen.kt @@ -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, 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, ) } diff --git a/app/src/main/java/org/mydomain/myscan/view/DocumentScreen.kt b/app/src/main/java/org/mydomain/myscan/view/DocumentScreen.kt index 1752109..4bcd7c8 100644 --- a/app/src/main/java/org/mydomain/myscan/view/DocumentScreen.kt +++ b/app/src/main/java/org/mydomain/myscan/view/DocumentScreen.kt @@ -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, - imageLoader: (String) -> Bitmap?, - currentPageIndex: MutableState, - 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, @@ -296,3 +263,10 @@ fun DocumentScreenPreview() { ) } } + +@Preview(showBackground = true, widthDp = 640, heightDp = 320) +@Composable +fun DocumentScreenPreviewInLandscapeMode() { + DocumentScreenPreview() +} + diff --git a/app/src/main/java/org/mydomain/myscan/view/PageList.kt b/app/src/main/java/org/mydomain/myscan/view/PageList.kt index 74c82b7..cf464bf 100644 --- a/app/src/main/java/org/mydomain/myscan/view/PageList.kt +++ b/app/src/main/java/org/mydomain/myscan/view/PageList.kt @@ -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 -> diff --git a/app/src/main/java/org/mydomain/myscan/view/Scaffold.kt b/app/src/main/java/org/mydomain/myscan/view/Scaffold.kt new file mode 100644 index 0000000..e79b754 --- /dev/null +++ b/app/src/main/java/org/mydomain/myscan/view/Scaffold.kt @@ -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 . + */ +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 +}