Home screen: change layout to focus on "New scan"

This commit is contained in:
Pierre-Yves Nicolas
2025-08-31 11:36:40 +02:00
parent 0b01a836f6
commit bbee4baec3
7 changed files with 135 additions and 71 deletions

View File

@@ -80,7 +80,7 @@ class MainActivity : ComponentActivity() {
cameraPermission = cameraPermission, cameraPermission = cameraPermission,
currentDocument = document, currentDocument = document,
navigation = navigation, navigation = navigation,
onStartNewScan = navigation.toCameraScreen, onClearScan = { viewModel.startNewDocument() },
recentDocuments = recentDocs, recentDocuments = recentDocs,
onOpenPdf = { file -> openPdf(file.toUri()) } onOpenPdf = { file -> openPdf(file.toUri()) }
) )

View File

@@ -355,7 +355,7 @@ class MainViewModel(
current.toBuilder() current.toBuilder()
.addDocuments(0, newDoc) .addDocuments(0, newDoc)
.also { builder -> .also { builder ->
while (builder.documentsCount > 10) { while (builder.documentsCount > 3) {
builder.removeDocuments(builder.documentsCount - 1) builder.removeDocuments(builder.documentsCount - 1)
} }
} }

View File

@@ -23,29 +23,34 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DeleteOutline
import androidx.compose.material.icons.filled.PhotoCamera import androidx.compose.material.icons.filled.PhotoCamera
import androidx.compose.material.icons.filled.PictureAsPdf import androidx.compose.material.icons.filled.PictureAsPdf
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@@ -65,11 +70,10 @@ fun HomeScreen(
cameraPermission: CameraPermissionState, cameraPermission: CameraPermissionState,
currentDocument: DocumentUiModel, currentDocument: DocumentUiModel,
navigation: Navigation, navigation: Navigation,
onStartNewScan: () -> Unit, onClearScan: () -> Unit,
recentDocuments: List<RecentDocumentUiState>, recentDocuments: List<RecentDocumentUiState>,
onOpenPdf: (File) -> Unit, onOpenPdf: (File) -> Unit,
) { ) {
val showCloseDocDialog = rememberSaveable { mutableStateOf(false) }
Scaffold ( Scaffold (
topBar = { topBar = {
TopAppBar( TopAppBar(
@@ -79,25 +83,6 @@ fun HomeScreen(
} }
) )
}, },
bottomBar = {
BottomAppBar {
Spacer(Modifier.weight(1f))
MainActionButton(
onClick = {
if (currentDocument.isEmpty()) {
onStartNewScan()
} else {
showCloseDocDialog.value = true
}
},
icon = Icons.Default.PhotoCamera,
text = stringResource(R.string.start_a_new_scan),
modifier = Modifier
.padding(12.dp)
.height(48.dp),
)
}
}
) { padding -> ) { padding ->
Column ( Column (
modifier = Modifier modifier = Modifier
@@ -105,26 +90,31 @@ fun HomeScreen(
.fillMaxSize() .fillMaxSize()
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
Spacer(Modifier.weight(1f))
if (!cameraPermission.isGranted) { if (!cameraPermission.isGranted) {
CameraPermissionRationale(cameraPermission) CameraPermissionRationale(cameraPermission)
} else {
ScanButton(
onClick = {
onClearScan()
navigation.toCameraScreen()
},
Modifier.align(Alignment.CenterHorizontally)
)
} }
Spacer(Modifier.weight(1f))
if (!currentDocument.isEmpty()) { if (!currentDocument.isEmpty()) {
SectionTitle(stringResource(R.string.current_document)) OngoingScanBanner(
CurrentDocumentCard(currentDocument, navigation) currentDocument,
} onResumeScan = navigation.toDocumentScreen,
onClearScan = onClearScan,
if (recentDocuments.isNotEmpty()) { )
SectionTitle(stringResource(R.string.last_saved_documents)) } else if (recentDocuments.isNotEmpty()) {
RecentDocumentList(recentDocuments, onOpenPdf) RecentDocumentList(recentDocuments, onOpenPdf)
} }
if (showCloseDocDialog.value) {
NewDocumentDialog(
onConfirm = onStartNewScan,
showCloseDocDialog,
stringResource(R.string.new_document))
}
} }
} }
} }
@@ -139,10 +129,12 @@ private fun CameraPermissionRationale(cameraPermission: CameraPermissionState) {
Column(Modifier.padding(16.dp)) { Column(Modifier.padding(16.dp)) {
Text( Text(
stringResource(R.string.camera_permission_rationale), stringResource(R.string.camera_permission_rationale),
style = MaterialTheme.typography.bodyMedium
) )
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
Button(onClick = { cameraPermission.request() }) { Button(
onClick = { cameraPermission.request() },
modifier = Modifier.align(Alignment.CenterHorizontally)
) {
Text(stringResource(R.string.grant_permission)) Text(stringResource(R.string.grant_permission))
} }
} }
@@ -150,33 +142,82 @@ private fun CameraPermissionRationale(cameraPermission: CameraPermissionState) {
} }
@Composable @Composable
private fun CurrentDocumentCard( fun ScanButton(onClick: () -> Unit, modifier: Modifier) {
currentDocument: DocumentUiModel, Button(
navigation: Navigation, onClick = onClick,
modifier = modifier.padding(32.dp),
elevation = ButtonDefaults.buttonElevation(defaultElevation = 6.dp)
) { ) {
Card( Icon(
modifier = Modifier imageVector = Icons.Default.PhotoCamera,
.fillMaxWidth() contentDescription = null,
.padding(horizontal = 12.dp, vertical = 6.dp) modifier = Modifier.size(48.dp)
)
Spacer(Modifier.width(8.dp))
Text(
text = stringResource(R.string.scan_button),
style = MaterialTheme.typography.titleLarge
)
}
}
@Composable
fun OngoingScanBanner(
currentDocument: DocumentUiModel,
onResumeScan: () -> Unit,
onClearScan: () -> Unit
) {
Surface(
tonalElevation = 2.dp,
shadowElevation = 4.dp,
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, modifier = Modifier
modifier = Modifier.padding(12.dp) .fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) { ) {
currentDocument.load(0)?.let { currentDocument.load(0)?.let {
Image( Image(
bitmap = it.asImageBitmap(), bitmap = it.asImageBitmap(),
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
.height(100.dp) .size(60.dp)
.padding(4.dp) .clip(RoundedCornerShape(4.dp)),
contentScale = ContentScale.Fit
)
}
Spacer(Modifier.width(12.dp))
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = stringResource(R.string.scan_in_progress),
style = MaterialTheme.typography.bodyLarge
)
Text(
text = pageCountText(currentDocument.pageCount()),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f))
}
IconButton(
onClick = onClearScan,
modifier = Modifier.size(24.dp)
) {
Icon(
imageVector = Icons.Default.DeleteOutline,
contentDescription = stringResource(R.string.discard_scan),
tint = MaterialTheme.colorScheme.primary
) )
} }
Spacer(Modifier.width(12.dp)) Spacer(Modifier.width(12.dp))
Column(Modifier.weight(1f)) { Button(onClick = onResumeScan) {
Text(pageCountText(currentDocument.pageCount())) Text(stringResource(R.string.resume))
} }
MainActionButton(navigation.toDocumentScreen, stringResource(R.string.open))
} }
} }
} }
@@ -186,8 +227,13 @@ private fun RecentDocumentList(
recentDocuments: List<RecentDocumentUiState>, recentDocuments: List<RecentDocumentUiState>,
onOpenPdf: (File) -> Unit onOpenPdf: (File) -> Unit
) { ) {
HorizontalDivider()
Text(
stringResource(R.string.last_saved_documents),
modifier = Modifier.padding(start = 12.dp, top = 16.dp, bottom = 8.dp)
)
Column { Column {
val maxListSize = 5 val maxListSize = 3
recentDocuments.subList(0, min(maxListSize, recentDocuments.size)).forEach { doc -> recentDocuments.subList(0, min(maxListSize, recentDocuments.size)).forEach { doc ->
ListItem( ListItem(
headlineContent = { Text(doc.file.name) }, headlineContent = { Text(doc.file.name) },
@@ -202,29 +248,19 @@ private fun RecentDocumentList(
}, },
modifier = Modifier.clickable { onOpenPdf(doc.file) } modifier = Modifier.clickable { onOpenPdf(doc.file) }
) )
HorizontalDivider()
} }
} }
} }
@Composable
private fun SectionTitle(text: String) {
Text(
text,
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(start = 12.dp, top = 16.dp, bottom = 8.dp)
)
}
@Preview @Preview
@Composable @Composable
fun HomeScreenPreviewOnFirstLaunch() { fun HomeScreenPreviewOnFirstLaunch() {
MyScanTheme { MyScanTheme {
HomeScreen( HomeScreen(
cameraPermission = rememberCameraPermissionState(), cameraPermission = rememberCameraPermissionState(),
currentDocument = DocumentUiModel(listOf()) { _ -> null }, currentDocument = fakeDocument(),
navigation = dummyNavigation(), navigation = dummyNavigation(),
onStartNewScan = {}, onClearScan = {},
recentDocuments = listOf(), recentDocuments = listOf(),
onOpenPdf = {}, onOpenPdf = {},
) )
@@ -241,7 +277,22 @@ fun HomeScreenPreviewWithCurrentDocument() {
listOf("gallica.bnf.fr-bpt6k5530456s-1.jpg"), listOf("gallica.bnf.fr-bpt6k5530456s-1.jpg"),
LocalContext.current), LocalContext.current),
navigation = dummyNavigation(), navigation = dummyNavigation(),
onStartNewScan = {}, onClearScan = {},
recentDocuments = listOf(),
onOpenPdf = {},
)
}
}
@Preview
@Composable
fun HomeScreenPreviewWithLastSavedFiles() {
MyScanTheme {
HomeScreen(
cameraPermission = rememberCameraPermissionState(),
currentDocument = fakeDocument(),
navigation = dummyNavigation(),
onClearScan = {},
recentDocuments = listOf( recentDocuments = listOf(
RecentDocumentUiState(File("/path/my_file.pdf"), 1755971180000, 3), RecentDocumentUiState(File("/path/my_file.pdf"), 1755971180000, 3),
RecentDocumentUiState(File("/path/scan2.pdf"), 1755000500000, 1) RecentDocumentUiState(File("/path/scan2.pdf"), 1755000500000, 1)

View File

@@ -22,6 +22,10 @@ fun dummyNavigation(): Navigation {
return Navigation({}, {}, {}, {}, {}, {}, {}) return Navigation({}, {}, {}, {}, {}, {}, {})
} }
fun fakeDocument(): DocumentUiModel {
return DocumentUiModel(listOf()) { _ -> null }
}
fun fakeDocument(pageIds: List<String>, context: Context): DocumentUiModel { fun fakeDocument(pageIds: List<String>, context: Context): DocumentUiModel {
return DocumentUiModel(pageIds) { id -> return DocumentUiModel(pageIds) { id ->
context.assets.open(id).use { input -> context.assets.open(id).use { input ->

View File

@@ -13,6 +13,7 @@
<string name="current_document">Aktuelles Dokument</string> <string name="current_document">Aktuelles Dokument</string>
<string name="delete_page">Seite löschen</string> <string name="delete_page">Seite löschen</string>
<string name="delete_page_warning">Möchten Sie diese Seite löschen?</string> <string name="delete_page_warning">Möchten Sie diese Seite löschen?</string>
<string name="discard_scan">Löschen</string>
<string name="document">Dokument</string> <string name="document">Dokument</string>
<string name="end_scan">Scan beenden</string> <string name="end_scan">Scan beenden</string>
<string name="error">Fehler: %1$s</string> <string name="error">Fehler: %1$s</string>
@@ -34,10 +35,12 @@
<string name="open">Öffnen</string> <string name="open">Öffnen</string>
<string name="open_pdf">PDF öffnen</string> <string name="open_pdf">PDF öffnen</string>
<string name="pdf_saved_to">PDF gespeichert unter %1$s</string> <string name="pdf_saved_to">PDF gespeichert unter %1$s</string>
<string name="resume">Fortsetzen</string>
<string name="save">Speichern</string> <string name="save">Speichern</string>
<string name="scan_button">Neuer Scan</string>
<string name="scan_in_progress">Scan läuft</string>
<string name="share">Teilen</string> <string name="share">Teilen</string>
<string name="share_pdf">PDF teilen</string> <string name="share_pdf">PDF teilen</string>
<string name="start_a_new_scan">Neuen Scan starten</string>
<string name="unknown_size">Unbekannte Größe</string> <string name="unknown_size">Unbekannte Größe</string>
<string name="version">Version</string> <string name="version">Version</string>
<string name="view_the_full_license">Vollständige Lizenz anzeigen</string> <string name="view_the_full_license">Vollständige Lizenz anzeigen</string>

View File

@@ -13,6 +13,7 @@
<string name="current_document">Document en cours</string> <string name="current_document">Document en cours</string>
<string name="delete_page">Supprimer la page</string> <string name="delete_page">Supprimer la page</string>
<string name="delete_page_warning">Voulez-vous supprimer cette page ?</string> <string name="delete_page_warning">Voulez-vous supprimer cette page ?</string>
<string name="discard_scan">Supprimer le scan</string>
<string name="document">Document</string> <string name="document">Document</string>
<string name="end_scan">Terminer le scan</string> <string name="end_scan">Terminer le scan</string>
<string name="error">Erreur : %1$s</string> <string name="error">Erreur : %1$s</string>
@@ -34,10 +35,12 @@
<string name="open">Ouvrir</string> <string name="open">Ouvrir</string>
<string name="open_pdf">Ouvrir le PDF</string> <string name="open_pdf">Ouvrir le PDF</string>
<string name="pdf_saved_to">PDF enregistré dans %1$s</string> <string name="pdf_saved_to">PDF enregistré dans %1$s</string>
<string name="resume">Reprendre</string>
<string name="save">Enregistrer</string> <string name="save">Enregistrer</string>
<string name="scan_button">Nouveau scan</string>
<string name="scan_in_progress">Scan en cours</string>
<string name="share">Partager</string> <string name="share">Partager</string>
<string name="share_pdf">Partager le PDF</string> <string name="share_pdf">Partager le PDF</string>
<string name="start_a_new_scan">Nouveau scan</string>
<string name="unknown_size">Taille inconnue</string> <string name="unknown_size">Taille inconnue</string>
<string name="version">Version</string> <string name="version">Version</string>
<string name="view_the_full_license">Voir la licence complète</string> <string name="view_the_full_license">Voir la licence complète</string>

View File

@@ -14,6 +14,7 @@
<string name="current_document">Current document</string> <string name="current_document">Current document</string>
<string name="delete_page">Delete page</string> <string name="delete_page">Delete page</string>
<string name="delete_page_warning">Do you want to delete this page?</string> <string name="delete_page_warning">Do you want to delete this page?</string>
<string name="discard_scan">Discard scan</string>
<string name="document">Document</string> <string name="document">Document</string>
<string name="end_scan">End scan</string> <string name="end_scan">End scan</string>
<string name="error">Error: %1$s</string> <string name="error">Error: %1$s</string>
@@ -35,10 +36,12 @@
<string name="open">Open</string> <string name="open">Open</string>
<string name="open_pdf">Open PDF</string> <string name="open_pdf">Open PDF</string>
<string name="pdf_saved_to">PDF saved to %1$s</string> <string name="pdf_saved_to">PDF saved to %1$s</string>
<string name="resume">Resume</string>
<string name="save">Save</string> <string name="save">Save</string>
<string name="scan_button">New Scan</string>
<string name="scan_in_progress">Scan in progress</string>
<string name="share">Share</string> <string name="share">Share</string>
<string name="share_pdf">Share PDF</string> <string name="share_pdf">Share PDF</string>
<string name="start_a_new_scan">Start a new scan</string>
<string name="unknown_size">Unknown size</string> <string name="unknown_size">Unknown size</string>
<string name="version">Version</string> <string name="version">Version</string>
<string name="view_the_full_license">View the full license</string> <string name="view_the_full_license">View the full license</string>