Allow other apps to call FairScan to scan a document to PDF (#81)
* Intent for external calls * External calls: start on camera screen * Isolate captured images for each scan session * Remove access to settings when called externally
This commit is contained in:
23
README.md
23
README.md
@@ -62,6 +62,29 @@ FairScan works on any device that:
|
||||
|
||||
---
|
||||
|
||||
## Experimental: Scan to PDF via intent
|
||||
|
||||
FairScan can be invoked by other Android applications to perform a document scan and return a generated PDF.
|
||||
|
||||
This feature is **experimental** and intended for developers who want to rely on FairScan as a
|
||||
simple, privacy-respecting scanning tool.
|
||||
The intent contract and behavior may change between versions, and backward compatibility
|
||||
is not guaranteed at this stage.
|
||||
|
||||
Intent action: `org.fairscan.app.action.SCAN_TO_PDF`
|
||||
|
||||
This is an **implicit intent** that launches FairScan in a dedicated external mode.
|
||||
|
||||
When started via this intent:
|
||||
|
||||
- FairScan opens directly in scan mode
|
||||
- the user scans one or more pages
|
||||
- FairScan generates a single PDF
|
||||
- the resulting PDF is returned to the calling application as a URI with a limited lifetime
|
||||
- the calling application should immediately copy the content of the URI as FairScan deletes it later
|
||||
|
||||
---
|
||||
|
||||
## Technical details
|
||||
|
||||
FairScan uses:
|
||||
|
||||
@@ -27,9 +27,12 @@
|
||||
android:theme="@style/Theme.FairScan">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="org.fairscan.app.action.SCAN_TO_PDF" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
|
||||
@@ -21,16 +21,13 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewmodel.CreationExtras
|
||||
import org.fairscan.app.data.FileLogger
|
||||
import org.fairscan.app.data.ImageRepository
|
||||
import org.fairscan.app.data.LogRepository
|
||||
import org.fairscan.app.data.FileManager
|
||||
import org.fairscan.app.data.LogRepository
|
||||
import org.fairscan.app.data.recentDocumentsDataStore
|
||||
import org.fairscan.app.domain.ImageSegmentationService
|
||||
import org.fairscan.app.platform.AndroidPdfWriter
|
||||
import org.fairscan.app.platform.OpenCvTransformations
|
||||
import org.fairscan.app.ui.screens.about.AboutViewModel
|
||||
import org.fairscan.app.ui.screens.camera.CameraViewModel
|
||||
import org.fairscan.app.ui.screens.export.ExportViewModel
|
||||
import org.fairscan.app.ui.screens.home.HomeViewModel
|
||||
import org.fairscan.app.ui.screens.settings.SettingsRepository
|
||||
import org.fairscan.app.ui.screens.settings.SettingsViewModel
|
||||
@@ -42,15 +39,14 @@ class FairScanApp : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
appContainer = AppContainer(this)
|
||||
appContainer.cleanOrphanSessions()
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
private val cacheDir = context.cacheDir
|
||||
val preparationDir = File(context.cacheDir, "pdfs")
|
||||
val fileManager = FileManager(
|
||||
preparationDir,
|
||||
@@ -72,10 +68,30 @@ class AppContainer(context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
val mainViewModelFactory = viewModelFactory { MainViewModel(it) }
|
||||
val homeViewModelFactory = viewModelFactory { HomeViewModel(it, context) }
|
||||
val cameraViewModelFactory = viewModelFactory { CameraViewModel(it) }
|
||||
val exportViewModelFactory = viewModelFactory { ExportViewModel(it) }
|
||||
val aboutViewModelFactory = viewModelFactory { AboutViewModel(it) }
|
||||
val settingsViewModelFactory = viewModelFactory { SettingsViewModel(it) }
|
||||
|
||||
fun cleanOrphanSessions() {
|
||||
val sessionsRoot = sessionsRoot()
|
||||
if (!sessionsRoot.exists()) return
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
sessionsRoot.listFiles()
|
||||
?.filter { it.isDirectory }
|
||||
?.forEach { dir ->
|
||||
if (isOldSession(dir, now)) {
|
||||
dir.deleteRecursively()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun sessionsRoot(): File = File(cacheDir, "sessions")
|
||||
|
||||
private fun isOldSession(dir: File, now: Long): Boolean {
|
||||
val lastModified = dir.lastModified()
|
||||
return now - lastModified > 24 * 60 * 60 * 1000 // 24h
|
||||
}
|
||||
}
|
||||
|
||||
20
app/src/main/java/org/fairscan/app/LaunchMode.kt
Normal file
20
app/src/main/java/org/fairscan/app/LaunchMode.kt
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
enum class LaunchMode {
|
||||
NORMAL,
|
||||
EXTERNAL_SCAN_TO_PDF
|
||||
}
|
||||
@@ -72,17 +72,36 @@ import org.fairscan.app.ui.screens.settings.SettingsScreen
|
||||
import org.fairscan.app.ui.screens.settings.SettingsViewModel
|
||||
import org.fairscan.app.ui.theme.FairScanTheme
|
||||
import org.opencv.android.OpenCVLoader
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
private lateinit var sessionDir: File
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
initLibraries()
|
||||
val appContainer = (application as FairScanApp).appContainer
|
||||
val viewModel: MainViewModel by viewModels { appContainer.mainViewModelFactory }
|
||||
val launchMode = resolveLaunchMode(intent)
|
||||
sessionDir = when (launchMode) {
|
||||
LaunchMode.NORMAL -> filesDir
|
||||
LaunchMode.EXTERNAL_SCAN_TO_PDF ->
|
||||
File(appContainer.sessionsRoot(), UUID.randomUUID().toString()).apply { mkdirs() }
|
||||
}
|
||||
val sessionContainer = ScanSessionContainer(this, sessionDir)
|
||||
val viewModel: MainViewModel by viewModels {
|
||||
appContainer.viewModelFactory {
|
||||
MainViewModel(sessionContainer.imageRepository, launchMode)
|
||||
}
|
||||
}
|
||||
val exportViewModel: ExportViewModel by viewModels {
|
||||
appContainer.viewModelFactory {
|
||||
ExportViewModel(appContainer, sessionContainer.imageRepository)
|
||||
}
|
||||
}
|
||||
val homeViewModel: HomeViewModel by viewModels { appContainer.homeViewModelFactory }
|
||||
val cameraViewModel: CameraViewModel by viewModels { appContainer.cameraViewModelFactory }
|
||||
val exportViewModel: ExportViewModel by viewModels { appContainer.exportViewModelFactory }
|
||||
val aboutViewModel: AboutViewModel by viewModels { appContainer.aboutViewModelFactory }
|
||||
val settingsViewModel: SettingsViewModel
|
||||
by viewModels { appContainer.settingsViewModelFactory }
|
||||
@@ -102,7 +121,7 @@ class MainActivity : ComponentActivity() {
|
||||
CollectAboutEvents(context, aboutViewModel)
|
||||
|
||||
FairScanTheme {
|
||||
val navigation = navigation(viewModel)
|
||||
val navigation = navigation(viewModel, launchMode)
|
||||
when (val screen = currentScreen) {
|
||||
is Screen.Main.Home -> {
|
||||
val recentDocs by homeViewModel.recentDocuments.collectAsStateWithLifecycle()
|
||||
@@ -131,6 +150,19 @@ class MainActivity : ComponentActivity() {
|
||||
document = document,
|
||||
initialPage = screen.initialPage,
|
||||
navigation = navigation,
|
||||
onExportClick = if (launchMode == LaunchMode.EXTERNAL_SCAN_TO_PDF) {
|
||||
{
|
||||
lifecycleScope.launch {
|
||||
val result = exportViewModel.generatePdfForExternalCall()
|
||||
sendActivityResult(result)
|
||||
viewModel.startNewDocument()
|
||||
finish()
|
||||
}
|
||||
Unit
|
||||
}
|
||||
} else {
|
||||
navigation.toExportScreen
|
||||
},
|
||||
onDeleteImage = { id -> viewModel.deletePage(id) },
|
||||
onRotateImage = { id, clockwise -> viewModel.rotateImage(id, clockwise) },
|
||||
onPageReorder = { id, newIndex -> viewModel.movePage(id, newIndex) },
|
||||
@@ -145,12 +177,12 @@ class MainActivity : ComponentActivity() {
|
||||
setFilename = exportViewModel::setFilename,
|
||||
share = { share(exportViewModel.applyRenaming(), exportViewModel) },
|
||||
save = { exportViewModel.onSaveClicked() },
|
||||
open = { item -> openUri(item.uri, item.format.mimeType) }
|
||||
open = { item -> openUri(item.uri, item.format.mimeType) },
|
||||
),
|
||||
onCloseScan = {
|
||||
viewModel.startNewDocument()
|
||||
viewModel.navigateTo(Screen.Main.Home)
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
is Screen.Overlay.About -> {
|
||||
@@ -170,6 +202,20 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
if (resolveLaunchMode(intent) == LaunchMode.EXTERNAL_SCAN_TO_PDF) {
|
||||
sessionDir.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveLaunchMode(intent: Intent?): LaunchMode {
|
||||
return when (intent?.action) {
|
||||
"org.fairscan.app.action.SCAN_TO_PDF" -> LaunchMode.EXTERNAL_SCAN_TO_PDF
|
||||
else -> LaunchMode.NORMAL
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsScreenWrapper(settingsViewModel: SettingsViewModel, nav: Navigation) {
|
||||
val launcher = rememberLauncherForActivityResult(
|
||||
@@ -265,10 +311,7 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
viewModel.setAsShared()
|
||||
|
||||
val authority = "${applicationContext.packageName}.fileprovider"
|
||||
val uris = result.files.map { file ->
|
||||
FileProvider.getUriForFile(this, authority, file)
|
||||
}
|
||||
val uris = result.files.map(::uriForFile)
|
||||
val intent = Intent().apply {
|
||||
action = if (uris.size == 1) Intent.ACTION_SEND else Intent.ACTION_SEND_MULTIPLE
|
||||
type = result.format.mimeType
|
||||
@@ -293,6 +336,24 @@ class MainActivity : ComponentActivity() {
|
||||
startActivity(chooser)
|
||||
}
|
||||
|
||||
private fun sendActivityResult(result: ExportResult?) {
|
||||
val pdf = result as? ExportResult.Pdf ?: return
|
||||
|
||||
val uri = uriForFile(pdf.file)
|
||||
val resultIntent = Intent().apply {
|
||||
data = uri
|
||||
clipData = ClipData.newRawUri(null, uri)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
|
||||
setResult(RESULT_OK, resultIntent)
|
||||
}
|
||||
|
||||
private fun uriForFile(file: File): Uri {
|
||||
val authority = "${applicationContext.packageName}.fileprovider"
|
||||
return FileProvider.getUriForFile(this, authority, file)
|
||||
}
|
||||
|
||||
private fun checkPermissionThen(
|
||||
requestPermissionLauncher: ManagedActivityResultLauncher<String, Boolean>,
|
||||
action: () -> Unit
|
||||
@@ -312,8 +373,7 @@ class MainActivity : ComponentActivity() {
|
||||
if (fileUri.scheme == ContentResolver.SCHEME_CONTENT) {
|
||||
fileUri
|
||||
} else {
|
||||
val authority = "${applicationContext.packageName}.fileprovider"
|
||||
FileProvider.getUriForFile(this, authority, fileUri.toFile())
|
||||
uriForFile(fileUri.toFile())
|
||||
}
|
||||
val openIntent = Intent(Intent.ACTION_VIEW).apply {
|
||||
setDataAndType(uriToOpen, mimeType)
|
||||
@@ -335,15 +395,28 @@ class MainActivity : ComponentActivity() {
|
||||
Log.d("OpenCV", "Initialization successful")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigation(viewModel: MainViewModel): Navigation = Navigation(
|
||||
private fun navigation(viewModel: MainViewModel, launchMode: LaunchMode): Navigation = Navigation(
|
||||
toHomeScreen = { viewModel.navigateTo(Screen.Main.Home) },
|
||||
toCameraScreen = { viewModel.navigateTo(Screen.Main.Camera) },
|
||||
toDocumentScreen = { viewModel.navigateTo(Screen.Main.Document()) },
|
||||
toExportScreen = { viewModel.navigateTo(Screen.Main.Export) },
|
||||
toAboutScreen = { viewModel.navigateTo(Screen.Overlay.About) },
|
||||
toLibrariesScreen = { viewModel.navigateTo(Screen.Overlay.Libraries) },
|
||||
toSettingsScreen = { viewModel.navigateTo(Screen.Overlay.Settings) },
|
||||
back = { viewModel.navigateBack() }
|
||||
toSettingsScreen = if (launchMode == LaunchMode.EXTERNAL_SCAN_TO_PDF) null else {
|
||||
{
|
||||
viewModel.navigateTo(Screen.Overlay.Settings)
|
||||
}
|
||||
},
|
||||
back = {
|
||||
val origin = viewModel.currentScreen.value
|
||||
viewModel.navigateBack()
|
||||
val destination = viewModel.currentScreen.value
|
||||
if (destination == origin && launchMode == LaunchMode.EXTERNAL_SCAN_TO_PDF) {
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -26,15 +26,14 @@ import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.fairscan.app.data.ImageRepository
|
||||
import org.fairscan.app.ui.NavigationState
|
||||
import org.fairscan.app.ui.Screen
|
||||
import org.fairscan.app.ui.state.DocumentUiModel
|
||||
|
||||
class MainViewModel(appContainer: AppContainer): ViewModel() {
|
||||
class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode): ViewModel() {
|
||||
|
||||
private val imageRepository = appContainer.imageRepository
|
||||
|
||||
private val _navigationState = MutableStateFlow(NavigationState.initial())
|
||||
private val _navigationState = MutableStateFlow(NavigationState.initial(launchMode))
|
||||
val currentScreen: StateFlow<Screen> = _navigationState.map { it.current }
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, _navigationState.value.current)
|
||||
|
||||
|
||||
34
app/src/main/java/org/fairscan/app/ScanSessionContainer.kt
Normal file
34
app/src/main/java/org/fairscan/app/ScanSessionContainer.kt
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.content.Context
|
||||
import org.fairscan.app.data.ImageRepository
|
||||
import org.fairscan.app.platform.OpenCvTransformations
|
||||
import java.io.File
|
||||
|
||||
class ScanSessionContainer(
|
||||
context: Context,
|
||||
scanRootDir: File
|
||||
) {
|
||||
private val density = context.resources.displayMetrics.density
|
||||
private val thumbnailSizePx = (THUMBNAIL_SIZE_DP * density).toInt()
|
||||
|
||||
val imageRepository = ImageRepository(
|
||||
scanRootDir,
|
||||
OpenCvTransformations(),
|
||||
thumbnailSizePx
|
||||
)
|
||||
}
|
||||
@@ -23,16 +23,16 @@ const val SCAN_DIR_NAME = "scanned_pages"
|
||||
const val THUMBNAIL_DIR_NAME = "thumbnails"
|
||||
|
||||
class ImageRepository(
|
||||
appFilesDir: File,
|
||||
scanRootDir: File,
|
||||
val transformations: ImageTransformations,
|
||||
private val thumbnailSizePx: Int,
|
||||
) {
|
||||
|
||||
private val scanDir: File = File(appFilesDir, SCAN_DIR_NAME).apply {
|
||||
private val scanDir: File = File(scanRootDir, SCAN_DIR_NAME).apply {
|
||||
if (!exists()) mkdirs()
|
||||
}
|
||||
|
||||
private val thumbnailDir: File = File(appFilesDir, THUMBNAIL_DIR_NAME).apply {
|
||||
private val thumbnailDir: File = File(scanRootDir, THUMBNAIL_DIR_NAME).apply {
|
||||
if (!exists()) mkdirs()
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
*/
|
||||
package org.fairscan.app.ui
|
||||
|
||||
import org.fairscan.app.LaunchMode
|
||||
|
||||
sealed class Screen {
|
||||
sealed class Main : Screen() {
|
||||
object Home : Main()
|
||||
@@ -35,15 +37,24 @@ data class Navigation(
|
||||
val toExportScreen: () -> Unit,
|
||||
val toAboutScreen: () -> Unit,
|
||||
val toLibrariesScreen: () -> Unit,
|
||||
val toSettingsScreen: () -> Unit,
|
||||
val toSettingsScreen: (() -> Unit)?,
|
||||
val back: () -> Unit,
|
||||
)
|
||||
|
||||
fun startScreenFor(mode: LaunchMode): Screen.Main =
|
||||
when (mode) {
|
||||
LaunchMode.NORMAL -> Screen.Main.Home
|
||||
LaunchMode.EXTERNAL_SCAN_TO_PDF -> Screen.Main.Camera
|
||||
}
|
||||
|
||||
@ConsistentCopyVisibility
|
||||
data class NavigationState private constructor(val stack: List<Screen>) {
|
||||
data class NavigationState private constructor(val stack: List<Screen>, val root: Screen.Main) {
|
||||
|
||||
companion object {
|
||||
fun initial() = NavigationState(listOf(Screen.Main.Home))
|
||||
fun initial(mode: LaunchMode): NavigationState {
|
||||
val root = startScreenFor(mode)
|
||||
return NavigationState(listOf(root), root)
|
||||
}
|
||||
}
|
||||
|
||||
val current: Screen get() = stack.last()
|
||||
@@ -58,6 +69,7 @@ data class NavigationState private constructor(val stack: List<Screen>) {
|
||||
|
||||
fun navigateBack(): NavigationState {
|
||||
return when (current) {
|
||||
root -> this // Back handled by system
|
||||
is Screen.Main.Home -> this // Back handled by system
|
||||
is Screen.Main.Camera -> copy(stack = listOf(Screen.Main.Home))
|
||||
is Screen.Main.Document -> copy(stack = listOf(Screen.Main.Camera))
|
||||
|
||||
@@ -181,14 +181,16 @@ fun AppOverflowMenu(
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
) {
|
||||
|
||||
navigation.toSettingsScreen?.let { toSettings ->
|
||||
DropdownMenuItem(
|
||||
leadingIcon = { Icon(Icons.Default.Settings, contentDescription = null) },
|
||||
text = { Text(stringResource(R.string.settings)) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
navigation.toSettingsScreen()
|
||||
toSettings()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
DropdownMenuItem(
|
||||
leadingIcon = { Icon(Icons.Default.Info, contentDescription = null) },
|
||||
|
||||
@@ -73,6 +73,7 @@ fun DocumentScreen(
|
||||
document: DocumentUiModel,
|
||||
initialPage: Int,
|
||||
navigation: Navigation,
|
||||
onExportClick: () -> Unit,
|
||||
onDeleteImage: (String) -> Unit,
|
||||
onRotateImage: (String, Boolean) -> Unit,
|
||||
onPageReorder: (String, Int) -> Unit,
|
||||
@@ -105,7 +106,7 @@ fun DocumentScreen(
|
||||
),
|
||||
onBack = navigation.back,
|
||||
bottomBar = {
|
||||
BottomBar(navigation)
|
||||
BottomBar(onExportClick)
|
||||
},
|
||||
pageListButton = {
|
||||
SecondaryActionButton(
|
||||
@@ -217,7 +218,7 @@ fun RotationButtons(
|
||||
|
||||
@Composable
|
||||
private fun BottomBar(
|
||||
navigation: Navigation,
|
||||
onExportClick: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -225,7 +226,7 @@ private fun BottomBar(
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
MainActionButton(
|
||||
onClick = navigation.toExportScreen,
|
||||
onClick = onExportClick,
|
||||
icon = Icons.Default.Description,
|
||||
text = stringResource(R.string.export),
|
||||
)
|
||||
@@ -243,6 +244,7 @@ fun DocumentScreenPreview() {
|
||||
),
|
||||
initialPage = 1,
|
||||
navigation = dummyNavigation(),
|
||||
onExportClick = {},
|
||||
onDeleteImage = { _ -> },
|
||||
onRotateImage = { _,_ -> },
|
||||
onPageReorder = { _,_ -> },
|
||||
|
||||
@@ -75,7 +75,6 @@ import org.fairscan.app.ui.components.NewDocumentDialog
|
||||
import org.fairscan.app.ui.components.isLandscape
|
||||
import org.fairscan.app.ui.components.pageCountText
|
||||
import org.fairscan.app.ui.dummyNavigation
|
||||
import org.fairscan.app.ui.screens.settings.ExportFormat
|
||||
import org.fairscan.app.ui.screens.settings.ExportFormat.PDF
|
||||
import org.fairscan.app.ui.theme.FairScanTheme
|
||||
import java.io.File
|
||||
|
||||
@@ -21,7 +21,6 @@ data class ExportUiState(
|
||||
val format: ExportFormat = ExportFormat.PDF,
|
||||
val isGenerating: Boolean = false,
|
||||
val result: ExportResult? = null,
|
||||
val desiredFilename: String = "",
|
||||
val savedBundle: SavedBundle? = null,
|
||||
val hasShared: Boolean = false,
|
||||
val errorMessage: String? = null,
|
||||
|
||||
@@ -35,6 +35,7 @@ import kotlinx.coroutines.withContext
|
||||
import org.fairscan.app.AppContainer
|
||||
import org.fairscan.app.RecentDocument
|
||||
import org.fairscan.app.data.FileManager
|
||||
import org.fairscan.app.data.ImageRepository
|
||||
import org.fairscan.app.ui.screens.settings.ExportFormat
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
@@ -46,11 +47,10 @@ sealed interface ExportEvent {
|
||||
data object SaveError : ExportEvent
|
||||
}
|
||||
|
||||
class ExportViewModel(container: AppContainer): ViewModel() {
|
||||
class ExportViewModel(container: AppContainer, val imageRepository: ImageRepository): ViewModel() {
|
||||
|
||||
private val preparationDir = container.preparationDir
|
||||
private val fileManager = container.fileManager
|
||||
private val imageRepository = container.imageRepository
|
||||
private val settingsRepository = container.settingsRepository
|
||||
private val recentDocumentsDataStore = container.recentDocumentsDataStore
|
||||
private val logger = container.logger
|
||||
@@ -66,6 +66,10 @@ class ExportViewModel(container: AppContainer): ViewModel() {
|
||||
return@withContext ExportResult.Pdf(pdf.file, pdf.sizeInBytes, pdf.pageCount)
|
||||
}
|
||||
|
||||
suspend fun generatePdfForExternalCall(): ExportResult.Pdf {
|
||||
return generatePdf()
|
||||
}
|
||||
|
||||
private val _uiState = MutableStateFlow(ExportUiState())
|
||||
val uiState: StateFlow<ExportUiState> = _uiState.asStateFlow()
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package org.fairscan.app.ui
|
||||
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.fairscan.app.LaunchMode
|
||||
import org.fairscan.app.ui.Screen.Main.Camera
|
||||
import org.fairscan.app.ui.Screen.Main.Document
|
||||
import org.fairscan.app.ui.Screen.Main.Export
|
||||
@@ -27,14 +28,14 @@ class NavigationTest {
|
||||
|
||||
@Test
|
||||
fun empty_ScreenStack() {
|
||||
val empty = NavigationState.initial()
|
||||
val empty = NavigationState.initial(LaunchMode.NORMAL)
|
||||
assertThat(empty.current).isEqualTo(Home)
|
||||
assertThat(empty.navigateBack()).isEqualTo(empty)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun navigate_between_fixed_screens() {
|
||||
val atHome = NavigationState.initial()
|
||||
val atHome = NavigationState.initial(LaunchMode.NORMAL)
|
||||
val atCamera = atHome.navigateTo(Camera)
|
||||
val atDocument = atHome.navigateTo(Document())
|
||||
val atExport = atHome.navigateTo(Export)
|
||||
@@ -56,7 +57,7 @@ class NavigationTest {
|
||||
|
||||
@Test
|
||||
fun navigate_to_secondary_screens() {
|
||||
val atHome = NavigationState.initial()
|
||||
val atHome = NavigationState.initial(LaunchMode.NORMAL)
|
||||
val atCamera = atHome.navigateTo(Camera)
|
||||
|
||||
val atAboutAfterHome = atHome.navigateTo(About)
|
||||
@@ -71,4 +72,11 @@ class NavigationTest {
|
||||
assertThat(atLibrariesAfterCameraAbout.current).isEqualTo(Libraries)
|
||||
assertThat(atLibrariesAfterCameraAbout.navigateBack()).isEqualTo(atAboutAfterCamera)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun external_call() {
|
||||
val initial = NavigationState.initial(LaunchMode.EXTERNAL_SCAN_TO_PDF)
|
||||
assertThat(initial.current).isEqualTo(Camera)
|
||||
assertThat(initial.navigateBack().current).isEqualTo(Camera)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user