Skip to content
2 changes: 2 additions & 0 deletions app/src/main/java/org/fairscan/app/FairScanApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import org.fairscan.app.data.FileLogger
import org.fairscan.app.data.DocumentLibrary
import org.fairscan.app.data.FileManager
import org.fairscan.app.data.LogRepository
import org.fairscan.app.data.OcrLanguageRepository
Expand Down Expand Up @@ -57,6 +58,7 @@ class AppContainer(context: Context) {
private val cacheDir = context.cacheDir
private val dataStore = context.dataStore
val preparationDir = File(context.cacheDir, "pdfs")
val documentLibrary = DocumentLibrary(context.applicationContext)
val ocrLanguageRepository =
OcrLanguageRepository(dataStore, File(context.filesDir, "tesseract/tessdata"))
val ocrService = OcrService(ocrLanguageRepository, scope)
Expand Down
1,130 changes: 593 additions & 537 deletions app/src/main/java/org/fairscan/app/MainActivity.kt

Large diffs are not rendered by default.

23 changes: 15 additions & 8 deletions app/src/main/java/org/fairscan/app/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ import org.fairscan.imageprocessing.Quad
import kotlin.math.min

@OptIn(ExperimentalCoroutinesApi::class)
class MainViewModel(val imageRepository: ImageRepository, logger: Logger): ViewModel() {
class MainViewModel(
val imageRepository: ImageRepository,
logger: Logger,
private val launchMode: LaunchMode = LaunchMode.NORMAL,
): ViewModel() {

private val _navigationState = MutableStateFlow<NavigationState?>(null)
val currentScreen: StateFlow<Screen?> = _navigationState.map { it?.current }
Expand All @@ -69,12 +73,9 @@ class MainViewModel(val imageRepository: ImageRepository, logger: Logger): ViewM

_pages.value = pages

_navigationState.value =
if (pages.isEmpty()) {
NavigationState.initial()
} else {
NavigationState.initial().navigateTo(Screen.Main.ResumeScan)
}
_navigationState.value = NavigationState.initial(
externalScan = launchMode == LaunchMode.EXTERNAL_SCAN_TO_PDF
)
}
}

Expand Down Expand Up @@ -142,6 +143,10 @@ class MainViewModel(val imageRepository: ImageRepository, logger: Logger): ViewM
_navigationState.update { stack -> stack?.navigateBack() }
}

fun openScanner() {
navigateTo(if (_pages.value.isEmpty()) Screen.Main.Camera else Screen.Main.ResumeScan)
}

fun rotateCurrentPage(clockwise: Boolean) {
viewModelScope.launch {
val pages = withContext(Dispatchers.IO) {
Expand Down Expand Up @@ -217,12 +222,14 @@ class MainViewModel(val imageRepository: ImageRepository, logger: Logger): ViewM
"No current page for index $index (${pages.size} pages)")
}

fun startNewDocument() {
fun startNewDocument(onCleared: () -> Unit = {}) {
_pages.value = persistentListOf()
viewModelScope.launch {
withContext(Dispatchers.IO) {
imageRepository.clear()
}
_currentPageIndex.value = 0
onCleared()
}
}

Expand Down
44 changes: 44 additions & 0 deletions app/src/main/java/org/fairscan/app/data/DocumentFilenames.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright 2025-2026 The FairScan authors
*
* 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.data

import java.io.File

fun safeDocumentName(name: String): String {
val cleaned = name.trim()
.replace(Regex("[\\\\/:*?\"<>|\\p{Cntrl}]"), "_")
.trim('.', ' ')
.ifBlank { "FairScan" }
val extension = cleaned.substringAfterLast('.', "")
.takeIf { it.lowercase() in setOf("pdf", "jpg", "jpeg") }
?.let { ".$it" }.orEmpty()
var base = cleaned.removeSuffix(extension)
// Android filesystems limit names in bytes; leave room for an extension and collision suffix.
while (base.toByteArray(Charsets.UTF_8).size > 180) {
base = base.substring(0, base.offsetByCodePoints(base.length, -1))
}
return base.ifBlank { "FairScan" } + extension
}

fun availableDocumentFile(directory: File, filename: String): File {
val desired = File(directory, safeDocumentName(filename))
var target = desired
var suffix = 1
while (target.exists()) {
target = File(directory, "${desired.nameWithoutExtension} ($suffix).${desired.extension}")
suffix++
}
return target
}
Loading