From 3f10a1cd55f143064b4a09f302db32c55df7c82b Mon Sep 17 00:00:00 2001 From: Pierre-Yves Nicolas <6371790+pynicolas@users.noreply.github.com> Date: Wed, 26 Nov 2025 17:02:47 +0100 Subject: [PATCH] AboutScreen: add button to copy logs to clipboard --- .../main/java/org/fairscan/app/FairScanApp.kt | 5 +- .../java/org/fairscan/app/MainActivity.kt | 28 ++++++++- .../org/fairscan/app/data/LogRepository.kt | 2 +- .../app/ui/screens/{ => about}/AboutScreen.kt | 40 +++++++++++-- .../app/ui/screens/about/AboutViewModel.kt | 58 +++++++++++++++++++ app/src/main/res/values-cs/strings.xml | 3 +- app/src/main/res/values-de/strings.xml | 2 + app/src/main/res/values-es/strings.xml | 2 + app/src/main/res/values-fr/strings.xml | 2 + app/src/main/res/values-it/strings.xml | 2 + app/src/main/res/values-pt-rBR/strings.xml | 2 + app/src/main/res/values-ru/strings.xml | 2 + app/src/main/res/values-zh/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + .../fairscan/app/data/LogRepositoryTest.kt | 9 +++ 15 files changed, 150 insertions(+), 11 deletions(-) rename app/src/main/java/org/fairscan/app/ui/screens/{ => about}/AboutScreen.kt (90%) create mode 100644 app/src/main/java/org/fairscan/app/ui/screens/about/AboutViewModel.kt 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