diff --git a/app/src/main/java/org/mydomain/myscan/MainActivity.kt b/app/src/main/java/org/mydomain/myscan/MainActivity.kt index 09caab6..f18d1c6 100644 --- a/app/src/main/java/org/mydomain/myscan/MainActivity.kt +++ b/app/src/main/java/org/mydomain/myscan/MainActivity.kt @@ -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) } ) } diff --git a/app/src/main/java/org/mydomain/myscan/MainViewModel.kt b/app/src/main/java/org/mydomain/myscan/MainViewModel.kt index 86d36ee..744be4e 100644 --- a/app/src/main/java/org/mydomain/myscan/MainViewModel.kt +++ b/app/src/main/java/org/mydomain/myscan/MainViewModel.kt @@ -68,9 +68,9 @@ class MainViewModel( val liveAnalysisState: StateFlow = _liveAnalysisState.asStateFlow() private var lastSuccessfulLiveAnalysisState: LiveAnalysisState? = null - private val _screenStack = MutableStateFlow>(listOf(Screen.Camera)) + private val _screenStack = MutableStateFlow>(listOf(Screen.Home)) val currentScreen: StateFlow = _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 = diff --git a/app/src/main/java/org/mydomain/myscan/Navigation.kt b/app/src/main/java/org/mydomain/myscan/Navigation.kt index afdb36f..693fd1d 100644 --- a/app/src/main/java/org/mydomain/myscan/Navigation.kt +++ b/app/src/main/java/org/mydomain/myscan/Navigation.kt @@ -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, diff --git a/app/src/main/java/org/mydomain/myscan/PermissionHelpers.kt b/app/src/main/java/org/mydomain/myscan/PermissionHelpers.kt new file mode 100644 index 0000000..6dbe884 --- /dev/null +++ b/app/src/main/java/org/mydomain/myscan/PermissionHelpers.kt @@ -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 . + */ +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 { + 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() + } + } +} diff --git a/app/src/main/java/org/mydomain/myscan/view/Buttons.kt b/app/src/main/java/org/mydomain/myscan/view/Buttons.kt index fdf55e4..c077489 100644 --- a/app/src/main/java/org/mydomain/myscan/view/Buttons.kt +++ b/app/src/main/java/org/mydomain/myscan/view/Buttons.kt @@ -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(icon, contentDescription = iconDescription) + 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)) } diff --git a/app/src/main/java/org/mydomain/myscan/view/CameraPreview.kt b/app/src/main/java/org/mydomain/myscan/view/CameraPreview.kt index 95feb52..0302366 100644 --- a/app/src/main/java/org/mydomain/myscan/view/CameraPreview.kt +++ b/app/src/main/java/org/mydomain/myscan/view/CameraPreview.kt @@ -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) } } diff --git a/app/src/main/java/org/mydomain/myscan/view/CameraScreen.kt b/app/src/main/java/org/mydomain/myscan/view/CameraScreen.kt index 6db77c7..aec1a7e 100644 --- a/app/src/main/java/org/mydomain/myscan/view/CameraScreen.kt +++ b/app/src/main/java/org/mydomain/myscan/view/CameraScreen.kt @@ -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, - 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() ) } } diff --git a/app/src/main/java/org/mydomain/myscan/view/DocumentScreen.kt b/app/src/main/java/org/mydomain/myscan/view/DocumentScreen.kt index 1f4f575..20ae803 100644 --- a/app/src/main/java/org/mydomain/myscan/view/DocumentScreen.kt +++ b/app/src/main/java/org/mydomain/myscan/view/DocumentScreen.kt @@ -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) { +fun NewDocumentDialog(onConfirm: () -> Unit, showDialog: MutableState, 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) @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()), diff --git a/app/src/main/java/org/mydomain/myscan/view/HomeScreen.kt b/app/src/main/java/org/mydomain/myscan/view/HomeScreen.kt new file mode 100644 index 0000000..61a6d1c --- /dev/null +++ b/app/src/main/java/org/mydomain/myscan/view/HomeScreen.kt @@ -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 . + */ +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 = {} + ) + } +} diff --git a/app/src/main/java/org/mydomain/myscan/view/PreviewUtils.kt b/app/src/main/java/org/mydomain/myscan/view/PreviewUtils.kt new file mode 100644 index 0000000..3f34b26 --- /dev/null +++ b/app/src/main/java/org/mydomain/myscan/view/PreviewUtils.kt @@ -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 . + */ +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, context: Context): DocumentUiModel { + return DocumentUiModel(pageIds) { id -> + context.assets.open(id).use { input -> + BitmapFactory.decodeStream(input) + } + } +} diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 1f826d1..374ac8c 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -7,6 +7,7 @@ L\'autorisation d\'accès à la caméra a été refusée Annuler Fermer + Fermer le document Supprimer la page Document Erreur : %1$s diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7acd9bf..1d9ac07 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7,6 +7,7 @@ Camera permission was denied Cancel Close + Close document Delete page Document Error: %1$s