diff --git a/app/src/main/java/org/fairscan/app/FairScanApp.kt b/app/src/main/java/org/fairscan/app/FairScanApp.kt
index a34e850..9de959e 100644
--- a/app/src/main/java/org/fairscan/app/FairScanApp.kt
+++ b/app/src/main/java/org/fairscan/app/FairScanApp.kt
@@ -28,6 +28,7 @@ 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
@@ -53,7 +54,8 @@ class AppContainer(context: Context) {
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
AndroidPdfWriter()
)
- val logger = FileLogger(LogRepository(File(context.filesDir, "logs.txt")))
+ val logRepository = LogRepository(File(context.filesDir, "logs.txt"))
+ val logger = FileLogger(logRepository)
val imageSegmentationService = ImageSegmentationService(context, logger)
val recentDocumentsDataStore = context.recentDocumentsDataStore
@@ -70,4 +72,5 @@ class AppContainer(context: Context) {
val homeViewModelFactory = viewModelFactory { HomeViewModel(it) }
val cameraViewModelFactory = viewModelFactory { CameraViewModel(it) }
val exportViewModelFactory = viewModelFactory { ExportViewModel(it) }
+ val aboutViewModelFactory = viewModelFactory { AboutViewModel(it) }
}
diff --git a/app/src/main/java/org/fairscan/app/MainActivity.kt b/app/src/main/java/org/fairscan/app/MainActivity.kt
index 8236137..0d781c4 100644
--- a/app/src/main/java/org/fairscan/app/MainActivity.kt
+++ b/app/src/main/java/org/fairscan/app/MainActivity.kt
@@ -16,6 +16,7 @@ package org.fairscan.app
import android.Manifest
import android.content.ActivityNotFoundException
+import android.content.ClipData
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.PackageManager.PERMISSION_GRANTED
@@ -34,7 +35,10 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
+import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.toClipEntry
+import androidx.compose.ui.res.stringResource
import androidx.core.content.ContextCompat.checkSelfPermission
import androidx.core.content.FileProvider
import androidx.core.net.toFile
@@ -47,9 +51,11 @@ import org.fairscan.app.data.GeneratedPdf
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.AboutScreen
import org.fairscan.app.ui.screens.DocumentScreen
import org.fairscan.app.ui.screens.LibrariesScreen
+import org.fairscan.app.ui.screens.about.AboutEvent
+import org.fairscan.app.ui.screens.about.AboutScreen
+import org.fairscan.app.ui.screens.about.AboutViewModel
import org.fairscan.app.ui.screens.camera.CameraEvent
import org.fairscan.app.ui.screens.camera.CameraScreen
import org.fairscan.app.ui.screens.camera.CameraViewModel
@@ -74,6 +80,7 @@ class MainActivity : ComponentActivity() {
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 }
lifecycleScope.launch(Dispatchers.IO) {
exportViewModel.cleanUpOldPdfs(1000 * 3600)
}
@@ -118,6 +125,20 @@ class MainActivity : ComponentActivity() {
}
}
}
+ val clipboard = LocalClipboard.current
+ val msgCopiedLogs = stringResource(R.string.copied_logs)
+ LaunchedEffect(aboutViewModel.events) {
+ aboutViewModel.events.collect { event ->
+ when (event) {
+ is AboutEvent.CopyLogs -> {
+ clipboard.setClipEntry(
+ ClipData.newPlainText("FairScan logs", event.logs).toClipEntry()
+ )
+ Toast.makeText(context, msgCopiedLogs, Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+ }
FairScanTheme {
val navigation = Navigation(
@@ -180,7 +201,10 @@ class MainActivity : ComponentActivity() {
)
}
is Screen.Overlay.About -> {
- AboutScreen(onBack = navigation.back, onViewLibraries = navigation.toLibrariesScreen)
+ AboutScreen(
+ onBack = navigation.back,
+ onCopyLogs = { aboutViewModel.onCopyLogsClicked() },
+ onViewLibraries = navigation.toLibrariesScreen)
}
is Screen.Overlay.Libraries -> {
LibrariesScreen(onBack = navigation.back)
diff --git a/app/src/main/java/org/fairscan/app/data/LogRepository.kt b/app/src/main/java/org/fairscan/app/data/LogRepository.kt
index 440a40a..630379e 100644
--- a/app/src/main/java/org/fairscan/app/data/LogRepository.kt
+++ b/app/src/main/java/org/fairscan/app/data/LogRepository.kt
@@ -18,7 +18,7 @@ import java.io.File
class LogRepository(private val file: File) {
- fun getLogs(): String = file.readText()
+ fun getLogs(): String = if (file.exists()) file.readText() else ""
fun log(tag: String, message: String, throwable: Throwable) {
val line = buildString {
diff --git a/app/src/main/java/org/fairscan/app/ui/screens/AboutScreen.kt b/app/src/main/java/org/fairscan/app/ui/screens/about/AboutScreen.kt
similarity index 90%
rename from app/src/main/java/org/fairscan/app/ui/screens/AboutScreen.kt
rename to app/src/main/java/org/fairscan/app/ui/screens/about/AboutScreen.kt
index 01e6286..2ba7204 100644
--- a/app/src/main/java/org/fairscan/app/ui/screens/AboutScreen.kt
+++ b/app/src/main/java/org/fairscan/app/ui/screens/about/AboutScreen.kt
@@ -12,7 +12,7 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see .
*/
-package org.fairscan.app.ui.screens
+package org.fairscan.app.ui.screens.about
import android.content.Intent
import androidx.activity.compose.BackHandler
@@ -32,6 +32,7 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material.icons.filled.Email
import androidx.compose.material.icons.filled.Language
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -69,7 +70,11 @@ import org.fairscan.app.ui.theme.FairScanTheme
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-fun AboutScreen(onBack: () -> Unit, onViewLibraries: () -> Unit) {
+fun AboutScreen(
+ onBack: () -> Unit,
+ onCopyLogs: () -> Unit,
+ onViewLibraries: () -> Unit,
+) {
val showLicenseDialog = rememberSaveable { mutableStateOf(false) }
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
BackHandler { onBack() }
@@ -81,7 +86,11 @@ fun AboutScreen(onBack: () -> Unit, onViewLibraries: () -> Unit) {
)
}
) { paddingValues ->
- AboutContent(modifier = Modifier.padding(paddingValues), showLicenseDialog, onViewLibraries)
+ AboutContent(
+ modifier = Modifier.padding(paddingValues),
+ onCopyLogs,
+ showLicenseDialog,
+ onViewLibraries)
}
if (showLicenseDialog.value) {
LicenseBottomSheet(sheetState, onDismiss = { showLicenseDialog.value = false })
@@ -91,6 +100,7 @@ fun AboutScreen(onBack: () -> Unit, onViewLibraries: () -> Unit) {
@Composable
fun AboutContent(
modifier: Modifier = Modifier,
+ onCopyLogs: () -> Unit,
showLicenseDialog: MutableState,
onViewLibraries: () -> Unit,
) {
@@ -142,12 +152,12 @@ fun AboutContent(
context.startActivity(intent)
}
)
+ CopyLogsButton (onClick = onCopyLogs)
}
Section(title = stringResource(R.string.license)) {
Text(
stringResource(R.string.licensed_under),
-
)
Text(
text = stringResource(R.string.view_the_full_license),
@@ -156,7 +166,6 @@ fun AboutContent(
)
}
-
Section(title = stringResource(R.string.libraries)) {
Text(
stringResource(R.string.libraries_intro) +
@@ -259,10 +268,29 @@ fun LicenseBottomSheet(
}
}
+@Composable
+fun CopyLogsButton(onClick: () -> Unit) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable { onClick() }
+ .padding(vertical = 8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ imageVector = Icons.Default.ContentCopy,
+ contentDescription = null
+ )
+ Spacer(modifier = Modifier.width(16.dp))
+ Text(stringResource(R.string.copy_logs))
+ }
+}
+
+
@Preview
@Composable
fun AboutScreenPreview() {
FairScanTheme {
- AboutScreen(onBack = {}, onViewLibraries = {})
+ AboutScreen(onBack = {}, onCopyLogs = {}, onViewLibraries = {})
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/fairscan/app/ui/screens/about/AboutViewModel.kt b/app/src/main/java/org/fairscan/app/ui/screens/about/AboutViewModel.kt
new file mode 100644
index 0000000..7135cc6
--- /dev/null
+++ b/app/src/main/java/org/fairscan/app/ui/screens/about/AboutViewModel.kt
@@ -0,0 +1,58 @@
+/*
+ * 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 .
+ */
+package org.fairscan.app.ui.screens.about
+
+import android.os.Build
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.launch
+import org.fairscan.app.AppContainer
+import org.fairscan.app.BuildConfig
+import java.time.LocalDateTime
+
+sealed interface AboutEvent {
+ data class CopyLogs(val logs: String) : AboutEvent
+}
+
+class AboutViewModel(container: AppContainer): ViewModel() {
+
+ private val logRepository = container.logRepository
+
+ private val _events = MutableSharedFlow()
+ val events = _events.asSharedFlow()
+
+ fun onCopyLogsClicked() {
+ viewModelScope.launch {
+ val logs = buildFullLogs()
+ _events.emit(AboutEvent.CopyLogs(logs))
+ }
+ }
+
+ private fun buildFullLogs(): String {
+ val header = buildString {
+ appendLine("FairScan diagnostics report")
+ appendLine("App version: ${BuildConfig.VERSION_NAME}")
+ appendLine("Android version: ${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT})")
+ appendLine("Generated: ${LocalDateTime.now()}")
+ appendLine()
+ appendLine("-- Application logs --")
+ appendLine()
+ }
+ return header + logRepository.getLogs()
+ }
+
+}
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
index 40b1907..fae4b2f 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -1,7 +1,6 @@
O aplikaci
Přidat stránku
- FairScan
Jednoduchá a respektující aplikace pro skenování vašich dokumentů
Zpět
Byl odepřen přístup k fotoaparátu
@@ -9,6 +8,8 @@
Zrušit
Smazat text
Kontakt
+ Protokoly zkopírovány do schránky
+ Kopírovat protokoly
Vytváření PDF…
Smazat stránku
Chcete smazat tuto stránku?
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index a03ce65..468048a 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -8,6 +8,8 @@
Abbrechen
Text löschen
Kontakt
+ Logs in die Zwischenablage kopiert
+ Logs kopieren
PDF wird erstellt…
Seite löschen
Möchten Sie diese Seite löschen?
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index f650e92..a8441d7 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -8,6 +8,8 @@
Cancelar
Borrar texto
Contacto
+ Registros copiados al portapapeles
+ Copiar registros
Creando PDF…
Eliminar página
¿Quieres eliminar esta página?
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 255f026..ced34a7 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -8,6 +8,8 @@
Annuler
Contact
Effacer le text
+ Logs copiés dans le presse-papiers
+ Copier les logs
Création du PDF…
Supprimer la page
Voulez-vous supprimer cette page ?
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 90da1de..b7a30c5 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -8,6 +8,8 @@
Annulla
Svuota testo
Contatti
+ Log copiati negli appunti
+ Copia log
Creazione PDF…
Elimina pagina
Vuoi eliminare questa pagina?
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 5e8a816..dcc36cb 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -8,6 +8,8 @@
Cancelar
Limpar texto
Contato
+ Registros copiados para a área de transferência
+ Copiar registros
Criando PDF…
Excluir página
Deseja excluir esta página?
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 49a9daf..659209e 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -8,6 +8,8 @@
Отмена
Стереть текст
Контакты
+ Журналы скопированы в буфер обмена
+ Копировать журналы
Создание PDF…
Удалить страницу
Вы желаете удалить эту страницу?
diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml
index 53f501e..715d2ac 100644
--- a/app/src/main/res/values-zh/strings.xml
+++ b/app/src/main/res/values-zh/strings.xml
@@ -8,6 +8,8 @@
取消
清除文字
联系人
+ 日志已复制到剪贴板
+ 复制日志
正在创建 PDF…
删除页面
是否要删除此页面?
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index bc8e481..e201c13 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -9,6 +9,8 @@
Cancel
Clear text
Contact
+ Logs copied to clipboard
+ Copy logs
Creating PDF…
Delete page
Do you want to delete this page?
diff --git a/app/src/test/java/org/fairscan/app/data/LogRepositoryTest.kt b/app/src/test/java/org/fairscan/app/data/LogRepositoryTest.kt
index 6b20ac7..ee248aa 100644
--- a/app/src/test/java/org/fairscan/app/data/LogRepositoryTest.kt
+++ b/app/src/test/java/org/fairscan/app/data/LogRepositoryTest.kt
@@ -18,6 +18,7 @@ import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
+import java.io.File
class LogRepositoryTest {
@@ -33,4 +34,12 @@ class LogRepositoryTest {
assertThat(repo.getLogs()).contains("my exception")
print(repo.getLogs())
}
+
+ @Test
+ fun get_logs_with_file_not_yet_created() {
+ val file = File(folder.newFolder(), "log.txt")
+ val repo = LogRepository(file)
+ assertThat(file).doesNotExist()
+ assertThat(repo.getLogs()).isEmpty()
+ }
}
\ No newline at end of file