Export: avoid running preparation when it's useless

This commit is contained in:
Pierre-Yves Nicolas
2026-02-02 16:43:41 +01:00
parent b135e8108a
commit c91237cd2f
4 changed files with 58 additions and 32 deletions

View File

@@ -185,7 +185,7 @@ class MainActivity : ComponentActivity() {
uiState = exportUiState, uiState = exportUiState,
currentDocument = document, currentDocument = document,
pdfActions = ExportActions( pdfActions = ExportActions(
initializeExportScreen = exportViewModel::initializeExportScreen, prepareExportIfNeeded = exportViewModel::prepareExportIfNeeded,
setFilename = exportViewModel::setFilename, setFilename = exportViewModel::setFilename,
share = { share(exportViewModel.applyRenaming(), exportViewModel) }, share = { share(exportViewModel.applyRenaming(), exportViewModel) },
save = { exportViewModel.onSaveClicked() }, save = { exportViewModel.onSaveClicked() },

View File

@@ -115,7 +115,7 @@ fun ExportScreenWrapper(
val showConfirmationDialog = rememberSaveable { mutableStateOf(false) } val showConfirmationDialog = rememberSaveable { mutableStateOf(false) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
pdfActions.initializeExportScreen() pdfActions.prepareExportIfNeeded()
} }
val onFilenameChange = { newName:String -> val onFilenameChange = { newName:String ->

View File

@@ -27,6 +27,8 @@ import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@@ -44,6 +46,7 @@ import org.fairscan.app.RecentDocument
import org.fairscan.app.data.FileManager import org.fairscan.app.data.FileManager
import org.fairscan.app.data.ImageRepository import org.fairscan.app.data.ImageRepository
import org.fairscan.app.domain.ExportQuality import org.fairscan.app.domain.ExportQuality
import org.fairscan.app.domain.PageViewKey
import org.fairscan.app.domain.jpegsForExport import org.fairscan.app.domain.jpegsForExport
import org.fairscan.app.ui.screens.settings.ExportFormat import org.fairscan.app.ui.screens.settings.ExportFormat
import java.io.File import java.io.File
@@ -52,6 +55,7 @@ import java.io.IOException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
import java.util.concurrent.CancellationException
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
@@ -85,8 +89,8 @@ class ExportViewModel(container: AppContainer, val imageRepository: ImageReposit
private val _uiState = MutableStateFlow(ExportUiState()) private val _uiState = MutableStateFlow(ExportUiState())
val uiState: StateFlow<ExportUiState> = _uiState.asStateFlow() val uiState: StateFlow<ExportUiState> = _uiState.asStateFlow()
private var lastPreparationKey: ExportPreparationKey? = null
private var preparationJob: Job? = null private var preparationJob: Job? = null
private var exportFormat = ExportFormat.PDF
fun setFilename(name: String) { fun setFilename(name: String) {
_uiState.update { _uiState.update {
@@ -114,36 +118,52 @@ class ExportViewModel(container: AppContainer, val imageRepository: ImageReposit
} }
} }
fun initializeExportScreen() { private fun currentPageKeys(): ImmutableList<PageViewKey> =
preparationJob?.cancel() imageRepository.pages().map {
_uiState.update { ExportUiState(filename = it.filename) } PageViewKey(it.id, it.manualRotation)
}.toImmutableList()
fun prepareExportIfNeeded() {
ensureValidFilename() ensureValidFilename()
preparationJob = viewModelScope.launch { viewModelScope.launch {
val exportQuality = settingsRepository.exportQuality.first() val exportQuality = settingsRepository.exportQuality.first()
exportFormat = settingsRepository.exportFormat.first() val exportFormat = settingsRepository.exportFormat.first()
_uiState.update { it.copy(format = exportFormat, isGenerating = true) }
try { val key = ExportPreparationKey(currentPageKeys(), exportFormat, exportQuality)
val t1 = System.currentTimeMillis() if (key == lastPreparationKey) {
val result = if (exportFormat == ExportFormat.JPEG) { return@launch
generateJpegs(exportQuality) }
} else {
generatePdf(exportQuality) lastPreparationKey = key
} preparationJob?.cancel()
preparationJob = launch {
_uiState.update { _uiState.update {
it.copy(isGenerating = false, result = result) ExportUiState(filename = it.filename, format = exportFormat, isGenerating = true)
} }
val t2 = System.currentTimeMillis() try {
val pageCount = result.pageCount val t1 = System.currentTimeMillis()
Log.i("Export", "Generation: $pageCount pages, $exportQuality, ${t2-t1} ms") val result = if (exportFormat == ExportFormat.JPEG) {
} catch (e: Exception) { generateJpegs(exportQuality)
val message = "Failed to prepare $exportFormat export" } else {
logger.e("FairScan", message, e) generatePdf(exportQuality)
_uiState.update { }
it.copy( _uiState.update { it.copy(result = result) }
isGenerating = false, val t2 = System.currentTimeMillis()
error = ExportError.OnPrepare(message, e), val pageCount = result.pageCount
) Log.i("Export", "Generation: $pageCount pages, $exportQuality, ${t2 - t1} ms")
} catch (e: CancellationException) {
// Preparation cancelled: do nothing
throw e
} catch (e: Exception) {
val message = "Failed to prepare $exportFormat export"
logger.e("FairScan", message, e)
_uiState.update {
it.copy(error = ExportError.OnPrepare(message, e))
}
} finally {
_uiState.update { it.copy(isGenerating = false) }
} }
} }
} }
@@ -216,12 +236,13 @@ class ExportViewModel(container: AppContainer, val imageRepository: ImageReposit
fun onRequestSave(context: Context) { fun onRequestSave(context: Context) {
viewModelScope.launch { viewModelScope.launch {
_uiState.update {it.copy(isSaving = true, error = null, savedBundle = null) } _uiState.update {it.copy(isSaving = true, error = null, savedBundle = null) }
val exportFormat = uiState.value.format
val saveDir = saveDir(context) val saveDir = saveDir(context)
try { try {
// Must not run on the main thread: some SAF providers (e.g. Nextcloud) // Must not run on the main thread: some SAF providers (e.g. Nextcloud)
// may perform network I/O // may perform network I/O
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
save(context, saveDir) save(context, saveDir, exportFormat)
} }
} catch (e: MissingExportDirPermissionException) { } catch (e: MissingExportDirPermissionException) {
logger.e("FairScan", "Missing export dir permission", e) logger.e("FairScan", "Missing export dir permission", e)
@@ -246,7 +267,7 @@ class ExportViewModel(container: AppContainer, val imageRepository: ImageReposit
return SaveDir(uri, name) return SaveDir(uri, name)
} }
private suspend fun save(context: Context, saveDir: SaveDir?) { private suspend fun save(context: Context, saveDir: SaveDir?, exportFormat: ExportFormat) {
val result = applyRenaming() ?: return val result = applyRenaming() ?: return
val savedItems = mutableListOf<SavedItem>() val savedItems = mutableListOf<SavedItem>()
val filesForMediaScan = mutableListOf<File>() val filesForMediaScan = mutableListOf<File>()
@@ -389,6 +410,12 @@ class ExportViewModel(container: AppContainer, val imageRepository: ImageReposit
} }
} }
data class ExportPreparationKey(
val pages: ImmutableList<PageViewKey>,
val format: ExportFormat,
val quality: ExportQuality
)
sealed class ExportResult { sealed class ExportResult {
abstract val files: List<File> abstract val files: List<File>
abstract val sizeInBytes: Long abstract val sizeInBytes: Long
@@ -415,7 +442,7 @@ sealed class ExportResult {
} }
data class ExportActions( data class ExportActions(
val initializeExportScreen: () -> Unit, val prepareExportIfNeeded: () -> Unit,
val setFilename: (String) -> Unit, val setFilename: (String) -> Unit,
val share: () -> Unit, val share: () -> Unit,
val save: () -> Unit, val save: () -> Unit,

View File

@@ -88,7 +88,6 @@ private fun SettingsContent(
onExportQualityChanged: (ExportQuality) -> Unit, onExportQualityChanged: (ExportQuality) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
println(uiState)
val (folderLabel, folderLabelColor) = when { val (folderLabel, folderLabelColor) = when {
uiState.exportDirUri == null -> uiState.exportDirUri == null ->
stringResource(R.string.download_dirname) to stringResource(R.string.download_dirname) to