Split MainViewModel: extract HomeViewModel

This commit is contained in:
Pierre-Yves Nicolas
2025-11-21 16:27:18 +01:00
committed by pynicolas
parent 7c53dcface
commit d4a3c78c23
4 changed files with 90 additions and 51 deletions

View File

@@ -53,7 +53,7 @@ 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.screens.AboutScreen import org.fairscan.app.ui.screens.AboutScreen
import org.fairscan.app.ui.screens.DocumentScreen import org.fairscan.app.ui.screens.DocumentScreen
import org.fairscan.app.ui.screens.HomeScreen import org.fairscan.app.ui.screens.home.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.CameraScreen
@@ -61,6 +61,7 @@ import org.fairscan.app.ui.screens.camera.CameraViewModel
import org.fairscan.app.ui.screens.export.ExportScreenWrapper import org.fairscan.app.ui.screens.export.ExportScreenWrapper
import org.fairscan.app.ui.screens.export.ExportViewModel import org.fairscan.app.ui.screens.export.ExportViewModel
import org.fairscan.app.ui.screens.export.PdfGenerationActions import org.fairscan.app.ui.screens.export.PdfGenerationActions
import org.fairscan.app.ui.screens.home.HomeViewModel
import org.fairscan.app.ui.theme.FairScanTheme import org.fairscan.app.ui.theme.FairScanTheme
import org.opencv.android.OpenCVLoader import org.opencv.android.OpenCVLoader
@@ -72,6 +73,7 @@ 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) }
val homeViewModel: HomeViewModel by viewModels { HomeViewModel.getFactory(this) }
val cameraViewModel: CameraViewModel by viewModels { CameraViewModel.getFactory(this) } val cameraViewModel: CameraViewModel by viewModels { CameraViewModel.getFactory(this) }
val exportViewModel: ExportViewModel by viewModels { ExportViewModel.getFactory(this) } val exportViewModel: ExportViewModel by viewModels { ExportViewModel.getFactory(this) }
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
@@ -91,7 +93,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(exportViewModel.getFinalPdf(), viewModel, exportViewModel) } val savePdf = { savePdf(exportViewModel.getFinalPdf(), homeViewModel, exportViewModel) }
val storagePermissionLauncher = rememberLauncherForActivityResult( val storagePermissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission() ActivityResultContracts.RequestPermission()
) { isGranted -> ) { isGranted ->
@@ -114,7 +116,7 @@ class MainActivity : ComponentActivity() {
) )
when (val screen = currentScreen) { when (val screen = currentScreen) {
is Screen.Main.Home -> { is Screen.Main.Home -> {
val recentDocs by viewModel.recentDocuments.collectAsStateWithLifecycle() val recentDocs by homeViewModel.recentDocuments.collectAsStateWithLifecycle()
HomeScreen( HomeScreen(
cameraPermission = cameraPermission, cameraPermission = cameraPermission,
currentDocument = document, currentDocument = document,
@@ -210,7 +212,7 @@ class MainActivity : ComponentActivity() {
private fun savePdf( private fun savePdf(
generatedPdf: GeneratedPdf?, generatedPdf: GeneratedPdf?,
viewModel: MainViewModel, homeViewModel: HomeViewModel,
exportViewModel: ExportViewModel exportViewModel: ExportViewModel
) { ) {
if (generatedPdf == null) if (generatedPdf == null)
@@ -220,7 +222,7 @@ class MainActivity : ComponentActivity() {
appScope.launch { appScope.launch {
try { try {
val targetFile = exportViewModel.saveFile(generatedPdf.file) val targetFile = exportViewModel.saveFile(generatedPdf.file)
viewModel.addRecentDocument(targetFile.absolutePath, generatedPdf.pageCount) homeViewModel.addRecentDocument(targetFile.absolutePath, generatedPdf.pageCount)
suspendCancellableCoroutine { continuation -> suspendCancellableCoroutine { continuation ->
MediaScannerConnection.scanFile( MediaScannerConnection.scanFile(

View File

@@ -17,7 +17,6 @@ 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 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
@@ -31,16 +30,12 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.fairscan.app.data.ImageRepository import org.fairscan.app.data.ImageRepository
import org.fairscan.app.data.recentDocumentsDataStore
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.RecentDocumentUiState
import java.io.File
class MainViewModel( class MainViewModel(
private val imageRepository: ImageRepository, private val imageRepository: ImageRepository
private val recentDocumentsDataStore: DataStore<RecentDocuments>,
): ViewModel() { ): ViewModel() {
companion object { companion object {
@@ -48,10 +43,7 @@ class MainViewModel(
@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 app = context.applicationContext as FairScanApp val app = context.applicationContext as FairScanApp
return MainViewModel( return MainViewModel(app.appContainer.imageRepository) as T
app.appContainer.imageRepository,
context.recentDocumentsDataStore,
) as T
} }
} }
} }
@@ -116,41 +108,6 @@ class MainViewModel(
return bytes?.let { BitmapFactory.decodeByteArray(it, 0, it.size) } return bytes?.let { BitmapFactory.decodeByteArray(it, 0, it.size) }
} }
val recentDocuments: StateFlow<List<RecentDocumentUiState>> =
recentDocumentsDataStore.data.map {
it.documentsList.map {
doc ->
RecentDocumentUiState(
file = File(doc.filePath),
saveTimestamp = doc.createdAt,
pageCount = doc.pageCount,
)
}.filter { doc -> doc.file.exists() }
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = emptyList(),
)
fun addRecentDocument(filePath: String, pageCount: Int) {
viewModelScope.launch {
recentDocumentsDataStore.updateData { current ->
val newDoc = RecentDocument.newBuilder()
.setFilePath(filePath)
.setPageCount(pageCount)
.setCreatedAt(System.currentTimeMillis())
.build()
current.toBuilder()
.addDocuments(0, newDoc)
.also { builder ->
while (builder.documentsCount > 3) {
builder.removeDocuments(builder.documentsCount - 1)
}
}
.build()
}
}
}
fun handleImageCaptured(jpegBytes: ByteArray) { fun handleImageCaptured(jpegBytes: ByteArray) {
imageRepository.add(jpegBytes) imageRepository.add(jpegBytes)
_pageIds.value = imageRepository.imageIds() _pageIds.value = imageRepository.imageIds()

View File

@@ -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.home
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable

View File

@@ -0,0 +1,80 @@
/*
* 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.home
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.fairscan.app.RecentDocument
import org.fairscan.app.RecentDocuments
import org.fairscan.app.data.recentDocumentsDataStore
import org.fairscan.app.ui.state.RecentDocumentUiState
import java.io.File
class HomeViewModel(private val recentDocumentsDataStore: DataStore<RecentDocuments>): ViewModel() {
companion object {
fun getFactory(context: Context) = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
return HomeViewModel(context.recentDocumentsDataStore) as T
}
}
}
val recentDocuments: StateFlow<List<RecentDocumentUiState>> =
recentDocumentsDataStore.data.map {
it.documentsList.map {
doc ->
RecentDocumentUiState(
file = File(doc.filePath),
saveTimestamp = doc.createdAt,
pageCount = doc.pageCount,
)
}.filter { doc -> doc.file.exists() }
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = emptyList(),
)
fun addRecentDocument(filePath: String, pageCount: Int) {
viewModelScope.launch {
recentDocumentsDataStore.updateData { current ->
val newDoc = RecentDocument.newBuilder()
.setFilePath(filePath)
.setPageCount(pageCount)
.setCreatedAt(System.currentTimeMillis())
.build()
current.toBuilder()
.addDocuments(0, newDoc)
.also { builder ->
while (builder.documentsCount > 3) {
builder.removeDocuments(builder.documentsCount - 1)
}
}
.build()
}
}
}
}