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,
currentDocument = document,
navigation = navigation,
onStartNewScan = navigation.toCameraScreen,
onClearScan = { viewModel.startNewDocument() },
recentDocuments = recentDocs,
onOpenPdf = { file -> openPdf(file.toUri()) }
)

View File

@@ -355,7 +355,7 @@ class MainViewModel(
current.toBuilder()
.addDocuments(0, newDoc)
.also { builder ->
while (builder.documentsCount > 10) {
while (builder.documentsCount > 3) {
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.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
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.PictureAsPdf
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
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.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
@@ -65,11 +70,10 @@ fun HomeScreen(
cameraPermission: CameraPermissionState,
currentDocument: DocumentUiModel,
navigation: Navigation,
onStartNewScan: () -> Unit,
onClearScan: () -> Unit,
recentDocuments: List<RecentDocumentUiState>,
onOpenPdf: (File) -> Unit,
) {
val showCloseDocDialog = rememberSaveable { mutableStateOf(false) }
Scaffold (
topBar = {
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 ->
Column (
modifier = Modifier
@@ -105,26 +90,31 @@ fun HomeScreen(
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
Spacer(Modifier.weight(1f))
if (!cameraPermission.isGranted) {
CameraPermissionRationale(cameraPermission)
} else {
ScanButton(
onClick = {
onClearScan()
navigation.toCameraScreen()
},
Modifier.align(Alignment.CenterHorizontally)
)
}
Spacer(Modifier.weight(1f))
if (!currentDocument.isEmpty()) {
SectionTitle(stringResource(R.string.current_document))
CurrentDocumentCard(currentDocument, navigation)
}
if (recentDocuments.isNotEmpty()) {
SectionTitle(stringResource(R.string.last_saved_documents))
OngoingScanBanner(
currentDocument,
onResumeScan = navigation.toDocumentScreen,
onClearScan = onClearScan,
)
} else if (recentDocuments.isNotEmpty()) {
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)) {
Text(
stringResource(R.string.camera_permission_rationale),
style = MaterialTheme.typography.bodyMedium
)
Spacer(Modifier.height(8.dp))
Button(onClick = { cameraPermission.request() }) {
Button(
onClick = { cameraPermission.request() },
modifier = Modifier.align(Alignment.CenterHorizontally)
) {
Text(stringResource(R.string.grant_permission))
}
}
@@ -150,33 +142,82 @@ private fun CameraPermissionRationale(cameraPermission: CameraPermissionState) {
}
@Composable
private fun CurrentDocumentCard(
currentDocument: DocumentUiModel,
navigation: Navigation,
fun ScanButton(onClick: () -> Unit, modifier: Modifier) {
Button(
onClick = onClick,
modifier = modifier.padding(32.dp),
elevation = ButtonDefaults.buttonElevation(defaultElevation = 6.dp)
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 6.dp)
Icon(
imageVector = Icons.Default.PhotoCamera,
contentDescription = null,
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(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(12.dp)
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
currentDocument.load(0)?.let {
Image(
bitmap = it.asImageBitmap(),
contentDescription = null,
modifier = Modifier
.height(100.dp)
.padding(4.dp)
.size(60.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))
Column(Modifier.weight(1f)) {
Text(pageCountText(currentDocument.pageCount()))
Button(onClick = onResumeScan) {
Text(stringResource(R.string.resume))
}
MainActionButton(navigation.toDocumentScreen, stringResource(R.string.open))
}
}
}
@@ -186,8 +227,13 @@ private fun RecentDocumentList(
recentDocuments: List<RecentDocumentUiState>,
onOpenPdf: (File) -> Unit
) {
HorizontalDivider()
Text(
stringResource(R.string.last_saved_documents),
modifier = Modifier.padding(start = 12.dp, top = 16.dp, bottom = 8.dp)
)
Column {
val maxListSize = 5
val maxListSize = 3
recentDocuments.subList(0, min(maxListSize, recentDocuments.size)).forEach { doc ->
ListItem(
headlineContent = { Text(doc.file.name) },
@@ -202,29 +248,19 @@ private fun RecentDocumentList(
},
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
@Composable
fun HomeScreenPreviewOnFirstLaunch() {
MyScanTheme {
HomeScreen(
cameraPermission = rememberCameraPermissionState(),
currentDocument = DocumentUiModel(listOf()) { _ -> null },
currentDocument = fakeDocument(),
navigation = dummyNavigation(),
onStartNewScan = {},
onClearScan = {},
recentDocuments = listOf(),
onOpenPdf = {},
)
@@ -241,7 +277,22 @@ fun HomeScreenPreviewWithCurrentDocument() {
listOf("gallica.bnf.fr-bpt6k5530456s-1.jpg"),
LocalContext.current),
navigation = dummyNavigation(),
onStartNewScan = {},
onClearScan = {},
recentDocuments = listOf(),
onOpenPdf = {},
)
}
}
@Preview
@Composable
fun HomeScreenPreviewWithLastSavedFiles() {
MyScanTheme {
HomeScreen(
cameraPermission = rememberCameraPermissionState(),
currentDocument = fakeDocument(),
navigation = dummyNavigation(),
onClearScan = {},
recentDocuments = listOf(
RecentDocumentUiState(File("/path/my_file.pdf"), 1755971180000, 3),
RecentDocumentUiState(File("/path/scan2.pdf"), 1755000500000, 1)

View File

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

View File

@@ -13,6 +13,7 @@
<string name="current_document">Aktuelles Dokument</string>
<string name="delete_page">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="end_scan">Scan beenden</string>
<string name="error">Fehler: %1$s</string>
@@ -34,10 +35,12 @@
<string name="open">Öffnen</string>
<string name="open_pdf">PDF öffnen</string>
<string name="pdf_saved_to">PDF gespeichert unter %1$s</string>
<string name="resume">Fortsetzen</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_pdf">PDF teilen</string>
<string name="start_a_new_scan">Neuen Scan starten</string>
<string name="unknown_size">Unbekannte Größe</string>
<string name="version">Version</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="delete_page">Supprimer la 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="end_scan">Terminer le scan</string>
<string name="error">Erreur : %1$s</string>
@@ -34,10 +35,12 @@
<string name="open">Ouvrir</string>
<string name="open_pdf">Ouvrir le PDF</string>
<string name="pdf_saved_to">PDF enregistré dans %1$s</string>
<string name="resume">Reprendre</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_pdf">Partager le PDF</string>
<string name="start_a_new_scan">Nouveau scan</string>
<string name="unknown_size">Taille inconnue</string>
<string name="version">Version</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="delete_page">Delete 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="end_scan">End scan</string>
<string name="error">Error: %1$s</string>
@@ -35,10 +36,12 @@
<string name="open">Open</string>
<string name="open_pdf">Open PDF</string>
<string name="pdf_saved_to">PDF saved to %1$s</string>
<string name="resume">Resume</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_pdf">Share PDF</string>
<string name="start_a_new_scan">Start a new scan</string>
<string name="unknown_size">Unknown size</string>
<string name="version">Version</string>
<string name="view_the_full_license">View the full license</string>