diff --git a/app/src/main/java/org/mydomain/myscan/MainActivity.kt b/app/src/main/java/org/mydomain/myscan/MainActivity.kt index b342a47..09caab6 100644 --- a/app/src/main/java/org/mydomain/myscan/MainActivity.kt +++ b/app/src/main/java/org/mydomain/myscan/MainActivity.kt @@ -58,7 +58,7 @@ class MainActivity : ComponentActivity() { setContent { val currentScreen by viewModel.currentScreen.collectAsStateWithLifecycle() val liveAnalysisState by viewModel.liveAnalysisState.collectAsStateWithLifecycle() - val pageIds by viewModel.pageIds.collectAsStateWithLifecycle() + val document by viewModel.documentUiModel.collectAsStateWithLifecycle() MyScanTheme { val navigation = Navigation( toCameraScreen = { viewModel.navigateTo(Screen.Camera) }, @@ -78,9 +78,8 @@ class MainActivity : ComponentActivity() { } is Screen.Document -> { DocumentScreen ( - pageIds, + document = document, initialPage = screen.initialPage, - imageLoader = { id -> viewModel.getBitmap(id) }, navigation = navigation, pdfActions = PdfGenerationActions( startGeneration = viewModel::startPdfGeneration, diff --git a/app/src/main/java/org/mydomain/myscan/MainViewModel.kt b/app/src/main/java/org/mydomain/myscan/MainViewModel.kt index c9df5b8..86d36ee 100644 --- a/app/src/main/java/org/mydomain/myscan/MainViewModel.kt +++ b/app/src/main/java/org/mydomain/myscan/MainViewModel.kt @@ -38,6 +38,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.mydomain.myscan.ui.PdfGenerationUiState +import org.mydomain.myscan.view.DocumentUiModel import java.io.ByteArrayOutputStream import java.io.File @@ -71,8 +72,18 @@ class MainViewModel( val currentScreen: StateFlow = _screenStack.map { it.last() } .stateIn(viewModelScope, SharingStarted.Eagerly, Screen.Camera) - private val _pageIds = MutableStateFlow>(imageRepository.imageIds()) - val pageIds: StateFlow> = _pageIds + private val _pageIds = MutableStateFlow(imageRepository.imageIds()) + val documentUiModel: StateFlow = + _pageIds.map { ids -> + DocumentUiModel( + pageIds = ids, + imageLoader = ::getBitmap + ) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = DocumentUiModel(emptyList(), ::getBitmap) + ) private val _captureState = MutableStateFlow(CaptureState.Idle) val captureState: StateFlow = _captureState 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 c0cacee..6db77c7 100644 --- a/app/src/main/java/org/mydomain/myscan/view/CameraScreen.kt +++ b/app/src/main/java/org/mydomain/myscan/view/CameraScreen.kt @@ -101,7 +101,7 @@ fun CameraScreen( onFinalizePressed: () -> Unit, ) { var previewView by remember { mutableStateOf(null) } - val pageIds by viewModel.pageIds.collectAsStateWithLifecycle() + val document by viewModel.documentUiModel.collectAsStateWithLifecycle() val thumbnailCoords = remember { mutableStateOf(Offset.Zero) } var isDebugMode by remember { mutableStateOf(false) } @@ -129,9 +129,9 @@ fun CameraScreen( } val listState = rememberLazyListState() - LaunchedEffect(pageIds.size) { - if (pageIds.isNotEmpty()) { - listState.animateScrollToItem(pageIds.lastIndex) + LaunchedEffect(document.pageCount()) { + if (!document.isEmpty()) { + listState.animateScrollToItem(document.lastIndex()) } } val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE @@ -145,14 +145,13 @@ fun CameraScreen( }, pageListState = CommonPageListState( - pageIds = pageIds, - imageLoader = { id -> viewModel.getBitmap(id) }, + document = document, onPageClick = { index -> viewModel.navigateTo(Screen.Document(index)) }, listState = listState, onLastItemPosition = { offset -> thumbnailCoords.value = offset }, ), cameraUiState = CameraUiState( - pageIds.size, + document.pageCount(), liveAnalysisState, captureState, showDetectionError, @@ -457,12 +456,13 @@ private fun ScreenPreview(captureState: CaptureState, rotationDegrees: Float = 0 }, pageListState = CommonPageListState( - pageIds = listOf(1, 2, 2, 2).map { "gallica.bnf.fr-bpt6k5530456s-$it.jpg" }, - imageLoader = { id -> - context.assets.open(id).use { input -> - BitmapFactory.decodeStream(input) - } - }, + document = DocumentUiModel( + pageIds = listOf(1, 2, 2, 2).map { "gallica.bnf.fr-bpt6k5530456s-$it.jpg" }, + imageLoader = { id -> + context.assets.open(id).use { input -> + BitmapFactory.decodeStream(input) + } + }), onPageClick = {}, listState = LazyListState(), ), 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 1bde9cc..1f4f575 100644 --- a/app/src/main/java/org/mydomain/myscan/view/DocumentScreen.kt +++ b/app/src/main/java/org/mydomain/myscan/view/DocumentScreen.kt @@ -14,7 +14,6 @@ */ package org.mydomain.myscan.view -import android.graphics.Bitmap import android.graphics.BitmapFactory import androidx.activity.compose.BackHandler import androidx.compose.foundation.Image @@ -68,9 +67,8 @@ import org.mydomain.myscan.ui.theme.MyScanTheme @OptIn(ExperimentalMaterial3Api::class) @Composable fun DocumentScreen( - pageIds: List, + document: DocumentUiModel, initialPage: Int, - imageLoader: (String) -> Bitmap?, navigation: Navigation, pdfActions: PdfGenerationActions, onStartNew: () -> Unit, @@ -80,8 +78,8 @@ fun DocumentScreen( val showNewDocDialog = rememberSaveable { mutableStateOf(false) } val showPdfDialog = rememberSaveable { mutableStateOf(false) } val currentPageIndex = rememberSaveable { mutableIntStateOf(initialPage) } - if (currentPageIndex.intValue >= pageIds.size) { - currentPageIndex.intValue = pageIds.size - 1 + if (currentPageIndex.intValue >= document.pageCount()) { + currentPageIndex.intValue = document.pageCount() - 1 } if (currentPageIndex.intValue < 0) { navigation.toCameraScreen() @@ -97,8 +95,7 @@ fun DocumentScreen( MyScaffold( toAboutScreen = navigation.toAboutScreen, pageListState = CommonPageListState( - pageIds, - imageLoader, + document, onPageClick = { index -> currentPageIndex.intValue = index }, currentPageIndex = currentPageIndex.intValue, listState = listState, @@ -115,7 +112,7 @@ fun DocumentScreen( ) }, ) { modifier -> - DocumentPreview(pageIds, imageLoader, currentPageIndex, onDeleteImage, modifier) + DocumentPreview(document, currentPageIndex, onDeleteImage, modifier) if (showNewDocDialog.value) { NewDocumentDialog(onConfirm = onStartNew, showNewDocDialog) } @@ -130,13 +127,12 @@ fun DocumentScreen( @Composable private fun DocumentPreview( - pageIds: List, - imageLoader: (String) -> Bitmap?, + document: DocumentUiModel, currentPageIndex: MutableIntState, onDeleteImage: (String) -> Unit, modifier: Modifier, ) { - val imageId = pageIds[currentPageIndex.intValue] + val imageId = document.pageId(currentPageIndex.intValue) Column ( modifier = modifier .background(MaterialTheme.colorScheme.surfaceContainerLow) @@ -144,7 +140,7 @@ private fun DocumentPreview( Box ( modifier = Modifier.fillMaxSize() ) { - val bitmap = imageLoader(imageId) + val bitmap = document.load(currentPageIndex.intValue) if (bitmap != null) { val imageBitmap = bitmap.asImageBitmap() val zoomState = rememberZoomState( @@ -175,7 +171,7 @@ private fun DocumentPreview( .align(Alignment.BottomEnd) .padding(8.dp) ) - Text("${currentPageIndex.intValue + 1} / ${pageIds.size}", + Text("${currentPageIndex.intValue + 1} / ${document.pageCount()}", color = MaterialTheme.colorScheme.inverseOnSurface, modifier = Modifier .align(Alignment.BottomStart) @@ -243,13 +239,15 @@ fun DocumentScreenPreview() { val context = LocalContext.current MyScanTheme { DocumentScreen( - pageIds = listOf(1, 2, 2, 2).map { "gallica.bnf.fr-bpt6k5530456s-$it.jpg" }, - initialPage = 1, - imageLoader = { id -> - context.assets.open(id).use { input -> - BitmapFactory.decodeStream(input) + DocumentUiModel( + listOf(1, 2, 2, 2).map { "gallica.bnf.fr-bpt6k5530456s-$it.jpg" }, + { id -> + context.assets.open(id).use { input -> + BitmapFactory.decodeStream(input) + } } - }, + ), + initialPage = 1, navigation = Navigation( {}, {}, {}, {}, {}), pdfActions = PdfGenerationActions( diff --git a/app/src/main/java/org/mydomain/myscan/view/DocumentUiModel.kt b/app/src/main/java/org/mydomain/myscan/view/DocumentUiModel.kt new file mode 100644 index 0000000..42a241f --- /dev/null +++ b/app/src/main/java/org/mydomain/myscan/view/DocumentUiModel.kt @@ -0,0 +1,38 @@ +/* + * 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.graphics.Bitmap + +data class DocumentUiModel( + private val pageIds: List, + private val imageLoader: (String) -> Bitmap? +) { + fun pageCount(): Int { + return pageIds.size + } + fun pageId(index: Int): String { + return pageIds[index] + } + fun isEmpty(): Boolean { + return pageIds.isEmpty() + } + fun lastIndex(): Int { + return pageIds.lastIndex + } + fun load(index: Int): Bitmap? { + return imageLoader(pageIds[index]) + } +} \ No newline at end of file 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 cf464bf..0220266 100644 --- a/app/src/main/java/org/mydomain/myscan/view/PageList.kt +++ b/app/src/main/java/org/mydomain/myscan/view/PageList.kt @@ -28,9 +28,9 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -51,8 +51,7 @@ import androidx.compose.ui.unit.sp const val PAGE_LIST_ELEMENT_SIZE_DP = 120 data class CommonPageListState( - val pageIds: List, - val imageLoader: (String) -> Bitmap?, + val document: DocumentUiModel, val onPageClick: (Int) -> Unit, val listState: LazyListState, val currentPageIndex: Int? = null, @@ -65,37 +64,32 @@ fun CommonPageList( modifier: Modifier = Modifier, ) { val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE + val content: LazyListScope.() -> Unit = { + items(state.document.pageCount()) { index -> + // TODO Use small images rather than big ones + val image = state.document.load(index) + if (image != null) { + PageThumbnail(image, index, state) + } + } + } if (isLandscape) { LazyColumn ( horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier - ) { - itemsIndexed(state.pageIds) { index, id -> - // TODO Use small images rather than big ones - val image = state.imageLoader(id) - if (image != null) { - PageThumbnail(image, index, state) - } - } - } + modifier = modifier, + content = content, + ) } else { LazyRow ( state = state.listState, contentPadding = PaddingValues(4.dp), modifier = Modifier.fillMaxWidth().background(MaterialTheme.colorScheme.surfaceContainer), horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - itemsIndexed(state.pageIds) { index, id -> - // TODO Use small images rather than big ones - val image = state.imageLoader(id) - if (image != null) { - PageThumbnail(image, index, state) - } - } - } + verticalAlignment = Alignment.CenterVertically, + content = content, + ) } - if (state.pageIds.isEmpty()) { + if (state.document.isEmpty()) { Box( modifier = Modifier .height(120.dp) @@ -120,7 +114,7 @@ private fun PageThumbnail( Modifier.height(maxImageSize) else Modifier.width(maxImageSize) - if (index == state.pageIds.lastIndex) { + if (index == state.document.lastIndex()) { val density = LocalDensity.current modifier = modifier.addPositionCallback(state.onLastItemPosition, density, 1.0f) }