Export screen: persist (in-memory) filename during scan
This commit is contained in:
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
|||||||
Reference in New Issue
Block a user