Split MainViewModel: extract ExportViewModel
This commit is contained in:
committed by
pynicolas
parent
15e3d9d917
commit
7c53dcface
@@ -15,6 +15,7 @@
|
|||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
android:name=".FairScanApp"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.FairScan"
|
android:theme="@style/Theme.FairScan"
|
||||||
|
|||||||
46
app/src/main/java/org/fairscan/app/FairScanApp.kt
Normal file
46
app/src/main/java/org/fairscan/app/FairScanApp.kt
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* 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.fairscan.app
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Environment
|
||||||
|
import org.fairscan.app.data.ImageRepository
|
||||||
|
import org.fairscan.app.data.PdfFileManager
|
||||||
|
import org.fairscan.app.platform.AndroidPdfWriter
|
||||||
|
import org.fairscan.app.platform.OpenCvTransformations
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class FairScanApp : Application() {
|
||||||
|
lateinit var appContainer: AppContainer
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
appContainer = AppContainer(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const val THUMBNAIL_SIZE_DP = 120
|
||||||
|
|
||||||
|
class AppContainer(context: Context) {
|
||||||
|
private val density = context.resources.displayMetrics.density
|
||||||
|
private val thumbnailSizePx = (THUMBNAIL_SIZE_DP * density).toInt()
|
||||||
|
val imageRepository = ImageRepository(context.filesDir, OpenCvTransformations(), thumbnailSizePx)
|
||||||
|
val pdfFileManager = PdfFileManager(
|
||||||
|
File(context.cacheDir, "pdfs"),
|
||||||
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
AndroidPdfWriter()
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -51,15 +51,17 @@ import org.fairscan.app.data.GeneratedPdf
|
|||||||
import org.fairscan.app.ui.Navigation
|
import org.fairscan.app.ui.Navigation
|
||||||
import org.fairscan.app.ui.Screen
|
import org.fairscan.app.ui.Screen
|
||||||
import org.fairscan.app.ui.components.rememberCameraPermissionState
|
import org.fairscan.app.ui.components.rememberCameraPermissionState
|
||||||
import org.fairscan.app.ui.theme.FairScanTheme
|
|
||||||
import org.fairscan.app.ui.screens.AboutScreen
|
import org.fairscan.app.ui.screens.AboutScreen
|
||||||
import org.fairscan.app.ui.screens.camera.CameraScreen
|
|
||||||
import org.fairscan.app.ui.screens.DocumentScreen
|
import org.fairscan.app.ui.screens.DocumentScreen
|
||||||
import org.fairscan.app.ui.screens.ExportScreenWrapper
|
|
||||||
import org.fairscan.app.ui.screens.HomeScreen
|
import org.fairscan.app.ui.screens.HomeScreen
|
||||||
import org.fairscan.app.ui.screens.LibrariesScreen
|
import org.fairscan.app.ui.screens.LibrariesScreen
|
||||||
import org.fairscan.app.ui.screens.camera.CameraEvent
|
import org.fairscan.app.ui.screens.camera.CameraEvent
|
||||||
|
import org.fairscan.app.ui.screens.camera.CameraScreen
|
||||||
import org.fairscan.app.ui.screens.camera.CameraViewModel
|
import org.fairscan.app.ui.screens.camera.CameraViewModel
|
||||||
|
import org.fairscan.app.ui.screens.export.ExportScreenWrapper
|
||||||
|
import org.fairscan.app.ui.screens.export.ExportViewModel
|
||||||
|
import org.fairscan.app.ui.screens.export.PdfGenerationActions
|
||||||
|
import org.fairscan.app.ui.theme.FairScanTheme
|
||||||
import org.opencv.android.OpenCVLoader
|
import org.opencv.android.OpenCVLoader
|
||||||
|
|
||||||
private const val PDF_MIME_TYPE = "application/pdf"
|
private const val PDF_MIME_TYPE = "application/pdf"
|
||||||
@@ -70,10 +72,11 @@ class MainActivity : ComponentActivity() {
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
initLibraries()
|
initLibraries()
|
||||||
val viewModel: MainViewModel by viewModels { MainViewModel.getFactory(this) }
|
val viewModel: MainViewModel by viewModels { MainViewModel.getFactory(this) }
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
viewModel.cleanUpOldPdfs(1000 * 3600)
|
|
||||||
}
|
|
||||||
val cameraViewModel: CameraViewModel by viewModels { CameraViewModel.getFactory(this) }
|
val cameraViewModel: CameraViewModel by viewModels { CameraViewModel.getFactory(this) }
|
||||||
|
val exportViewModel: ExportViewModel by viewModels { ExportViewModel.getFactory(this) }
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
exportViewModel.cleanUpOldPdfs(1000 * 3600)
|
||||||
|
}
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
@@ -88,7 +91,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
val liveAnalysisState by cameraViewModel.liveAnalysisState.collectAsStateWithLifecycle()
|
val liveAnalysisState by cameraViewModel.liveAnalysisState.collectAsStateWithLifecycle()
|
||||||
val document by viewModel.documentUiModel.collectAsStateWithLifecycle()
|
val document by viewModel.documentUiModel.collectAsStateWithLifecycle()
|
||||||
val cameraPermission = rememberCameraPermissionState()
|
val cameraPermission = rememberCameraPermissionState()
|
||||||
val savePdf = { savePdf(viewModel.getFinalPdf(), viewModel) }
|
val savePdf = { savePdf(exportViewModel.getFinalPdf(), viewModel, exportViewModel) }
|
||||||
val storagePermissionLauncher = rememberLauncherForActivityResult(
|
val storagePermissionLauncher = rememberLauncherForActivityResult(
|
||||||
ActivityResultContracts.RequestPermission()
|
ActivityResultContracts.RequestPermission()
|
||||||
) { isGranted ->
|
) { isGranted ->
|
||||||
@@ -146,12 +149,12 @@ class MainActivity : ComponentActivity() {
|
|||||||
ExportScreenWrapper(
|
ExportScreenWrapper(
|
||||||
navigation = navigation,
|
navigation = navigation,
|
||||||
pdfActions = PdfGenerationActions(
|
pdfActions = PdfGenerationActions(
|
||||||
startGeneration = viewModel::startPdfGeneration,
|
startGeneration = exportViewModel::startPdfGeneration,
|
||||||
setFilename = viewModel::setFilename,
|
setFilename = exportViewModel::setFilename,
|
||||||
uiStateFlow = viewModel.pdfUiState,
|
uiStateFlow = exportViewModel.pdfUiState,
|
||||||
sharePdf = { sharePdf(viewModel.getFinalPdf(), viewModel) },
|
sharePdf = { sharePdf(exportViewModel.getFinalPdf(), exportViewModel) },
|
||||||
savePdf = { checkPermissionThen(storagePermissionLauncher, savePdf) },
|
savePdf = { checkPermissionThen(storagePermissionLauncher, savePdf) },
|
||||||
openPdf = { openPdf(viewModel.pdfUiState.value.savedFileUri) }
|
openPdf = { openPdf(exportViewModel.pdfUiState.value.savedFileUri) }
|
||||||
),
|
),
|
||||||
onCloseScan = {
|
onCloseScan = {
|
||||||
viewModel.startNewDocument()
|
viewModel.startNewDocument()
|
||||||
@@ -170,7 +173,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sharePdf(generatedPdf: GeneratedPdf?, viewModel: MainViewModel) {
|
private fun sharePdf(generatedPdf: GeneratedPdf?, viewModel: ExportViewModel) {
|
||||||
if (generatedPdf == null)
|
if (generatedPdf == null)
|
||||||
return
|
return
|
||||||
viewModel.setPdfAsShared()
|
viewModel.setPdfAsShared()
|
||||||
@@ -205,14 +208,18 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun savePdf(generatedPdf: GeneratedPdf?, viewModel: MainViewModel) {
|
private fun savePdf(
|
||||||
|
generatedPdf: GeneratedPdf?,
|
||||||
|
viewModel: MainViewModel,
|
||||||
|
exportViewModel: ExportViewModel
|
||||||
|
) {
|
||||||
if (generatedPdf == null)
|
if (generatedPdf == null)
|
||||||
return
|
return
|
||||||
val appScope = CoroutineScope(Dispatchers.IO)
|
val appScope = CoroutineScope(Dispatchers.IO)
|
||||||
val context = this
|
val context = this
|
||||||
appScope.launch {
|
appScope.launch {
|
||||||
try {
|
try {
|
||||||
val targetFile = viewModel.saveFile(generatedPdf.file)
|
val targetFile = exportViewModel.saveFile(generatedPdf.file)
|
||||||
viewModel.addRecentDocument(targetFile.absolutePath, generatedPdf.pageCount)
|
viewModel.addRecentDocument(targetFile.absolutePath, generatedPdf.pageCount)
|
||||||
|
|
||||||
suspendCancellableCoroutine { continuation ->
|
suspendCancellableCoroutine { continuation ->
|
||||||
|
|||||||
@@ -17,44 +17,29 @@ package org.fairscan.app
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.os.Environment
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.datastore.core.DataStore
|
import androidx.datastore.core.DataStore
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.CreationExtras
|
import androidx.lifecycle.viewmodel.CreationExtras
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.fairscan.app.data.GeneratedPdf
|
|
||||||
import org.fairscan.app.data.ImageRepository
|
import org.fairscan.app.data.ImageRepository
|
||||||
import org.fairscan.app.data.PdfFileManager
|
|
||||||
import org.fairscan.app.data.recentDocumentsDataStore
|
import org.fairscan.app.data.recentDocumentsDataStore
|
||||||
import org.fairscan.app.platform.AndroidPdfWriter
|
|
||||||
import org.fairscan.app.platform.OpenCvTransformations
|
|
||||||
import org.fairscan.app.ui.NavigationState
|
import org.fairscan.app.ui.NavigationState
|
||||||
import org.fairscan.app.ui.Screen
|
import org.fairscan.app.ui.Screen
|
||||||
import org.fairscan.app.ui.state.DocumentUiModel
|
import org.fairscan.app.ui.state.DocumentUiModel
|
||||||
import org.fairscan.app.ui.state.PdfGenerationUiState
|
|
||||||
import org.fairscan.app.ui.state.RecentDocumentUiState
|
import org.fairscan.app.ui.state.RecentDocumentUiState
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
const val THUMBNAIL_SIZE_DP = 120
|
|
||||||
|
|
||||||
class MainViewModel(
|
class MainViewModel(
|
||||||
private val imageRepository: ImageRepository,
|
private val imageRepository: ImageRepository,
|
||||||
private val pdfFileManager: PdfFileManager,
|
|
||||||
private val recentDocumentsDataStore: DataStore<RecentDocuments>,
|
private val recentDocumentsDataStore: DataStore<RecentDocuments>,
|
||||||
): ViewModel() {
|
): ViewModel() {
|
||||||
|
|
||||||
@@ -62,15 +47,9 @@ class MainViewModel(
|
|||||||
fun getFactory(context: Context) = object : ViewModelProvider.Factory {
|
fun getFactory(context: Context) = object : ViewModelProvider.Factory {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
|
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
|
||||||
val density = context.resources.displayMetrics.density
|
val app = context.applicationContext as FairScanApp
|
||||||
val thumbnailSizePx = (THUMBNAIL_SIZE_DP * density).toInt()
|
|
||||||
return MainViewModel(
|
return MainViewModel(
|
||||||
ImageRepository(context.filesDir, OpenCvTransformations(), thumbnailSizePx),
|
app.appContainer.imageRepository,
|
||||||
PdfFileManager(
|
|
||||||
File(context.cacheDir, "pdfs"),
|
|
||||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
|
||||||
AndroidPdfWriter()
|
|
||||||
),
|
|
||||||
context.recentDocumentsDataStore,
|
context.recentDocumentsDataStore,
|
||||||
) as T
|
) as T
|
||||||
}
|
}
|
||||||
@@ -137,85 +116,6 @@ class MainViewModel(
|
|||||||
return bytes?.let { BitmapFactory.decodeByteArray(it, 0, it.size) }
|
return bytes?.let { BitmapFactory.decodeByteArray(it, 0, it.size) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun generatePdf(): GeneratedPdf = withContext(Dispatchers.IO) {
|
|
||||||
val imageIds = imageRepository.imageIds()
|
|
||||||
val jpegs = imageIds.asSequence()
|
|
||||||
.map { id -> imageRepository.getContent(id) }
|
|
||||||
.filterNotNull()
|
|
||||||
return@withContext pdfFileManager.generatePdf(jpegs)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val _pdfUiState = MutableStateFlow(PdfGenerationUiState())
|
|
||||||
val pdfUiState: StateFlow<PdfGenerationUiState> = _pdfUiState.asStateFlow()
|
|
||||||
|
|
||||||
private var generationJob: Job? = null
|
|
||||||
private var desiredFilename: String = ""
|
|
||||||
|
|
||||||
fun setFilename(name: String) {
|
|
||||||
desiredFilename = name
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startPdfGeneration() {
|
|
||||||
cancelPdfGeneration()
|
|
||||||
generationJob = viewModelScope.launch {
|
|
||||||
try {
|
|
||||||
val result = generatePdf()
|
|
||||||
_pdfUiState.update {
|
|
||||||
it.copy(
|
|
||||||
isGenerating = false,
|
|
||||||
generatedPdf = result
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("FairScan", "PDF generation failed", e)
|
|
||||||
_pdfUiState.update {
|
|
||||||
it.copy(
|
|
||||||
isGenerating = false,
|
|
||||||
errorMessage = "PDF generation failed"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cancelPdfGeneration() {
|
|
||||||
generationJob?.cancel()
|
|
||||||
_pdfUiState.value = PdfGenerationUiState()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setPdfAsShared() {
|
|
||||||
_pdfUiState.update { it.copy(hasSharedPdf = true) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getFinalPdf(): GeneratedPdf? {
|
|
||||||
val tempPdf = _pdfUiState.value.generatedPdf ?: return null
|
|
||||||
val tempFile = tempPdf.file
|
|
||||||
val fileName = PdfFileManager.addExtensionIfMissing(desiredFilename)
|
|
||||||
val newFile = File(tempFile.parentFile, fileName)
|
|
||||||
if (tempFile.absolutePath != newFile.absolutePath) {
|
|
||||||
if (newFile.exists()) newFile.delete()
|
|
||||||
val success = tempFile.renameTo(newFile)
|
|
||||||
if (!success) return null
|
|
||||||
_pdfUiState.update {
|
|
||||||
it.copy(generatedPdf = GeneratedPdf(
|
|
||||||
newFile, tempPdf.sizeInBytes, tempPdf.pageCount)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return _pdfUiState.value.generatedPdf
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveFile(pdfFile: File): File {
|
|
||||||
val copiedFile = pdfFileManager.copyToExternalDir(pdfFile)
|
|
||||||
_pdfUiState.update { it.copy(savedFileUri = copiedFile.toUri()) }
|
|
||||||
return copiedFile
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cleanUpOldPdfs(thresholdInMillis: Int) {
|
|
||||||
pdfFileManager.cleanUpOldFiles(thresholdInMillis)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
val recentDocuments: StateFlow<List<RecentDocumentUiState>> =
|
val recentDocuments: StateFlow<List<RecentDocumentUiState>> =
|
||||||
recentDocumentsDataStore.data.map {
|
recentDocumentsDataStore.data.map {
|
||||||
it.documentsList.map {
|
it.documentsList.map {
|
||||||
@@ -256,13 +156,3 @@ class MainViewModel(
|
|||||||
_pageIds.value = imageRepository.imageIds()
|
_pageIds.value = imageRepository.imageIds()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Move somewhere else: ViewModel should not depend on that
|
|
||||||
data class PdfGenerationActions(
|
|
||||||
val startGeneration: () -> Unit,
|
|
||||||
val setFilename: (String) -> Unit,
|
|
||||||
val uiStateFlow: StateFlow<PdfGenerationUiState>,// TODO is it ok to have that here?
|
|
||||||
val sharePdf: () -> Unit,
|
|
||||||
val savePdf: () -> Unit,
|
|
||||||
val openPdf: () -> Unit,
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License along with
|
* You should have received a copy of the GNU General Public License along with
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package org.fairscan.app.ui.screens
|
package org.fairscan.app.ui.screens.export
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.text.format.Formatter
|
import android.text.format.Formatter
|
||||||
@@ -66,11 +66,9 @@ import androidx.compose.ui.text.font.FontStyle
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
import org.fairscan.app.R
|
||||||
import org.fairscan.app.data.GeneratedPdf
|
import org.fairscan.app.data.GeneratedPdf
|
||||||
import org.fairscan.app.ui.Navigation
|
import org.fairscan.app.ui.Navigation
|
||||||
import org.fairscan.app.PdfGenerationActions
|
|
||||||
import org.fairscan.app.R
|
|
||||||
import org.fairscan.app.ui.state.PdfGenerationUiState
|
|
||||||
import org.fairscan.app.ui.components.AboutScreenNavButton
|
import org.fairscan.app.ui.components.AboutScreenNavButton
|
||||||
import org.fairscan.app.ui.components.BackButton
|
import org.fairscan.app.ui.components.BackButton
|
||||||
import org.fairscan.app.ui.components.MainActionButton
|
import org.fairscan.app.ui.components.MainActionButton
|
||||||
@@ -78,6 +76,7 @@ import org.fairscan.app.ui.components.NewDocumentDialog
|
|||||||
import org.fairscan.app.ui.components.isLandscape
|
import org.fairscan.app.ui.components.isLandscape
|
||||||
import org.fairscan.app.ui.components.pageCountText
|
import org.fairscan.app.ui.components.pageCountText
|
||||||
import org.fairscan.app.ui.dummyNavigation
|
import org.fairscan.app.ui.dummyNavigation
|
||||||
|
import org.fairscan.app.ui.state.PdfGenerationUiState
|
||||||
import org.fairscan.app.ui.theme.FairScanTheme
|
import org.fairscan.app.ui.theme.FairScanTheme
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
/*
|
||||||
|
* 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.fairscan.app.ui.screens.export
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.lifecycle.viewmodel.CreationExtras
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.fairscan.app.FairScanApp
|
||||||
|
import org.fairscan.app.data.GeneratedPdf
|
||||||
|
import org.fairscan.app.data.ImageRepository
|
||||||
|
import org.fairscan.app.data.PdfFileManager
|
||||||
|
import org.fairscan.app.ui.state.PdfGenerationUiState
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class ExportViewModel(
|
||||||
|
private val pdfFileManager: PdfFileManager,
|
||||||
|
private val imageRepository: ImageRepository,
|
||||||
|
): ViewModel() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getFactory(context: Context) = object : ViewModelProvider.Factory {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
|
||||||
|
val app = context.applicationContext as FairScanApp
|
||||||
|
val pdfFileManager = app.appContainer.pdfFileManager
|
||||||
|
val imageRepository = app.appContainer.imageRepository
|
||||||
|
return ExportViewModel(pdfFileManager, imageRepository) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun generatePdf(): GeneratedPdf = withContext(Dispatchers.IO) {
|
||||||
|
val imageIds = imageRepository.imageIds()
|
||||||
|
val jpegs = imageIds.asSequence()
|
||||||
|
.map { id -> imageRepository.getContent(id) }
|
||||||
|
.filterNotNull()
|
||||||
|
return@withContext pdfFileManager.generatePdf(jpegs)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _pdfUiState = MutableStateFlow(PdfGenerationUiState())
|
||||||
|
val pdfUiState: StateFlow<PdfGenerationUiState> = _pdfUiState.asStateFlow()
|
||||||
|
|
||||||
|
private var generationJob: Job? = null
|
||||||
|
private var desiredFilename: String = ""
|
||||||
|
|
||||||
|
fun setFilename(name: String) {
|
||||||
|
desiredFilename = name
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startPdfGeneration() {
|
||||||
|
cancelPdfGeneration()
|
||||||
|
generationJob = viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val result = generatePdf()
|
||||||
|
_pdfUiState.update {
|
||||||
|
it.copy(
|
||||||
|
isGenerating = false,
|
||||||
|
generatedPdf = result
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("FairScan", "PDF generation failed", e)
|
||||||
|
_pdfUiState.update {
|
||||||
|
it.copy(
|
||||||
|
isGenerating = false,
|
||||||
|
errorMessage = "PDF generation failed"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelPdfGeneration() {
|
||||||
|
generationJob?.cancel()
|
||||||
|
_pdfUiState.value = PdfGenerationUiState()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPdfAsShared() {
|
||||||
|
_pdfUiState.update { it.copy(hasSharedPdf = true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFinalPdf(): GeneratedPdf? {
|
||||||
|
val tempPdf = _pdfUiState.value.generatedPdf ?: return null
|
||||||
|
val tempFile = tempPdf.file
|
||||||
|
val fileName = PdfFileManager.addExtensionIfMissing(desiredFilename)
|
||||||
|
val newFile = File(tempFile.parentFile, fileName)
|
||||||
|
if (tempFile.absolutePath != newFile.absolutePath) {
|
||||||
|
if (newFile.exists()) newFile.delete()
|
||||||
|
val success = tempFile.renameTo(newFile)
|
||||||
|
if (!success) return null
|
||||||
|
_pdfUiState.update {
|
||||||
|
it.copy(generatedPdf = GeneratedPdf(
|
||||||
|
newFile, tempPdf.sizeInBytes, tempPdf.pageCount)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _pdfUiState.value.generatedPdf
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveFile(pdfFile: File): File {
|
||||||
|
val copiedFile = pdfFileManager.copyToExternalDir(pdfFile)
|
||||||
|
_pdfUiState.update { it.copy(savedFileUri = copiedFile.toUri()) }
|
||||||
|
return copiedFile
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cleanUpOldPdfs(thresholdInMillis: Int) {
|
||||||
|
pdfFileManager.cleanUpOldFiles(thresholdInMillis)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
data class PdfGenerationActions(
|
||||||
|
val startGeneration: () -> Unit,
|
||||||
|
val setFilename: (String) -> Unit,
|
||||||
|
val uiStateFlow: StateFlow<PdfGenerationUiState>,// TODO is it ok to have that here?
|
||||||
|
val sharePdf: () -> Unit,
|
||||||
|
val savePdf: () -> Unit,
|
||||||
|
val openPdf: () -> Unit,
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user