AboutScreen: add button to copy logs to clipboard
This commit is contained in:
committed by
pynicolas
parent
f4aad46cb6
commit
3f10a1cd55
@@ -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) }
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* 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
|
||||
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<Boolean>,
|
||||
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 = {})
|
||||
}
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<AboutEvent>()
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
<resources>
|
||||
<string name="about">O aplikaci</string>
|
||||
<string name="add_page">Přidat stránku</string>
|
||||
<string name="app_name" translatable="false">FairScan</string>
|
||||
<string name="app_tagline">Jednoduchá a respektující aplikace pro skenování vašich dokumentů</string>
|
||||
<string name="back">Zpět</string>
|
||||
<string name="camera_permission_denied">Byl odepřen přístup k fotoaparátu</string>
|
||||
@@ -9,6 +8,8 @@
|
||||
<string name="cancel">Zrušit</string>
|
||||
<string name="clear_text">Smazat text</string>
|
||||
<string name="contact">Kontakt</string>
|
||||
<string name="copied_logs">Protokoly zkopírovány do schránky</string>
|
||||
<string name="copy_logs">Kopírovat protokoly</string>
|
||||
<string name="creating_pdf">Vytváření PDF…</string>
|
||||
<string name="delete_page">Smazat stránku</string>
|
||||
<string name="delete_page_warning">Chcete smazat tuto stránku?</string>
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
<string name="cancel">Abbrechen</string>
|
||||
<string name="clear_text">Text löschen</string>
|
||||
<string name="contact">Kontakt</string>
|
||||
<string name="copied_logs">Logs in die Zwischenablage kopiert</string>
|
||||
<string name="copy_logs">Logs kopieren</string>
|
||||
<string name="creating_pdf">PDF wird erstellt…</string>
|
||||
<string name="delete_page">Seite löschen</string>
|
||||
<string name="delete_page_warning">Möchten Sie diese Seite löschen?</string>
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
<string name="cancel">Cancelar</string>
|
||||
<string name="clear_text">Borrar texto</string>
|
||||
<string name="contact">Contacto</string>
|
||||
<string name="copied_logs">Registros copiados al portapapeles</string>
|
||||
<string name="copy_logs">Copiar registros</string>
|
||||
<string name="creating_pdf">Creando PDF…</string>
|
||||
<string name="delete_page">Eliminar página</string>
|
||||
<string name="delete_page_warning">¿Quieres eliminar esta página?</string>
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
<string name="cancel">Annuler</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="clear_text">Effacer le text</string>
|
||||
<string name="copied_logs">Logs copiés dans le presse-papiers</string>
|
||||
<string name="copy_logs">Copier les logs</string>
|
||||
<string name="creating_pdf">Création du PDF…</string>
|
||||
<string name="delete_page">Supprimer la page</string>
|
||||
<string name="delete_page_warning">Voulez-vous supprimer cette page ?</string>
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
<string name="cancel">Annulla</string>
|
||||
<string name="clear_text">Svuota testo</string>
|
||||
<string name="contact">Contatti</string>
|
||||
<string name="copied_logs">Log copiati negli appunti</string>
|
||||
<string name="copy_logs">Copia log</string>
|
||||
<string name="creating_pdf">Creazione PDF…</string>
|
||||
<string name="delete_page">Elimina pagina</string>
|
||||
<string name="delete_page_warning">Vuoi eliminare questa pagina?</string>
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
<string name="cancel">Cancelar</string>
|
||||
<string name="clear_text">Limpar texto</string>
|
||||
<string name="contact">Contato</string>
|
||||
<string name="copied_logs">Registros copiados para a área de transferência</string>
|
||||
<string name="copy_logs">Copiar registros</string>
|
||||
<string name="creating_pdf">Criando PDF…</string>
|
||||
<string name="delete_page">Excluir página</string>
|
||||
<string name="delete_page_warning">Deseja excluir esta página?</string>
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
<string name="cancel">Отмена</string>
|
||||
<string name="clear_text">Стереть текст</string>
|
||||
<string name="contact">Контакты</string>
|
||||
<string name="copied_logs">Журналы скопированы в буфер обмена</string>
|
||||
<string name="copy_logs">Копировать журналы</string>
|
||||
<string name="creating_pdf">Создание PDF…</string>
|
||||
<string name="delete_page">Удалить страницу</string>
|
||||
<string name="delete_page_warning">Вы желаете удалить эту страницу?</string>
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
<string name="cancel">取消</string>
|
||||
<string name="clear_text">清除文字</string>
|
||||
<string name="contact">联系人</string>
|
||||
<string name="copied_logs">日志已复制到剪贴板</string>
|
||||
<string name="copy_logs">复制日志</string>
|
||||
<string name="creating_pdf">正在创建 PDF…</string>
|
||||
<string name="delete_page">删除页面</string>
|
||||
<string name="delete_page_warning">是否要删除此页面?</string>
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
<string name="cancel">Cancel</string>
|
||||
<string name="clear_text">Clear text</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="copied_logs">Logs copied to clipboard</string>
|
||||
<string name="copy_logs">Copy logs</string>
|
||||
<string name="creating_pdf">Creating PDF…</string>
|
||||
<string name="delete_page">Delete page</string>
|
||||
<string name="delete_page_warning">Do you want to delete this page?</string>
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user