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