Refactoring: introduce DocumentUiModel (#13)
* Refactoring: introduce DocumentUiModel * Make DocumentUiModel.imageLoader private * Make DocumentUiModel.pageIds private * Expose DocumentUiModel in MainViewModel * Rename variables named documentUiModel
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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<Screen> = _screenStack.map { it.last() }
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, Screen.Camera)
|
||||
|
||||
private val _pageIds = MutableStateFlow<List<String>>(imageRepository.imageIds())
|
||||
val pageIds: StateFlow<List<String>> = _pageIds
|
||||
private val _pageIds = MutableStateFlow(imageRepository.imageIds())
|
||||
val documentUiModel: StateFlow<DocumentUiModel> =
|
||||
_pageIds.map { ids ->
|
||||
DocumentUiModel(
|
||||
pageIds = ids,
|
||||
imageLoader = ::getBitmap
|
||||
)
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = DocumentUiModel(emptyList(), ::getBitmap)
|
||||
)
|
||||
|
||||
private val _captureState = MutableStateFlow<CaptureState>(CaptureState.Idle)
|
||||
val captureState: StateFlow<CaptureState> = _captureState
|
||||
|
||||
@@ -101,7 +101,7 @@ fun CameraScreen(
|
||||
onFinalizePressed: () -> Unit,
|
||||
) {
|
||||
var previewView by remember { mutableStateOf<PreviewView?>(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(),
|
||||
),
|
||||
|
||||
@@ -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<String>,
|
||||
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<String>,
|
||||
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(
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.mydomain.myscan.view
|
||||
|
||||
import android.graphics.Bitmap
|
||||
|
||||
data class DocumentUiModel(
|
||||
private val pageIds: List<String>,
|
||||
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])
|
||||
}
|
||||
}
|
||||
@@ -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<String>,
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user