Remove Home screen

This commit is contained in:
Pierre-Yves Nicolas
2026-05-11 16:09:18 +02:00
parent 9dedaaec9d
commit e43da6b9aa
29 changed files with 36 additions and 589 deletions

View File

@@ -3,7 +3,6 @@ plugins {
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.aboutLibrariesAndroid)
alias(libs.plugins.protobuf)
alias(libs.plugins.kotlin.serialization)
}
@@ -122,10 +121,8 @@ dependencies {
implementation(libs.androidx.camera.camera2)
implementation(libs.androidx.camera.lifecycle)
implementation(libs.androidx.camera.view)
implementation(libs.androidx.datastore)
implementation(libs.androidx.datastore.preferences)
implementation(libs.androidx.documentfile)
implementation(libs.protobuf.javalite)
implementation(libs.litert)
implementation(libs.litert.support)
implementation(libs.litert.metadata)
@@ -160,21 +157,6 @@ aboutLibraries {
}
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:4.32.0"
}
generateProtoTasks {
all().forEach { task ->
task.builtins {
create("java") {
option("lite")
}
}
}
}
}
// See https://developer.android.com/build/configure-apk-splits
androidComponents {
onVariants { variant ->

View File

@@ -23,12 +23,10 @@ import androidx.lifecycle.viewmodel.CreationExtras
import org.fairscan.app.data.FileLogger
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.AndroidImageLoader
import org.fairscan.app.platform.AndroidPdfWriter
import org.fairscan.app.ui.screens.camera.CameraViewModel
import org.fairscan.app.ui.screens.home.HomeViewModel
import org.fairscan.app.ui.screens.settings.SettingsRepository
import org.fairscan.app.ui.screens.settings.SettingsViewModel
import java.io.File
@@ -57,7 +55,6 @@ class AppContainer(context: Context) {
val logger = FileLogger(logRepository)
val imageSegmentationService = ImageSegmentationService(context, logger)
val imageLoader = AndroidImageLoader(context.contentResolver)
val recentDocumentsDataStore = context.recentDocumentsDataStore
val settingsRepository = SettingsRepository(context)
@Suppress("UNCHECKED_CAST")
@@ -69,7 +66,6 @@ class AppContainer(context: Context) {
}
}
val homeViewModelFactory = viewModelFactory { HomeViewModel(it, context) }
val cameraViewModelFactory = viewModelFactory { CameraViewModel(it) }
val settingsViewModelFactory = viewModelFactory { SettingsViewModel(it) }

View File

@@ -54,7 +54,6 @@ import org.fairscan.app.data.ImageRepository
import org.fairscan.app.ui.Navigation
import org.fairscan.app.ui.Screen
import org.fairscan.app.ui.components.rememberCameraPermissionState
import org.fairscan.app.ui.screens.document.DocumentScreen
import org.fairscan.app.ui.screens.LibrariesScreen
import org.fairscan.app.ui.screens.about.AboutEvent
import org.fairscan.app.ui.screens.about.AboutScreen
@@ -63,14 +62,12 @@ import org.fairscan.app.ui.screens.about.createEmailWithImageIntent
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.document.DocumentScreen
import org.fairscan.app.ui.screens.export.ExportActions
import org.fairscan.app.ui.screens.export.ExportEvent
import org.fairscan.app.ui.screens.export.ExportResult
import org.fairscan.app.ui.screens.export.ExportScreenWrapper
import org.fairscan.app.ui.screens.export.ExportViewModel
import org.fairscan.app.ui.screens.home.HomeScreen
import org.fairscan.app.ui.screens.home.HomeViewModel
import org.fairscan.app.ui.screens.settings.ExportFormat
import org.fairscan.app.ui.screens.settings.SettingsScreen
import org.fairscan.app.ui.screens.settings.SettingsViewModel
import org.fairscan.app.ui.theme.FairScanTheme
@@ -97,7 +94,7 @@ class MainActivity : ComponentActivity() {
val imageRepository = sessionViewModel.imageRepository
val viewModel: MainViewModel by viewModels {
appContainer.viewModelFactory {
MainViewModel(imageRepository, launchMode)
MainViewModel(imageRepository)
}
}
val exportViewModel: ExportViewModel by viewModels {
@@ -110,7 +107,6 @@ class MainActivity : ComponentActivity() {
AboutViewModel(appContainer, imageRepository)
}
}
val homeViewModel: HomeViewModel by viewModels { appContainer.homeViewModelFactory }
val cameraViewModel: CameraViewModel by viewModels { appContainer.cameraViewModelFactory }
val settingsViewModel: SettingsViewModel
@@ -157,16 +153,8 @@ class MainActivity : ComponentActivity() {
}
when (currentScreen) {
is Screen.Main.Home -> {
val recentDocs by homeViewModel.recentDocuments.collectAsStateWithLifecycle()
HomeScreen(
cameraPermission = cameraPermission,
currentDocument = document,
navigation = navigation,
onClearScan = { viewModel.startNewDocument() },
recentDocuments = recentDocs,
onOpenPdf = { fileUri -> openUri(fileUri, ExportFormat.PDF.mimeType, logger) }
)
null -> {
// waiting to load pages to get an initial screen
}
is Screen.Main.Camera -> {
val pickMultiple = rememberLauncherForActivityResult(
@@ -216,7 +204,7 @@ class MainActivity : ComponentActivity() {
onCloseScan = {
exportViewModel.resetFilename()
viewModel.startNewDocument()
viewModel.navigateTo(Screen.Main.Home)
viewModel.navigateTo(Screen.Main.Camera)
}
)
}
@@ -468,7 +456,6 @@ class MainActivity : ComponentActivity() {
}
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) },

View File

@@ -45,17 +45,27 @@ import org.fairscan.imageprocessing.ColorMode
import kotlin.math.min
@OptIn(ExperimentalCoroutinesApi::class)
class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode): ViewModel() {
class MainViewModel(val imageRepository: ImageRepository): ViewModel() {
private val _navigationState = MutableStateFlow(NavigationState.initial(launchMode))
val currentScreen: StateFlow<Screen> = _navigationState.map { it.current }
.stateIn(viewModelScope, SharingStarted.Eagerly, _navigationState.value.current)
private val _navigationState = MutableStateFlow<NavigationState?>(null)
val currentScreen: StateFlow<Screen?> = _navigationState.map { it?.current }
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
private val _pages = MutableStateFlow<List<ScanPage>>(emptyList())
init {
viewModelScope.launch {
_pages.value = imageRepository.pages()
val pages = imageRepository.pages()
_pages.value = pages
_navigationState.value =
if (pages.isEmpty()) {
NavigationState.initial()
} else {
NavigationState.initial().navigateTo(Screen.Main.Export)
}
}
}
@@ -110,11 +120,11 @@ class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode
}
_currentPageIndex.value = min(_pages.value.size - 1, destination.initialPage)
}
_navigationState.update { it.navigateTo(destination) }
_navigationState.update { it?.navigateTo(destination) }
}
fun navigateBack() {
_navigationState.update { stack -> stack.navigateBack() }
_navigationState.update { stack -> stack?.navigateBack() }
}
fun rotateCurrentPage(clockwise: Boolean) {

View File

@@ -1,47 +0,0 @@
/*
* Copyright 2025-2026 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.data
import android.content.Context
import androidx.datastore.core.CorruptionException
import androidx.datastore.core.DataStore
import androidx.datastore.core.Serializer
import androidx.datastore.dataStore
import com.google.protobuf.InvalidProtocolBufferException
import org.fairscan.app.RecentDocuments
import java.io.InputStream
import java.io.OutputStream
object RecentDocumentsSerializer : Serializer<RecentDocuments> {
override val defaultValue: RecentDocuments = RecentDocuments.getDefaultInstance()
override suspend fun readFrom(input: InputStream): RecentDocuments {
return try {
RecentDocuments.parseFrom(input)
} catch (e: InvalidProtocolBufferException) {
throw CorruptionException("Cannot read proto.", e)
}
}
override suspend fun writeTo(
t: RecentDocuments,
output: OutputStream
) = t.writeTo(output)
}
val Context.recentDocumentsDataStore: DataStore<RecentDocuments> by dataStore(
fileName = "recent_documents.pb",
serializer = RecentDocumentsSerializer
)

View File

@@ -14,11 +14,8 @@
*/
package org.fairscan.app.ui
import org.fairscan.app.LaunchMode
sealed class Screen {
sealed class Main : Screen() {
object Home : Main()
object Camera : Main()
data class Document(val initialPage: Int = 0) : Main()
object Export : Main()
@@ -31,7 +28,6 @@ sealed class Screen {
}
data class Navigation(
val toHomeScreen: () -> Unit,
val toCameraScreen: () -> Unit,
val toDocumentScreen: () -> Unit,
val toExportScreen: () -> Unit,
@@ -41,18 +37,12 @@ data class Navigation(
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>, val root: Screen.Main) {
companion object {
fun initial(mode: LaunchMode): NavigationState {
val root = startScreenFor(mode)
fun initial(): NavigationState {
val root = Screen.Main.Camera
return NavigationState(listOf(root), root)
}
}
@@ -70,8 +60,7 @@ data class NavigationState private constructor(val stack: List<Screen>, val root
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.Camera -> this // Back handled by system
is Screen.Main.Document -> copy(stack = listOf(Screen.Main.Camera))
is Screen.Main.Export -> copy(stack = listOf(Screen.Main.Camera))
is Screen.Overlay -> copy(stack = stack.dropLast(1))

View File

@@ -16,7 +16,6 @@ package org.fairscan.app.ui
import android.content.Context
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import org.fairscan.app.domain.Jpeg
import org.fairscan.app.domain.PageViewKey
@@ -26,11 +25,7 @@ import org.fairscan.app.ui.state.PageThumbnail
import org.fairscan.imageprocessing.ColorMode
fun dummyNavigation(): Navigation {
return Navigation({}, {}, {}, {}, {}, {}, {}, {})
}
fun fakeDocument(): DocumentUiModel {
return DocumentUiModel(persistentListOf())
return Navigation({}, {}, {}, {}, {}, {}, {})
}
fun fakeDocument(pageIds: ImmutableList<String>, context: Context): DocumentUiModel {

View File

@@ -141,8 +141,6 @@ fun CameraScreen(
val isTorchEnabled by cameraViewModel.isTorchEnabled.collectAsStateWithLifecycle()
var torchReapplied by remember { mutableStateOf(false) }
BackHandler { navigation.back() }
val captureController = remember { CameraCaptureController() }
DisposableEffect(Unit) {
onDispose {

View File

@@ -42,7 +42,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.fairscan.app.AppContainer
import org.fairscan.app.R
import org.fairscan.app.RecentDocument
import org.fairscan.app.data.FileManager
import org.fairscan.app.data.ImageRepository
import org.fairscan.app.domain.ExportQuality
@@ -69,7 +68,6 @@ class ExportViewModel(container: AppContainer, val imageRepository: ImageReposit
private val preparationDir = container.preparationDir
private val fileManager = container.fileManager
private val settingsRepository = container.settingsRepository
private val recentDocumentsDataStore = container.recentDocumentsDataStore
private val logger = container.logger
private val _events = MutableSharedFlow<ExportEvent>()
@@ -310,12 +308,6 @@ class ExportViewModel(container: AppContainer, val imageRepository: ImageReposit
val bundle = SavedBundle(savedItems, saveDir)
_uiState.update { it.copy(savedBundle = bundle) }
if (exportFormat == ExportFormat.PDF) {
savedItems.forEach { item ->
addRecentDocument(item.uri, item.fileName, result.pageCount)
}
}
filesForMediaScan.forEach { f -> mediaScan(context, f, exportFormat.mimeType) }
}
@@ -395,27 +387,6 @@ class ExportViewModel(container: AppContainer, val imageRepository: ImageReposit
DocumentFile.fromTreeUri(context, exportDirUri)?.name
}
}
fun addRecentDocument(fileUri: Uri, fileName: String, pageCount: Int) {
viewModelScope.launch {
recentDocumentsDataStore.updateData { current ->
val newDoc = RecentDocument.newBuilder()
.setFileUri(fileUri.toString())
.setFileName(fileName)
.setPageCount(pageCount)
.setCreatedAt(System.currentTimeMillis())
.build()
current.toBuilder()
.addDocuments(0, newDoc)
.also { builder ->
while (builder.documentsCount > 3) {
builder.removeDocuments(builder.documentsCount - 1)
}
}
.build()
}
}
}
}
data class ExportPreparationKey(

View File

@@ -1,296 +0,0 @@
/*
* Copyright 2025-2026 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.net.Uri
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DeleteOutline
import androidx.compose.material.icons.filled.PhotoCamera
import androidx.compose.material.icons.filled.PictureAsPdf
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import kotlinx.collections.immutable.persistentListOf
import org.fairscan.app.R
import org.fairscan.app.ui.Navigation
import org.fairscan.app.ui.components.AppOverflowMenu
import org.fairscan.app.ui.components.CameraPermissionState
import org.fairscan.app.ui.components.formatDate
import org.fairscan.app.ui.components.pageCountText
import org.fairscan.app.ui.components.rememberCameraPermissionState
import org.fairscan.app.ui.dummyNavigation
import org.fairscan.app.ui.fakeDocument
import org.fairscan.app.ui.state.DocumentUiModel
import org.fairscan.app.ui.theme.FairScanTheme
import java.io.File
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(
cameraPermission: CameraPermissionState,
currentDocument: DocumentUiModel,
navigation: Navigation,
onClearScan: () -> Unit,
recentDocuments: List<RecentDocumentUiState>,
onOpenPdf: (Uri) -> Unit,
) {
Scaffold (
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.app_name)) },
actions = { AppOverflowMenu(navigation) }
)
},
) { padding ->
Column (
modifier = Modifier
.padding(padding)
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
Spacer(Modifier.weight(1f))
ScanButton(
onClick = {
onClearScan()
navigation.toCameraScreen()
},
Modifier.align(Alignment.CenterHorizontally)
)
Spacer(Modifier.weight(1f))
if (!currentDocument.isEmpty()) {
OngoingScanBanner(
currentDocument,
onResumeScan = navigation.toDocumentScreen,
onClearScan = onClearScan,
)
} else if (recentDocuments.isNotEmpty()) {
RecentDocumentList(recentDocuments, onOpenPdf)
}
}
}
}
@Composable
fun ScanButton(onClick: () -> Unit, modifier: Modifier) {
Button(
onClick = onClick,
modifier = modifier.padding(32.dp),
elevation = ButtonDefaults.buttonElevation(defaultElevation = 6.dp)
) {
Icon(
imageVector = Icons.Default.PhotoCamera,
contentDescription = null,
modifier = Modifier.size(48.dp)
)
Spacer(Modifier.width(8.dp))
Text(
text = stringResource(R.string.scan_button),
style = MaterialTheme.typography.titleLarge
)
}
}
@Composable
fun OngoingScanBanner(
currentDocument: DocumentUiModel,
onResumeScan: () -> Unit,
onClearScan: () -> Unit
) {
Surface(
tonalElevation = 2.dp,
shadowElevation = 4.dp,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
currentDocument.thumbnail(0)?.let {
Image(
bitmap = it.asImageBitmap(),
contentDescription = null,
modifier = Modifier
.size(60.dp)
.clip(RoundedCornerShape(4.dp)),
contentScale = ContentScale.Fit
)
}
Spacer(Modifier.width(12.dp))
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = stringResource(R.string.scan_in_progress),
style = MaterialTheme.typography.bodyLarge
)
Text(
text = pageCountText(currentDocument.pageCount()),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f))
}
IconButton(
onClick = onClearScan,
modifier = Modifier.size(24.dp)
) {
Icon(
imageVector = Icons.Default.DeleteOutline,
contentDescription = stringResource(R.string.discard_scan),
tint = MaterialTheme.colorScheme.primary
)
}
Spacer(Modifier.width(12.dp))
Button(onClick = onResumeScan) {
Text(stringResource(R.string.resume))
}
}
}
}
@Composable
private fun RecentDocumentList(
recentDocuments: List<RecentDocumentUiState>,
onOpenPdf: (Uri) -> Unit
) {
Spacer(Modifier.height(8.dp))
Text(
stringResource(R.string.last_saved_pdf_files),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(start = 12.dp, bottom = 4.dp)
)
Column {
recentDocuments.forEach { doc ->
ListItem(
headlineContent = {
Text(
doc.fileName,
style = MaterialTheme.typography.bodyMedium
)
},
supportingContent = {
Text(
text = pageCountText(doc.pageCount) + "" +
formatDate(doc.saveTimestamp, LocalContext.current),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
},
leadingContent = {
Icon(
Icons.Default.PictureAsPdf,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
},
modifier = Modifier.clickable { onOpenPdf(doc.fileUri) }
)
}
}
}
@Preview
@Composable
fun HomeScreenPreviewOnFirstLaunch() {
FairScanTheme {
HomeScreen(
cameraPermission = rememberCameraPermissionState(),
currentDocument = fakeDocument(),
navigation = dummyNavigation(),
onClearScan = {},
recentDocuments = listOf(),
onOpenPdf = {},
)
}
}
@Preview
@Composable
fun HomeScreenPreviewWithCurrentDocument() {
FairScanTheme {
HomeScreen(
cameraPermission = rememberCameraPermissionState(),
currentDocument = fakeDocument(
persistentListOf("gallica.bnf.fr-bpt6k5530456s-1"),
LocalContext.current
),
navigation = dummyNavigation(),
onClearScan = {},
recentDocuments = listOf(),
onOpenPdf = {},
)
}
}
@Preview
@Composable
fun HomeScreenPreviewWithLastSavedFiles() {
FairScanTheme {
HomeScreen(
cameraPermission = rememberCameraPermissionState(),
currentDocument = fakeDocument(),
navigation = dummyNavigation(),
onClearScan = {},
recentDocuments = listOf(
RecentDocumentUiState(
File("/path/my_file.pdf").toUri(), "my_file.pdf", 1755971180000, 3),
RecentDocumentUiState(
"content:///path/scan2.pdf".toUri(), "scan2.pdf",1755000500000, 1)
),
onOpenPdf = {},
)
}
}

View File

@@ -1,24 +0,0 @@
/*
* Copyright 2025-2026 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.net.Uri
data class RecentDocumentUiState(
val fileUri: Uri,
val fileName: String,
val saveTimestamp: Long,
val pageCount: Int,
)

View File

@@ -1,75 +0,0 @@
/*
* Copyright 2025-2026 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 android.net.Uri
import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import org.fairscan.app.AppContainer
import java.io.File
class HomeViewModel(appContainer: AppContainer, appContext: Context): ViewModel() {
private val recentDocumentsDataStore = appContainer.recentDocumentsDataStore
val recentDocuments: StateFlow<List<RecentDocumentUiState>> =
recentDocumentsDataStore.data.map {
it.documentsList.mapNotNull { doc ->
var fileName = doc.fileName
var uri: Uri? = null
if (doc.fileUri.isNullOrEmpty()) {
if (!doc.filePath.isNullOrEmpty()) {
val file = File(doc.filePath)
uri = file.toUri()
fileName = file.name
}
} else {
uri = doc.fileUri.toUri()
}
if (uri != null) {
RecentDocumentUiState(
fileUri = uri,
fileName = fileName,
saveTimestamp = doc.createdAt,
pageCount = doc.pageCount,
)
} else null
}.filter { item -> uriExists(appContext, item.fileUri) }
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = emptyList(),
)
private fun uriExists(context: Context, uri: Uri): Boolean {
return if (uri.scheme == "file") {
File(uri.path.orEmpty()).exists()
} else {
try {
DocumentFile.fromSingleUri(context, uri)?.exists() == true
} catch (_: Exception) {
false
}
}
}
}

View File

@@ -33,7 +33,7 @@ data class DocumentUiModel(
return pages.lastIndex
}
fun thumbnail(index: Int): Bitmap? {
return pages[index].thumbnail?.toBitmap()
return pages.getOrNull(index)?.thumbnail?.toBitmap()
}
}

View File

@@ -1,16 +0,0 @@
syntax = "proto3";
option java_package = "org.fairscan.app";
option java_multiple_files = true;
message RecentDocument {
string file_path = 1;
int64 created_at = 2; // timestamp in ms
int32 page_count = 3;
string file_uri = 4;
string file_name = 5;
}
message RecentDocuments {
repeated RecentDocument documents = 1;
}

View File

@@ -44,7 +44,6 @@
<string name="filename">اسم الملف</string>
<string name="grant_permission">امنح الأذن</string>
<string name="import_photos">استيراد</string>
<string name="last_saved_pdf_files">ملفات PDF المحفوظة حديثًا على هذا الجهاز:</string>
<string name="libraries">المكتبات</string>
<string name="libraries_open_source">مكتبات مفتوحة المصدر</string>
<string name="license">الترخيص</string>

View File

@@ -44,7 +44,6 @@
<string name="filename">Název souboru</string>
<string name="grant_permission">Povolit přístup</string>
<string name="import_photos">Import</string>
<string name="last_saved_pdf_files">Poslední PDF uložené v tomto zařízení:</string>
<string name="libraries">Kníhovny</string>
<string name="libraries_open_source">Open-source knihovny</string>
<string name="license">Licence</string>

View File

@@ -44,7 +44,6 @@
<string name="filename">Dateiname</string>
<string name="grant_permission">Berechtigung erteilen</string>
<string name="import_photos">Importieren</string>
<string name="last_saved_pdf_files">Zuletzt auf diesem Gerät gespeicherte PDFs:</string>
<string name="libraries">Bibliotheken</string>
<string name="libraries_open_source">Open-Source-Bibliotheken</string>
<string name="license">Lizenz</string>

View File

@@ -44,7 +44,6 @@
<string name="filename">Nombre del archivo</string>
<string name="grant_permission">Conceder permiso</string>
<string name="import_photos">Importar</string>
<string name="last_saved_pdf_files">PDF recientes guardados en este dispositivo:</string>
<string name="libraries">Bibliotecas</string>
<string name="libraries_open_source">Bibliotecas de código abierto</string>
<string name="license">Licencia</string>

View File

@@ -44,7 +44,6 @@
<string name="filename">Nom de fichier</string>
<string name="grant_permission">Autoriser</string>
<string name="import_photos">Importer</string>
<string name="last_saved_pdf_files">Derniers PDF enregistrés sur lappareil :</string>
<string name="libraries">Bibliothèques</string>
<string name="libraries_open_source">Bibliothèques open source</string>
<string name="license">Licence</string>

View File

@@ -44,7 +44,6 @@
<string name="filename">Nome do ficheiro</string>
<string name="grant_permission">Conceder permiso</string>
<string name="import_photos">Importar</string>
<string name="last_saved_pdf_files">PDF recentes gardados neste dispositivo:</string>
<string name="libraries">Bibliotecas</string>
<string name="libraries_open_source">Bibliotecas de código aberto</string>
<string name="license">Licenza</string>

View File

@@ -44,7 +44,6 @@
<string name="filename">Nome file</string>
<string name="grant_permission">Concendi autorizzazione</string>
<string name="import_photos">Importa</string>
<string name="last_saved_pdf_files">PDF recenti salvati su questo dispositivo:</string>
<string name="libraries">Librerie</string>
<string name="libraries_open_source">Librerie open source</string>
<string name="license">Licenza</string>

View File

@@ -44,7 +44,6 @@
<string name="filename">Nome do arquivo</string>
<string name="grant_permission">Conceder permissão</string>
<string name="import_photos">Importar</string>
<string name="last_saved_pdf_files">PDFs recentes salvos neste dispositivo:</string>
<string name="libraries">Bibliotecas</string>
<string name="libraries_open_source">Bibliotecas de código aberto</string>
<string name="license">Licença</string>

View File

@@ -44,7 +44,6 @@
<string name="filename">Имя файла</string>
<string name="grant_permission">Предоставить разрешение</string>
<string name="import_photos">Импорт</string>
<string name="last_saved_pdf_files">Последние PDF, сохранённые на этом устройстве:</string>
<string name="libraries">Библиотеки</string>
<string name="libraries_open_source">Библиотеки с открытым исходным кодом</string>
<string name="license">Лицензия</string>

View File

@@ -44,7 +44,6 @@
<string name="filename">Dosya adı</string>
<string name="grant_permission">İzin ver</string>
<string name="import_photos">İçe aktar</string>
<string name="last_saved_pdf_files">Bu cihaza kaydedilen son PDF\'ler:</string>
<string name="libraries">Kütüphaneler</string>
<string name="libraries_open_source">ık kaynaklı kütüphaneler</string>
<string name="license">Lisans</string>

View File

@@ -44,7 +44,6 @@
<string name="filename">檔案名稱</string>
<string name="grant_permission">授予權限</string>
<string name="import_photos">匯入</string>
<string name="last_saved_pdf_files">此裝置上最近儲存的 PDF</string>
<string name="libraries">函式庫</string>
<string name="libraries_open_source">開放原始碼函式庫</string>
<string name="license">許可證</string>

View File

@@ -44,7 +44,6 @@
<string name="filename">文件名字</string>
<string name="grant_permission">授予权限</string>
<string name="import_photos">导入</string>
<string name="last_saved_pdf_files">最近保存在此设备上的 PDF</string>
<string name="libraries"></string>
<string name="libraries_open_source">开源库</string>
<string name="license">许可证</string>

View File

@@ -48,7 +48,6 @@
<string name="filename">Filename</string>
<string name="grant_permission">Grant permission</string>
<string name="import_photos">Import</string>
<string name="last_saved_pdf_files">Recent PDFs saved on this device:</string>
<string name="libraries">Libraries</string>
<string name="libraries_open_source">Open-source libraries</string>
<string name="license">License</string>

View File

@@ -15,11 +15,9 @@
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
import org.fairscan.app.ui.Screen.Main.Home
import org.fairscan.app.ui.Screen.Overlay.About
import org.fairscan.app.ui.Screen.Overlay.Libraries
import org.junit.Test
@@ -28,36 +26,33 @@ class NavigationTest {
@Test
fun empty_ScreenStack() {
val empty = NavigationState.initial(LaunchMode.NORMAL)
assertThat(empty.current).isEqualTo(Home)
val empty = NavigationState.initial()
assertThat(empty.current).isEqualTo(Camera)
assertThat(empty.navigateBack()).isEqualTo(empty)
}
@Test
fun navigate_between_fixed_screens() {
val atHome = NavigationState.initial(LaunchMode.NORMAL)
val atCamera = atHome.navigateTo(Camera)
val atDocument = atHome.navigateTo(Document())
val atExport = atHome.navigateTo(Export)
val atCamera = NavigationState.initial()
val atDocument = atCamera.navigateTo(Document())
val atExport = atCamera.navigateTo(Export)
assertThat(atHome.current).isEqualTo(Home)
assertThat(atCamera.current).isEqualTo(Camera)
assertThat(atDocument.current).isEqualTo(Document())
assertThat(atExport.current).isEqualTo(Export)
assertThat(atCamera.navigateTo(Document())).isEqualTo(atDocument)
assertThat(atDocument.navigateTo(Home)).isEqualTo(atHome)
assertThat(atDocument.navigateTo(Export)).isEqualTo(atExport)
assertThat(atDocument.navigateTo(Camera)).isEqualTo(atCamera)
assertThat(atHome.navigateBack()).isEqualTo(atHome)
assertThat(atCamera.navigateBack()).isEqualTo(atHome)
assertThat(atCamera.navigateBack()).isEqualTo(atCamera)
assertThat(atDocument.navigateBack()).isEqualTo(atCamera)
assertThat(atExport.navigateBack()).isEqualTo(atCamera)
}
@Test
fun navigate_to_secondary_screens() {
val atHome = NavigationState.initial(LaunchMode.NORMAL)
val atHome = NavigationState.initial()
val atCamera = atHome.navigateTo(Camera)
val atAboutAfterHome = atHome.navigateTo(About)
@@ -75,7 +70,7 @@ class NavigationTest {
@Test
fun external_call() {
val initial = NavigationState.initial(LaunchMode.EXTERNAL_SCAN_TO_PDF)
val initial = NavigationState.initial()
assertThat(initial.current).isEqualTo(Camera)
assertThat(initial.navigateBack().current).isEqualTo(Camera)
}

View File

@@ -19,8 +19,6 @@ assertj = "3.27.7"
pdfbox = "2.0.27.0"
zoomable = "2.11.1"
aboutLibraries = "13.2.1"
protobuf = "0.9.6"
protobufJavaLite = "4.34.1"
kotlinSerialization = "1.10.0"
reorderable = "3.0.0"
jetbrainsKotlinJvm = "2.3.10"
@@ -49,10 +47,8 @@ androidx-camera-core = { group = "androidx.camera", name = "camera-core", versio
androidx-camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "camerax" }
androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "camerax" }
androidx-camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "camerax" }
androidx-datastore = { group = "androidx.datastore", name = "datastore" , version.ref = "datastore" }
androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences" , version.ref = "datastore" }
androidx-documentfile = { group = "androidx.documentfile", name = "documentfile" , version.ref = "documentfile" }
protobuf-javalite = { group = "com.google.protobuf", name="protobuf-javalite", version.ref = "protobufJavaLite"}
litert = { group = "com.google.ai.edge.litert", name = "litert", version.ref = "litert" }
litert-support = { group = "com.google.ai.edge.litert", name = "litert-support", version.ref = "litert" }
litert-metadata = { group = "com.google.ai.edge.litert", name = "litert-metadata", version.ref = "litert" }
@@ -75,5 +71,4 @@ kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "ko
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
license = { id = "com.github.hierynomus.license", version.ref = "license" }
aboutLibrariesAndroid = { id = "com.mikepenz.aboutlibraries.plugin.android", version.ref = "aboutLibraries" }
protobuf = { id = "com.google.protobuf", version.ref = "protobuf" }
jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "jetbrainsKotlinJvm" }