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.android)
alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.compose)
alias(libs.plugins.aboutLibrariesAndroid) alias(libs.plugins.aboutLibrariesAndroid)
alias(libs.plugins.protobuf)
alias(libs.plugins.kotlin.serialization) alias(libs.plugins.kotlin.serialization)
} }
@@ -122,10 +121,8 @@ dependencies {
implementation(libs.androidx.camera.camera2) implementation(libs.androidx.camera.camera2)
implementation(libs.androidx.camera.lifecycle) implementation(libs.androidx.camera.lifecycle)
implementation(libs.androidx.camera.view) implementation(libs.androidx.camera.view)
implementation(libs.androidx.datastore)
implementation(libs.androidx.datastore.preferences) implementation(libs.androidx.datastore.preferences)
implementation(libs.androidx.documentfile) implementation(libs.androidx.documentfile)
implementation(libs.protobuf.javalite)
implementation(libs.litert) implementation(libs.litert)
implementation(libs.litert.support) implementation(libs.litert.support)
implementation(libs.litert.metadata) 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 // See https://developer.android.com/build/configure-apk-splits
androidComponents { androidComponents {
onVariants { variant -> onVariants { variant ->

View File

@@ -23,12 +23,10 @@ import androidx.lifecycle.viewmodel.CreationExtras
import org.fairscan.app.data.FileLogger import org.fairscan.app.data.FileLogger
import org.fairscan.app.data.FileManager import org.fairscan.app.data.FileManager
import org.fairscan.app.data.LogRepository import org.fairscan.app.data.LogRepository
import org.fairscan.app.data.recentDocumentsDataStore
import org.fairscan.app.domain.ImageSegmentationService import org.fairscan.app.domain.ImageSegmentationService
import org.fairscan.app.platform.AndroidImageLoader import org.fairscan.app.platform.AndroidImageLoader
import org.fairscan.app.platform.AndroidPdfWriter import org.fairscan.app.platform.AndroidPdfWriter
import org.fairscan.app.ui.screens.camera.CameraViewModel 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.SettingsRepository
import org.fairscan.app.ui.screens.settings.SettingsViewModel import org.fairscan.app.ui.screens.settings.SettingsViewModel
import java.io.File import java.io.File
@@ -57,7 +55,6 @@ class AppContainer(context: Context) {
val logger = FileLogger(logRepository) val logger = FileLogger(logRepository)
val imageSegmentationService = ImageSegmentationService(context, logger) val imageSegmentationService = ImageSegmentationService(context, logger)
val imageLoader = AndroidImageLoader(context.contentResolver) val imageLoader = AndroidImageLoader(context.contentResolver)
val recentDocumentsDataStore = context.recentDocumentsDataStore
val settingsRepository = SettingsRepository(context) val settingsRepository = SettingsRepository(context)
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
@@ -69,7 +66,6 @@ class AppContainer(context: Context) {
} }
} }
val homeViewModelFactory = viewModelFactory { HomeViewModel(it, context) }
val cameraViewModelFactory = viewModelFactory { CameraViewModel(it) } val cameraViewModelFactory = viewModelFactory { CameraViewModel(it) }
val settingsViewModelFactory = viewModelFactory { SettingsViewModel(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.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.screens.document.DocumentScreen
import org.fairscan.app.ui.screens.LibrariesScreen import org.fairscan.app.ui.screens.LibrariesScreen
import org.fairscan.app.ui.screens.about.AboutEvent import org.fairscan.app.ui.screens.about.AboutEvent
import org.fairscan.app.ui.screens.about.AboutScreen 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.CameraEvent
import org.fairscan.app.ui.screens.camera.CameraScreen 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.document.DocumentScreen
import org.fairscan.app.ui.screens.export.ExportActions import org.fairscan.app.ui.screens.export.ExportActions
import org.fairscan.app.ui.screens.export.ExportEvent import org.fairscan.app.ui.screens.export.ExportEvent
import org.fairscan.app.ui.screens.export.ExportResult import org.fairscan.app.ui.screens.export.ExportResult
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.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.SettingsScreen
import org.fairscan.app.ui.screens.settings.SettingsViewModel import org.fairscan.app.ui.screens.settings.SettingsViewModel
import org.fairscan.app.ui.theme.FairScanTheme import org.fairscan.app.ui.theme.FairScanTheme
@@ -97,7 +94,7 @@ class MainActivity : ComponentActivity() {
val imageRepository = sessionViewModel.imageRepository val imageRepository = sessionViewModel.imageRepository
val viewModel: MainViewModel by viewModels { val viewModel: MainViewModel by viewModels {
appContainer.viewModelFactory { appContainer.viewModelFactory {
MainViewModel(imageRepository, launchMode) MainViewModel(imageRepository)
} }
} }
val exportViewModel: ExportViewModel by viewModels { val exportViewModel: ExportViewModel by viewModels {
@@ -110,7 +107,6 @@ class MainActivity : ComponentActivity() {
AboutViewModel(appContainer, imageRepository) AboutViewModel(appContainer, imageRepository)
} }
} }
val homeViewModel: HomeViewModel by viewModels { appContainer.homeViewModelFactory }
val cameraViewModel: CameraViewModel by viewModels { appContainer.cameraViewModelFactory } val cameraViewModel: CameraViewModel by viewModels { appContainer.cameraViewModelFactory }
val settingsViewModel: SettingsViewModel val settingsViewModel: SettingsViewModel
@@ -157,16 +153,8 @@ class MainActivity : ComponentActivity() {
} }
when (currentScreen) { when (currentScreen) {
is Screen.Main.Home -> { null -> {
val recentDocs by homeViewModel.recentDocuments.collectAsStateWithLifecycle() // waiting to load pages to get an initial screen
HomeScreen(
cameraPermission = cameraPermission,
currentDocument = document,
navigation = navigation,
onClearScan = { viewModel.startNewDocument() },
recentDocuments = recentDocs,
onOpenPdf = { fileUri -> openUri(fileUri, ExportFormat.PDF.mimeType, logger) }
)
} }
is Screen.Main.Camera -> { is Screen.Main.Camera -> {
val pickMultiple = rememberLauncherForActivityResult( val pickMultiple = rememberLauncherForActivityResult(
@@ -216,7 +204,7 @@ class MainActivity : ComponentActivity() {
onCloseScan = { onCloseScan = {
exportViewModel.resetFilename() exportViewModel.resetFilename()
viewModel.startNewDocument() 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( private fun navigation(viewModel: MainViewModel, launchMode: LaunchMode): Navigation = Navigation(
toHomeScreen = { viewModel.navigateTo(Screen.Main.Home) },
toCameraScreen = { viewModel.navigateTo(Screen.Main.Camera) }, toCameraScreen = { viewModel.navigateTo(Screen.Main.Camera) },
toDocumentScreen = { viewModel.navigateTo(Screen.Main.Document()) }, toDocumentScreen = { viewModel.navigateTo(Screen.Main.Document()) },
toExportScreen = { viewModel.navigateTo(Screen.Main.Export) }, toExportScreen = { viewModel.navigateTo(Screen.Main.Export) },

View File

@@ -45,17 +45,27 @@ import org.fairscan.imageprocessing.ColorMode
import kotlin.math.min import kotlin.math.min
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class MainViewModel(val imageRepository: ImageRepository, launchMode: LaunchMode): ViewModel() { class MainViewModel(val imageRepository: ImageRepository): ViewModel() {
private val _navigationState = MutableStateFlow(NavigationState.initial(launchMode)) private val _navigationState = MutableStateFlow<NavigationState?>(null)
val currentScreen: StateFlow<Screen> = _navigationState.map { it.current } val currentScreen: StateFlow<Screen?> = _navigationState.map { it?.current }
.stateIn(viewModelScope, SharingStarted.Eagerly, _navigationState.value.current) .stateIn(viewModelScope, SharingStarted.Eagerly, null)
private val _pages = MutableStateFlow<List<ScanPage>>(emptyList()) private val _pages = MutableStateFlow<List<ScanPage>>(emptyList())
init { init {
viewModelScope.launch { 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) _currentPageIndex.value = min(_pages.value.size - 1, destination.initialPage)
} }
_navigationState.update { it.navigateTo(destination) } _navigationState.update { it?.navigateTo(destination) }
} }
fun navigateBack() { fun navigateBack() {
_navigationState.update { stack -> stack.navigateBack() } _navigationState.update { stack -> stack?.navigateBack() }
} }
fun rotateCurrentPage(clockwise: Boolean) { 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 package org.fairscan.app.ui
import org.fairscan.app.LaunchMode
sealed class Screen { sealed class Screen {
sealed class Main : Screen() { sealed class Main : Screen() {
object Home : Main()
object Camera : Main() object Camera : Main()
data class Document(val initialPage: Int = 0) : Main() data class Document(val initialPage: Int = 0) : Main()
object Export : Main() object Export : Main()
@@ -31,7 +28,6 @@ sealed class Screen {
} }
data class Navigation( data class Navigation(
val toHomeScreen: () -> Unit,
val toCameraScreen: () -> Unit, val toCameraScreen: () -> Unit,
val toDocumentScreen: () -> Unit, val toDocumentScreen: () -> Unit,
val toExportScreen: () -> Unit, val toExportScreen: () -> Unit,
@@ -41,18 +37,12 @@ data class Navigation(
val back: () -> 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 @ConsistentCopyVisibility
data class NavigationState private constructor(val stack: List<Screen>, val root: Screen.Main) { data class NavigationState private constructor(val stack: List<Screen>, val root: Screen.Main) {
companion object { companion object {
fun initial(mode: LaunchMode): NavigationState { fun initial(): NavigationState {
val root = startScreenFor(mode) val root = Screen.Main.Camera
return NavigationState(listOf(root), root) return NavigationState(listOf(root), root)
} }
} }
@@ -70,8 +60,7 @@ data class NavigationState private constructor(val stack: List<Screen>, val root
fun navigateBack(): NavigationState { fun navigateBack(): NavigationState {
return when (current) { return when (current) {
root -> this // Back handled by system root -> this // Back handled by system
is Screen.Main.Home -> this // Back handled by system is Screen.Main.Camera -> 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)) is Screen.Main.Document -> copy(stack = listOf(Screen.Main.Camera))
is Screen.Main.Export -> copy(stack = listOf(Screen.Main.Camera)) is Screen.Main.Export -> copy(stack = listOf(Screen.Main.Camera))
is Screen.Overlay -> copy(stack = stack.dropLast(1)) is Screen.Overlay -> copy(stack = stack.dropLast(1))

View File

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

View File

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

View File

@@ -42,7 +42,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.fairscan.app.AppContainer import org.fairscan.app.AppContainer
import org.fairscan.app.R import org.fairscan.app.R
import org.fairscan.app.RecentDocument
import org.fairscan.app.data.FileManager import org.fairscan.app.data.FileManager
import org.fairscan.app.data.ImageRepository import org.fairscan.app.data.ImageRepository
import org.fairscan.app.domain.ExportQuality import org.fairscan.app.domain.ExportQuality
@@ -69,7 +68,6 @@ class ExportViewModel(container: AppContainer, val imageRepository: ImageReposit
private val preparationDir = container.preparationDir private val preparationDir = container.preparationDir
private val fileManager = container.fileManager private val fileManager = container.fileManager
private val settingsRepository = container.settingsRepository private val settingsRepository = container.settingsRepository
private val recentDocumentsDataStore = container.recentDocumentsDataStore
private val logger = container.logger private val logger = container.logger
private val _events = MutableSharedFlow<ExportEvent>() private val _events = MutableSharedFlow<ExportEvent>()
@@ -310,12 +308,6 @@ class ExportViewModel(container: AppContainer, val imageRepository: ImageReposit
val bundle = SavedBundle(savedItems, saveDir) val bundle = SavedBundle(savedItems, saveDir)
_uiState.update { it.copy(savedBundle = bundle) } _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) } filesForMediaScan.forEach { f -> mediaScan(context, f, exportFormat.mimeType) }
} }
@@ -395,27 +387,6 @@ class ExportViewModel(container: AppContainer, val imageRepository: ImageReposit
DocumentFile.fromTreeUri(context, exportDirUri)?.name 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( 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 return pages.lastIndex
} }
fun thumbnail(index: Int): Bitmap? { 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="filename">اسم الملف</string>
<string name="grant_permission">امنح الأذن</string> <string name="grant_permission">امنح الأذن</string>
<string name="import_photos">استيراد</string> <string name="import_photos">استيراد</string>
<string name="last_saved_pdf_files">ملفات PDF المحفوظة حديثًا على هذا الجهاز:</string>
<string name="libraries">المكتبات</string> <string name="libraries">المكتبات</string>
<string name="libraries_open_source">مكتبات مفتوحة المصدر</string> <string name="libraries_open_source">مكتبات مفتوحة المصدر</string>
<string name="license">الترخيص</string> <string name="license">الترخيص</string>

View File

@@ -44,7 +44,6 @@
<string name="filename">Název souboru</string> <string name="filename">Název souboru</string>
<string name="grant_permission">Povolit přístup</string> <string name="grant_permission">Povolit přístup</string>
<string name="import_photos">Import</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">Kníhovny</string>
<string name="libraries_open_source">Open-source knihovny</string> <string name="libraries_open_source">Open-source knihovny</string>
<string name="license">Licence</string> <string name="license">Licence</string>

View File

@@ -44,7 +44,6 @@
<string name="filename">Dateiname</string> <string name="filename">Dateiname</string>
<string name="grant_permission">Berechtigung erteilen</string> <string name="grant_permission">Berechtigung erteilen</string>
<string name="import_photos">Importieren</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">Bibliotheken</string>
<string name="libraries_open_source">Open-Source-Bibliotheken</string> <string name="libraries_open_source">Open-Source-Bibliotheken</string>
<string name="license">Lizenz</string> <string name="license">Lizenz</string>

View File

@@ -44,7 +44,6 @@
<string name="filename">Nombre del archivo</string> <string name="filename">Nombre del archivo</string>
<string name="grant_permission">Conceder permiso</string> <string name="grant_permission">Conceder permiso</string>
<string name="import_photos">Importar</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">Bibliotecas</string>
<string name="libraries_open_source">Bibliotecas de código abierto</string> <string name="libraries_open_source">Bibliotecas de código abierto</string>
<string name="license">Licencia</string> <string name="license">Licencia</string>

View File

@@ -44,7 +44,6 @@
<string name="filename">Nom de fichier</string> <string name="filename">Nom de fichier</string>
<string name="grant_permission">Autoriser</string> <string name="grant_permission">Autoriser</string>
<string name="import_photos">Importer</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">Bibliothèques</string>
<string name="libraries_open_source">Bibliothèques open source</string> <string name="libraries_open_source">Bibliothèques open source</string>
<string name="license">Licence</string> <string name="license">Licence</string>

View File

@@ -44,7 +44,6 @@
<string name="filename">Nome do ficheiro</string> <string name="filename">Nome do ficheiro</string>
<string name="grant_permission">Conceder permiso</string> <string name="grant_permission">Conceder permiso</string>
<string name="import_photos">Importar</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">Bibliotecas</string>
<string name="libraries_open_source">Bibliotecas de código aberto</string> <string name="libraries_open_source">Bibliotecas de código aberto</string>
<string name="license">Licenza</string> <string name="license">Licenza</string>

View File

@@ -44,7 +44,6 @@
<string name="filename">Nome file</string> <string name="filename">Nome file</string>
<string name="grant_permission">Concendi autorizzazione</string> <string name="grant_permission">Concendi autorizzazione</string>
<string name="import_photos">Importa</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">Librerie</string>
<string name="libraries_open_source">Librerie open source</string> <string name="libraries_open_source">Librerie open source</string>
<string name="license">Licenza</string> <string name="license">Licenza</string>

View File

@@ -44,7 +44,6 @@
<string name="filename">Nome do arquivo</string> <string name="filename">Nome do arquivo</string>
<string name="grant_permission">Conceder permissão</string> <string name="grant_permission">Conceder permissão</string>
<string name="import_photos">Importar</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">Bibliotecas</string>
<string name="libraries_open_source">Bibliotecas de código aberto</string> <string name="libraries_open_source">Bibliotecas de código aberto</string>
<string name="license">Licença</string> <string name="license">Licença</string>

View File

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

View File

@@ -44,7 +44,6 @@
<string name="filename">Dosya adı</string> <string name="filename">Dosya adı</string>
<string name="grant_permission">İzin ver</string> <string name="grant_permission">İzin ver</string>
<string name="import_photos">İçe aktar</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">Kütüphaneler</string>
<string name="libraries_open_source">ık kaynaklı kütüphaneler</string> <string name="libraries_open_source">ık kaynaklı kütüphaneler</string>
<string name="license">Lisans</string> <string name="license">Lisans</string>

View File

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

View File

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

View File

@@ -48,7 +48,6 @@
<string name="filename">Filename</string> <string name="filename">Filename</string>
<string name="grant_permission">Grant permission</string> <string name="grant_permission">Grant permission</string>
<string name="import_photos">Import</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">Libraries</string>
<string name="libraries_open_source">Open-source libraries</string> <string name="libraries_open_source">Open-source libraries</string>
<string name="license">License</string> <string name="license">License</string>

View File

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

View File

@@ -19,8 +19,6 @@ assertj = "3.27.7"
pdfbox = "2.0.27.0" pdfbox = "2.0.27.0"
zoomable = "2.11.1" zoomable = "2.11.1"
aboutLibraries = "13.2.1" aboutLibraries = "13.2.1"
protobuf = "0.9.6"
protobufJavaLite = "4.34.1"
kotlinSerialization = "1.10.0" kotlinSerialization = "1.10.0"
reorderable = "3.0.0" reorderable = "3.0.0"
jetbrainsKotlinJvm = "2.3.10" 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-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "camerax" }
androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", 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-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-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences" , version.ref = "datastore" }
androidx-documentfile = { group = "androidx.documentfile", name = "documentfile" , version.ref = "documentfile" } 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 = { 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-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" } 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" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
license = { id = "com.github.hierynomus.license", version.ref = "license" } license = { id = "com.github.hierynomus.license", version.ref = "license" }
aboutLibrariesAndroid = { id = "com.mikepenz.aboutlibraries.plugin.android", version.ref = "aboutLibraries" } 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" } jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "jetbrainsKotlinJvm" }