Home screen
This commit is contained in:
committed by
pynicolas
parent
e74bbcd0d6
commit
5c7d603c3e
@@ -40,6 +40,7 @@ import org.mydomain.myscan.ui.theme.MyScanTheme
|
||||
import org.mydomain.myscan.view.AboutScreen
|
||||
import org.mydomain.myscan.view.CameraScreen
|
||||
import org.mydomain.myscan.view.DocumentScreen
|
||||
import org.mydomain.myscan.view.HomeScreen
|
||||
import org.mydomain.myscan.view.LibrariesScreen
|
||||
import org.opencv.android.OpenCVLoader
|
||||
|
||||
@@ -61,6 +62,7 @@ class MainActivity : ComponentActivity() {
|
||||
val document by viewModel.documentUiModel.collectAsStateWithLifecycle()
|
||||
MyScanTheme {
|
||||
val navigation = Navigation(
|
||||
toHomeScreen = { viewModel.navigateTo(Screen.Home) },
|
||||
toCameraScreen = { viewModel.navigateTo(Screen.Camera) },
|
||||
toDocumentScreen = { viewModel.navigateTo(Screen.Document()) },
|
||||
toAboutScreen = { viewModel.navigateTo(Screen.About) },
|
||||
@@ -68,9 +70,18 @@ class MainActivity : ComponentActivity() {
|
||||
back = { viewModel.navigateBack() }
|
||||
)
|
||||
when (val screen = currentScreen) {
|
||||
is Screen.Home -> {
|
||||
HomeScreen(
|
||||
hasCameraPermission = hasCameraPermission(this),
|
||||
currentDocument = document,
|
||||
navigation = navigation,
|
||||
onStartNewScan = navigation.toCameraScreen,
|
||||
)
|
||||
}
|
||||
is Screen.Camera -> {
|
||||
CameraScreen(
|
||||
viewModel,
|
||||
navigation,
|
||||
liveAnalysisState,
|
||||
onImageAnalyzed = { image -> viewModel.liveAnalysis(image) },
|
||||
onFinalizePressed = { viewModel.navigateTo(Screen.Document()) },
|
||||
@@ -92,7 +103,7 @@ class MainActivity : ComponentActivity() {
|
||||
),
|
||||
onStartNew = {
|
||||
viewModel.startNewDocument()
|
||||
viewModel.navigateTo(Screen.Camera) },
|
||||
viewModel.navigateTo(Screen.Home) },
|
||||
onDeleteImage = { id -> viewModel.deletePage(id) }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -68,9 +68,9 @@ class MainViewModel(
|
||||
val liveAnalysisState: StateFlow<LiveAnalysisState> = _liveAnalysisState.asStateFlow()
|
||||
private var lastSuccessfulLiveAnalysisState: LiveAnalysisState? = null
|
||||
|
||||
private val _screenStack = MutableStateFlow<List<Screen>>(listOf(Screen.Camera))
|
||||
private val _screenStack = MutableStateFlow<List<Screen>>(listOf(Screen.Home))
|
||||
val currentScreen: StateFlow<Screen> = _screenStack.map { it.last() }
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, Screen.Camera)
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, _screenStack.value.last())
|
||||
|
||||
private val _pageIds = MutableStateFlow(imageRepository.imageIds())
|
||||
val documentUiModel: StateFlow<DocumentUiModel> =
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package org.mydomain.myscan
|
||||
|
||||
sealed class Screen {
|
||||
object Home : Screen()
|
||||
object Camera : Screen()
|
||||
data class Document(val initialPage: Int = 0) : Screen()
|
||||
object About : Screen()
|
||||
@@ -22,6 +23,7 @@ sealed class Screen {
|
||||
}
|
||||
|
||||
data class Navigation(
|
||||
val toHomeScreen: () -> Unit,
|
||||
val toCameraScreen: () -> Unit,
|
||||
val toDocumentScreen: () -> Unit,
|
||||
val toAboutScreen: () -> Unit,
|
||||
|
||||
53
app/src/main/java/org/mydomain/myscan/PermissionHelpers.kt
Normal file
53
app/src/main/java/org/mydomain/myscan/PermissionHelpers.kt
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.ManagedActivityResultLauncher
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.content.ContextCompat
|
||||
|
||||
fun hasCameraPermission(context: Context): Boolean {
|
||||
val camera = Manifest.permission.CAMERA
|
||||
return ContextCompat.checkSelfPermission(context, camera) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberCameraPermissionLauncher(
|
||||
onGranted: () -> Unit = {},
|
||||
onDenied: () -> Unit = {}
|
||||
): ManagedActivityResultLauncher<String, Boolean> {
|
||||
val context = LocalContext.current
|
||||
return rememberLauncherForActivityResult (
|
||||
ActivityResultContracts.RequestPermission()
|
||||
) { isGranted ->
|
||||
if (isGranted) {
|
||||
onGranted()
|
||||
} else {
|
||||
onDenied()
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.camera_permission_denied),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.FilledIconButton
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -38,13 +38,15 @@ import org.mydomain.myscan.R
|
||||
fun MainActionButton(
|
||||
onClick: () -> Unit,
|
||||
text: String,
|
||||
icon: ImageVector,
|
||||
icon: ImageVector? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
iconDescription: String? = null,
|
||||
enabled: Boolean = true,
|
||||
) {
|
||||
Button(onClick = onClick, enabled = enabled, modifier = modifier) {
|
||||
icon?.let {
|
||||
Icon(icon, contentDescription = iconDescription)
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(text)
|
||||
}
|
||||
@@ -93,7 +95,7 @@ fun AboutScreenNavButton(
|
||||
modifier = modifier
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Info,
|
||||
imageVector = Icons.Default.Info,
|
||||
contentDescription = stringResource(R.string.about),
|
||||
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f))
|
||||
}
|
||||
|
||||
@@ -14,14 +14,10 @@
|
||||
*/
|
||||
package org.mydomain.myscan.view
|
||||
|
||||
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||
import android.graphics.Bitmap
|
||||
import android.util.Log
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.camera.core.ImageAnalysis
|
||||
import androidx.camera.core.ImageCapture
|
||||
@@ -58,7 +54,8 @@ import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import org.mydomain.myscan.LiveAnalysisState
|
||||
import org.mydomain.myscan.Point
|
||||
import org.mydomain.myscan.R
|
||||
import org.mydomain.myscan.hasCameraPermission
|
||||
import org.mydomain.myscan.rememberCameraPermissionLauncher
|
||||
import org.mydomain.myscan.scaledTo
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
@@ -71,18 +68,10 @@ fun CameraPreview(
|
||||
onPreviewViewReady: (PreviewView) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val requestPermissionLauncher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.RequestPermission()
|
||||
) { isGranted: Boolean ->
|
||||
if (!isGranted) {
|
||||
Toast.makeText(context,
|
||||
context.getString(R.string.camera_permission_denied), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
val requestPermissionLauncher = rememberCameraPermissionLauncher(onGranted = {}, onDenied = {})
|
||||
LaunchedEffect(Unit) {
|
||||
val camera = android.Manifest.permission.CAMERA
|
||||
if (ContextCompat.checkSelfPermission(context, camera) != PERMISSION_GRANTED) {
|
||||
if (!hasCameraPermission(context)) {
|
||||
requestPermissionLauncher.launch(camera)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import android.content.res.Configuration
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.camera.core.ImageProxy
|
||||
import androidx.camera.view.PreviewView
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
@@ -77,6 +78,7 @@ import kotlinx.coroutines.delay
|
||||
import org.mydomain.myscan.LiveAnalysisState
|
||||
import org.mydomain.myscan.MainViewModel
|
||||
import org.mydomain.myscan.MainViewModel.CaptureState
|
||||
import org.mydomain.myscan.Navigation
|
||||
import org.mydomain.myscan.R
|
||||
import org.mydomain.myscan.Screen
|
||||
import org.mydomain.myscan.ui.theme.MyScanTheme
|
||||
@@ -96,6 +98,7 @@ const val ANIMATION_DURATION = 200
|
||||
@Composable
|
||||
fun CameraScreen(
|
||||
viewModel: MainViewModel,
|
||||
navigation: Navigation,
|
||||
liveAnalysisState: LiveAnalysisState,
|
||||
onImageAnalyzed: (ImageProxy) -> Unit,
|
||||
onFinalizePressed: () -> Unit,
|
||||
@@ -105,6 +108,8 @@ fun CameraScreen(
|
||||
val thumbnailCoords = remember { mutableStateOf(Offset.Zero) }
|
||||
var isDebugMode by remember { mutableStateOf(false) }
|
||||
|
||||
BackHandler { navigation.back() }
|
||||
|
||||
val captureController = remember { CameraCaptureController() }
|
||||
DisposableEffect(Unit) {
|
||||
onDispose { captureController.shutdown() }
|
||||
@@ -169,7 +174,7 @@ fun CameraScreen(
|
||||
onFinalizePressed = onFinalizePressed,
|
||||
onDebugModeSwitched = { isDebugMode = !isDebugMode },
|
||||
thumbnailCoords = thumbnailCoords,
|
||||
toAboutScreen = { viewModel.navigateTo(Screen.About) }
|
||||
navigation = navigation
|
||||
)
|
||||
}
|
||||
|
||||
@@ -182,7 +187,7 @@ private fun CameraScreenScaffold(
|
||||
onFinalizePressed: () -> Unit,
|
||||
onDebugModeSwitched: () -> Unit,
|
||||
thumbnailCoords: MutableState<Offset>,
|
||||
toAboutScreen: () -> Unit,
|
||||
navigation: Navigation,
|
||||
) {
|
||||
var tapCount by remember { mutableStateOf(0) }
|
||||
var lastTapTime by remember { mutableStateOf(0L) }
|
||||
@@ -203,8 +208,9 @@ private fun CameraScreenScaffold(
|
||||
|
||||
Box {
|
||||
MyScaffold(
|
||||
toAboutScreen = toAboutScreen,
|
||||
toAboutScreen = navigation.toAboutScreen,
|
||||
pageListState = pageListState,
|
||||
onBack = navigation.back,
|
||||
bottomBar = { Bar(cameraUiState.pageCount, onPageCountClick, onFinalizePressed) }
|
||||
) {
|
||||
modifier -> CameraPreviewBox(cameraPreview, cameraUiState, onCapture, modifier)
|
||||
@@ -436,7 +442,6 @@ fun CameraScreenPreviewInLandscapeMode() {
|
||||
|
||||
@Composable
|
||||
private fun ScreenPreview(captureState: CaptureState, rotationDegrees: Float = 0f) {
|
||||
val context = LocalContext.current
|
||||
MyScanTheme {
|
||||
val thumbnailCoords = remember { mutableStateOf(Offset.Zero) }
|
||||
CameraScreenScaffold(
|
||||
@@ -456,13 +461,9 @@ private fun ScreenPreview(captureState: CaptureState, rotationDegrees: Float = 0
|
||||
},
|
||||
pageListState =
|
||||
CommonPageListState(
|
||||
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)
|
||||
}
|
||||
}),
|
||||
document = fakeDocument(
|
||||
listOf(1, 2, 2, 2).map { "gallica.bnf.fr-bpt6k5530456s-$it.jpg" },
|
||||
LocalContext.current),
|
||||
onPageClick = {},
|
||||
listState = LazyListState(),
|
||||
),
|
||||
@@ -472,7 +473,7 @@ private fun ScreenPreview(captureState: CaptureState, rotationDegrees: Float = 0
|
||||
onFinalizePressed = {},
|
||||
onDebugModeSwitched = {},
|
||||
thumbnailCoords = thumbnailCoords,
|
||||
toAboutScreen = {}
|
||||
navigation = dummyNavigation()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
*/
|
||||
package org.mydomain.myscan.view
|
||||
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
@@ -31,8 +30,8 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.PictureAsPdf
|
||||
import androidx.compose.material.icons.filled.RestartAlt
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@@ -114,7 +113,7 @@ fun DocumentScreen(
|
||||
) { modifier ->
|
||||
DocumentPreview(document, currentPageIndex, onDeleteImage, modifier)
|
||||
if (showNewDocDialog.value) {
|
||||
NewDocumentDialog(onConfirm = onStartNew, showNewDocDialog)
|
||||
NewDocumentDialog(onConfirm = onStartNew, showNewDocDialog, stringResource(R.string.close_document))
|
||||
}
|
||||
if (showPdfDialog.value) {
|
||||
PdfGenerationBottomSheetWrapper(
|
||||
@@ -203,7 +202,7 @@ private fun BottomBar(
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
SecondaryActionButton(
|
||||
icon = Icons.Default.RestartAlt,
|
||||
icon = Icons.Default.Close,
|
||||
contentDescription = stringResource(R.string.restart),
|
||||
onClick = { showNewDocDialog.value = true },
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
@@ -212,9 +211,9 @@ private fun BottomBar(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NewDocumentDialog(onConfirm: () -> Unit, showDialog: MutableState<Boolean>) {
|
||||
fun NewDocumentDialog(onConfirm: () -> Unit, showDialog: MutableState<Boolean>, title: String) {
|
||||
AlertDialog(
|
||||
title = { Text(stringResource(R.string.new_document)) },
|
||||
title = { Text(title) },
|
||||
text = { Text(stringResource(R.string.new_document_warning)) },
|
||||
confirmButton = {
|
||||
TextButton (onClick = {
|
||||
@@ -236,20 +235,13 @@ fun NewDocumentDialog(onConfirm: () -> Unit, showDialog: MutableState<Boolean>)
|
||||
@Composable
|
||||
@Preview
|
||||
fun DocumentScreenPreview() {
|
||||
val context = LocalContext.current
|
||||
MyScanTheme {
|
||||
DocumentScreen(
|
||||
DocumentUiModel(
|
||||
fakeDocument(
|
||||
listOf(1, 2, 2, 2).map { "gallica.bnf.fr-bpt6k5530456s-$it.jpg" },
|
||||
{ id ->
|
||||
context.assets.open(id).use { input ->
|
||||
BitmapFactory.decodeStream(input)
|
||||
}
|
||||
}
|
||||
),
|
||||
LocalContext.current),
|
||||
initialPage = 1,
|
||||
navigation = Navigation(
|
||||
{}, {}, {}, {}, {}),
|
||||
navigation = dummyNavigation(),
|
||||
pdfActions = PdfGenerationActions(
|
||||
{}, {}, {},
|
||||
MutableStateFlow(PdfGenerationUiState()),
|
||||
|
||||
210
app/src/main/java/org/mydomain/myscan/view/HomeScreen.kt
Normal file
210
app/src/main/java/org/mydomain/myscan/view/HomeScreen.kt
Normal file
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
* 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.Manifest
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
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.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.PhotoCamera
|
||||
import androidx.compose.material3.BottomAppBar
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
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.graphics.asImageBitmap
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.mydomain.myscan.Navigation
|
||||
import org.mydomain.myscan.R
|
||||
import org.mydomain.myscan.rememberCameraPermissionLauncher
|
||||
import org.mydomain.myscan.ui.theme.MyScanTheme
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
// FIXME Extract strings
|
||||
fun HomeScreen(
|
||||
hasCameraPermission: Boolean,
|
||||
currentDocument: DocumentUiModel,
|
||||
navigation: Navigation,
|
||||
onStartNewScan: () -> Unit
|
||||
) {
|
||||
val showCloseDocDialog = rememberSaveable { mutableStateOf(false) }
|
||||
Scaffold (
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.app_name)) },
|
||||
actions = {
|
||||
AboutScreenNavButton(onClick = navigation.toAboutScreen)
|
||||
}
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
BottomAppBar {
|
||||
Spacer(Modifier.weight(1f))
|
||||
MainActionButton(
|
||||
onClick = {
|
||||
if (currentDocument.isEmpty()) {
|
||||
onStartNewScan()
|
||||
} else {
|
||||
showCloseDocDialog.value = true
|
||||
}
|
||||
},
|
||||
icon = Icons.Default.PhotoCamera,
|
||||
text = "Start a new scan",
|
||||
modifier = Modifier
|
||||
.padding(12.dp)
|
||||
.height(48.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
) { padding ->
|
||||
Column (
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
if (!hasCameraPermission) {
|
||||
CameraPermissionRationale()
|
||||
}
|
||||
|
||||
if (!currentDocument.isEmpty()) {
|
||||
SectionTitle("Current document")
|
||||
CurrentDocumentCard(currentDocument, navigation)
|
||||
}
|
||||
|
||||
if (showCloseDocDialog.value) {
|
||||
NewDocumentDialog(
|
||||
onConfirm = onStartNewScan,
|
||||
showCloseDocDialog,
|
||||
stringResource(R.string.new_document))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CameraPermissionRationale() {
|
||||
val requestPermissionLauncher = rememberCameraPermissionLauncher(onGranted = {}, onDenied = {})
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Column(Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
"The app requires camera access to scan documents. " +
|
||||
"Captured images are stored only on this device and will be deleted " +
|
||||
"when you close the current document.",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Button(onClick = {
|
||||
requestPermissionLauncher.launch(Manifest.permission.CAMERA)
|
||||
}) {
|
||||
Text("Grant permission")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CurrentDocumentCard(
|
||||
currentDocument: DocumentUiModel,
|
||||
navigation: Navigation,
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 6.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(12.dp)
|
||||
) {
|
||||
currentDocument.load(0)?.let {
|
||||
Image(
|
||||
bitmap = it.asImageBitmap(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.height(100.dp)
|
||||
.padding(4.dp)
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(pageCountText(currentDocument.pageCount()))
|
||||
}
|
||||
MainActionButton(navigation.toDocumentScreen, "Open")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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(
|
||||
hasCameraPermission = false,
|
||||
currentDocument = DocumentUiModel(listOf()) { _ -> null },
|
||||
navigation = dummyNavigation(),
|
||||
onStartNewScan = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun HomeScreenPreviewWithCurrentDocument() {
|
||||
MyScanTheme {
|
||||
HomeScreen(
|
||||
hasCameraPermission = true,
|
||||
currentDocument = fakeDocument(
|
||||
listOf("gallica.bnf.fr-bpt6k5530456s-1.jpg"),
|
||||
LocalContext.current),
|
||||
navigation = dummyNavigation(),
|
||||
onStartNewScan = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
31
app/src/main/java/org/mydomain/myscan/view/PreviewUtils.kt
Normal file
31
app/src/main/java/org/mydomain/myscan/view/PreviewUtils.kt
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
import org.mydomain.myscan.Navigation
|
||||
|
||||
fun dummyNavigation(): Navigation {
|
||||
return Navigation({}, {}, {}, {}, {}, {})
|
||||
}
|
||||
|
||||
fun fakeDocument(pageIds: List<String>, context: Context): DocumentUiModel {
|
||||
return DocumentUiModel(pageIds) { id ->
|
||||
context.assets.open(id).use { input ->
|
||||
BitmapFactory.decodeStream(input)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
<string name="camera_permission_denied">L\'autorisation d\'accès à la caméra a été refusée</string>
|
||||
<string name="cancel">Annuler</string>
|
||||
<string name="close">Fermer</string>
|
||||
<string name="close_document">Fermer le document</string>
|
||||
<string name="delete_page">Supprimer la page</string>
|
||||
<string name="document">Document</string>
|
||||
<string name="error">Erreur : %1$s</string>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<string name="camera_permission_denied">Camera permission was denied</string>
|
||||
<string name="cancel">Cancel</string>
|
||||
<string name="close">Close</string>
|
||||
<string name="close_document">Close document</string>
|
||||
<string name="delete_page">Delete page</string>
|
||||
<string name="document">Document</string>
|
||||
<string name="error">Error: %1$s</string>
|
||||
|
||||
Reference in New Issue
Block a user