Export screen: persist (in-memory) filename during scan

This commit is contained in:
Pierre-Yves Nicolas
2026-02-02 13:58:34 +01:00
parent 974666f071
commit b135e8108a
4 changed files with 45 additions and 43 deletions

View File

@@ -192,6 +192,7 @@ class MainActivity : ComponentActivity() {
open = { item -> openUri(item.uri, item.format.mimeType) }, open = { item -> openUri(item.uri, item.format.mimeType) },
), ),
onCloseScan = { onCloseScan = {
exportViewModel.resetFilename()
viewModel.startNewDocument() viewModel.startNewDocument()
viewModel.navigateTo(Screen.Main.Home) viewModel.navigateTo(Screen.Main.Home)
} }

View File

@@ -64,7 +64,6 @@ 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.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -102,9 +101,6 @@ import org.fairscan.app.ui.state.DocumentUiModel
import org.fairscan.app.ui.theme.FairScanTheme import org.fairscan.app.ui.theme.FairScanTheme
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@Composable @Composable
fun ExportScreenWrapper( fun ExportScreenWrapper(
@@ -117,38 +113,27 @@ fun ExportScreenWrapper(
BackHandler { navigation.back() } BackHandler { navigation.back() }
val showConfirmationDialog = rememberSaveable { mutableStateOf(false) } val showConfirmationDialog = rememberSaveable { mutableStateOf(false) }
val filename = remember { mutableStateOf(defaultFilename()) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
pdfActions.setFilename(filename.value)
pdfActions.initializeExportScreen() pdfActions.initializeExportScreen()
} }
val onFilenameChange = { newName:String -> val onFilenameChange = { newName:String ->
filename.value = newName
pdfActions.setFilename(newName) pdfActions.setFilename(newName)
} }
val ensureCorrectFileName = {
val value = filename.value.trim().ifEmpty { defaultFilename() }
if (value != filename.value) {
onFilenameChange(value)
}
}
ExportScreen( ExportScreen(
filename = filename,
onFilenameChange = onFilenameChange, onFilenameChange = onFilenameChange,
uiState = uiState, uiState = uiState,
currentDocument = currentDocument, currentDocument = currentDocument,
navigation = navigation, navigation = navigation,
onShare = { onShare = {
if (!uiState.isSaving) { if (!uiState.isSaving) {
ensureCorrectFileName()
pdfActions.share() pdfActions.share()
} }
}, },
onSave = { onSave = {
if (!uiState.isSaving) { if (!uiState.isSaving) {
ensureCorrectFileName()
pdfActions.save() pdfActions.save()
} }
}, },
@@ -171,7 +156,6 @@ fun ExportScreenWrapper(
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ExportScreen( fun ExportScreen(
filename: MutableState<String>,
onFilenameChange: (String) -> Unit, onFilenameChange: (String) -> Unit,
uiState: ExportUiState, uiState: ExportUiState,
currentDocument: DocumentUiModel, currentDocument: DocumentUiModel,
@@ -203,7 +187,7 @@ fun ExportScreen(
) { ) {
PdfInfosAndResultBar(uiState, currentDocument, onOpen, onThumbnailClick) PdfInfosAndResultBar(uiState, currentDocument, onOpen, onThumbnailClick)
Spacer(Modifier.weight(1f)) // push buttons down Spacer(Modifier.weight(1f)) // push buttons down
MainActions(filename, onFilenameChange, uiState, onShare, onSave, onCloseScan) MainActions(onFilenameChange, uiState, onShare, onSave, onCloseScan)
} }
} else { } else {
Row( Row(
@@ -218,7 +202,7 @@ fun ExportScreen(
PdfInfosAndResultBar(uiState, currentDocument, onOpen, onThumbnailClick) PdfInfosAndResultBar(uiState, currentDocument, onOpen, onThumbnailClick)
} }
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
MainActions(filename, onFilenameChange, uiState, onShare, onSave, onCloseScan) MainActions(onFilenameChange, uiState, onShare, onSave, onCloseScan)
} }
} }
@@ -348,12 +332,12 @@ private fun SaveStatusBar(
@Composable @Composable
private fun FilenameTextField( private fun FilenameTextField(
filename: MutableState<String>, filename: String,
onFilenameChange: (String) -> Unit, onFilenameChange: (String) -> Unit,
) { ) {
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
OutlinedTextField( OutlinedTextField(
value = filename.value, value = filename,
onValueChange = onFilenameChange, onValueChange = onFilenameChange,
label = { Text(stringResource(R.string.filename)) }, label = { Text(stringResource(R.string.filename)) },
singleLine = true, singleLine = true,
@@ -361,9 +345,9 @@ private fun FilenameTextField(
.fillMaxWidth() .fillMaxWidth()
.focusRequester(focusRequester), .focusRequester(focusRequester),
trailingIcon = { trailingIcon = {
if (filename.value.isNotEmpty()) { if (filename.isNotEmpty()) {
IconButton(onClick = { IconButton(onClick = {
filename.value = "" onFilenameChange("")
focusRequester.requestFocus() focusRequester.requestFocus()
}) { }) {
Icon(Icons.Default.Clear, stringResource(R.string.clear_text)) Icon(Icons.Default.Clear, stringResource(R.string.clear_text))
@@ -375,7 +359,6 @@ private fun FilenameTextField(
@Composable @Composable
private fun MainActions( private fun MainActions(
filename: MutableState<String>,
onFilenameChange: (String) -> Unit, onFilenameChange: (String) -> Unit,
uiState: ExportUiState, uiState: ExportUiState,
onShare: () -> Unit, onShare: () -> Unit,
@@ -386,7 +369,7 @@ private fun MainActions(
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
ActionSurface { ActionSurface {
FilenameTextField(filename, onFilenameChange) FilenameTextField(uiState.filename, onFilenameChange)
Row( Row(
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
@@ -658,11 +641,6 @@ fun providerLabel(authority: String): String =
authority authority
} }
fun defaultFilename(): String {
val timestamp = SimpleDateFormat("yyyy-MM-dd HH.mm.ss", Locale.getDefault()).format(Date())
return "Scan $timestamp"
}
fun formatFileSize(sizeInBytes: Long?, context: Context): String { fun formatFileSize(sizeInBytes: Long?, context: Context): String {
return if (sizeInBytes == null) context.getString(R.string.unknown_size) return if (sizeInBytes == null) context.getString(R.string.unknown_size)
else Formatter.formatShortFileSize(context, sizeInBytes) else Formatter.formatShortFileSize(context, sizeInBytes)
@@ -672,7 +650,7 @@ fun formatFileSize(sizeInBytes: Long?, context: Context): String {
@Composable @Composable
fun PreviewExportScreenDuringGeneration() { fun PreviewExportScreenDuringGeneration() {
ExportPreviewToCustomize( ExportPreviewToCustomize(
uiState = ExportUiState(isGenerating = true) uiState = ExportUiState(isGenerating = true, filename = "Scan 2025-12-15 07:00:51")
) )
} }
@@ -695,7 +673,7 @@ fun PreviewExportScreenAfterSave() {
uiState = ExportUiState( uiState = ExportUiState(
result = ExportResult.Pdf(file, 442897L, 3), result = ExportResult.Pdf(file, 442897L, 3),
savedBundle = SavedBundle( savedBundle = SavedBundle(
listOf(SavedItem(file.toUri(), defaultFilename() + ".pdf", PDF)) listOf(SavedItem(file.toUri(), "12345.pdf", PDF))
), ),
), ),
) )
@@ -728,7 +706,6 @@ fun PreviewExportScreenAfterSaveHorizontal() {
fun ExportPreviewToCustomize(uiState: ExportUiState) { fun ExportPreviewToCustomize(uiState: ExportUiState) {
FairScanTheme { FairScanTheme {
ExportScreen( ExportScreen(
filename = remember { mutableStateOf("Scan 2025-07-02 17.40.42") },
onFilenameChange = {_->}, onFilenameChange = {_->},
uiState = uiState, uiState = uiState,
currentDocument = fakeDocument( currentDocument = fakeDocument(

View File

@@ -19,6 +19,7 @@ import org.fairscan.app.ui.screens.settings.ExportFormat
data class ExportUiState( data class ExportUiState(
val format: ExportFormat = ExportFormat.PDF, val format: ExportFormat = ExportFormat.PDF,
val filename: String = "",
val isGenerating: Boolean = false, val isGenerating: Boolean = false,
val isSaving: Boolean = false, val isSaving: Boolean = false,
val result: ExportResult? = null, val result: ExportResult? = null,

View File

@@ -49,6 +49,9 @@ import org.fairscan.app.ui.screens.settings.ExportFormat
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.IOException import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
@@ -83,15 +86,38 @@ class ExportViewModel(container: AppContainer, val imageRepository: ImageReposit
val uiState: StateFlow<ExportUiState> = _uiState.asStateFlow() val uiState: StateFlow<ExportUiState> = _uiState.asStateFlow()
private var preparationJob: Job? = null private var preparationJob: Job? = null
private var desiredFilename: String = ""
private var exportFormat = ExportFormat.PDF private var exportFormat = ExportFormat.PDF
fun setFilename(name: String) { fun setFilename(name: String) {
desiredFilename = name _uiState.update {
it.copy(filename = name)
}
}
fun resetFilename() {
_uiState.update {
it.copy(filename = "")
}
}
private fun defaultFilename(): String {
val timestamp = SimpleDateFormat("yyyy-MM-dd HH.mm.ss", Locale.getDefault()).format(Date())
return "Scan $timestamp"
}
private fun ensureValidFilename() {
_uiState.update {
val normalized = it.filename.trim().ifEmpty { defaultFilename() }
if (normalized != it.filename) {
it.copy(filename = normalized)
} else it
}
} }
fun initializeExportScreen() { fun initializeExportScreen() {
cancelPreparation() preparationJob?.cancel()
_uiState.update { ExportUiState(filename = it.filename) }
ensureValidFilename()
preparationJob = viewModelScope.launch { preparationJob = viewModelScope.launch {
val exportQuality = settingsRepository.exportQuality.first() val exportQuality = settingsRepository.exportQuality.first()
@@ -138,20 +164,17 @@ class ExportViewModel(container: AppContainer, val imageRepository: ImageReposit
ExportResult.Jpeg(files, sizeInBytes) ExportResult.Jpeg(files, sizeInBytes)
} }
fun cancelPreparation() {
preparationJob?.cancel()
_uiState.value = ExportUiState()
}
fun setAsShared() { fun setAsShared() {
_uiState.update { it.copy(hasShared = true) } _uiState.update { it.copy(hasShared = true) }
} }
fun applyRenaming(): ExportResult? { fun applyRenaming(): ExportResult? {
val result = _uiState.value.result ?: return null val result = _uiState.value.result ?: return null
ensureValidFilename()
val filename = _uiState.value.filename
when (result) { when (result) {
is ExportResult.Pdf -> { is ExportResult.Pdf -> {
val fileName = FileManager.addPdfExtensionIfMissing(desiredFilename) val fileName = FileManager.addPdfExtensionIfMissing(filename)
val newFile = File(result.file.parentFile, fileName) val newFile = File(result.file.parentFile, fileName)
val tempFile = result.file val tempFile = result.file
if (tempFile.absolutePath != newFile.absolutePath) { if (tempFile.absolutePath != newFile.absolutePath) {
@@ -166,7 +189,7 @@ class ExportViewModel(container: AppContainer, val imageRepository: ImageReposit
} }
} }
is ExportResult.Jpeg -> { is ExportResult.Jpeg -> {
val base = desiredFilename.removeSuffix(".jpg") val base = filename.removeSuffix(".jpg")
val files = result.files val files = result.files
val renamedFiles = files.mapIndexed { index, file -> val renamedFiles = files.mapIndexed { index, file ->
val indexSuffix = if (files.size == 1) "" else "_${index + 1}" val indexSuffix = if (files.size == 1) "" else "_${index + 1}"