AboutScreen: new button to send the last captured image (#88)

This commit is contained in:
Pierre-Yves Nicolas
2026-01-17 14:43:16 +01:00
parent 48985fb6b4
commit 2eaede0713
20 changed files with 228 additions and 15 deletions

View File

@@ -70,7 +70,6 @@ class AppContainer(context: Context) {
val homeViewModelFactory = viewModelFactory { HomeViewModel(it, context) } val homeViewModelFactory = viewModelFactory { HomeViewModel(it, context) }
val cameraViewModelFactory = viewModelFactory { CameraViewModel(it) } val cameraViewModelFactory = viewModelFactory { CameraViewModel(it) }
val aboutViewModelFactory = viewModelFactory { AboutViewModel(it) }
val settingsViewModelFactory = viewModelFactory { SettingsViewModel(it) } val settingsViewModelFactory = viewModelFactory { SettingsViewModel(it) }
fun cleanOrphanSessions() { fun cleanOrphanSessions() {

View File

@@ -50,6 +50,7 @@ import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.fairscan.app.data.FileLogger import org.fairscan.app.data.FileLogger
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
@@ -58,6 +59,7 @@ 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
import org.fairscan.app.ui.screens.about.AboutViewModel import org.fairscan.app.ui.screens.about.AboutViewModel
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
@@ -101,9 +103,14 @@ class MainActivity : ComponentActivity() {
ExportViewModel(appContainer, sessionContainer.imageRepository) ExportViewModel(appContainer, sessionContainer.imageRepository)
} }
} }
val aboutViewModel: AboutViewModel by viewModels {
appContainer.viewModelFactory {
AboutViewModel(appContainer, sessionContainer.imageRepository)
}
}
val homeViewModel: HomeViewModel by viewModels { appContainer.homeViewModelFactory } val homeViewModel: HomeViewModel by viewModels { appContainer.homeViewModelFactory }
val cameraViewModel: CameraViewModel by viewModels { appContainer.cameraViewModelFactory } val cameraViewModel: CameraViewModel by viewModels { appContainer.cameraViewModelFactory }
val aboutViewModel: AboutViewModel by viewModels { appContainer.aboutViewModelFactory }
val settingsViewModel: SettingsViewModel val settingsViewModel: SettingsViewModel
by viewModels { appContainer.settingsViewModelFactory } by viewModels { appContainer.settingsViewModelFactory }
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
@@ -119,7 +126,7 @@ class MainActivity : ComponentActivity() {
val cameraPermission = rememberCameraPermissionState() val cameraPermission = rememberCameraPermissionState()
CollectCameraEvents(cameraViewModel, viewModel) CollectCameraEvents(cameraViewModel, viewModel)
CollectExportEvents(context, exportViewModel) CollectExportEvents(context, exportViewModel)
CollectAboutEvents(context, aboutViewModel) CollectAboutEvents(context, aboutViewModel, sessionContainer.imageRepository)
FairScanTheme { FairScanTheme {
val navigation = navigation(viewModel, launchMode) val navigation = navigation(viewModel, launchMode)
@@ -187,9 +194,16 @@ class MainActivity : ComponentActivity() {
) )
} }
is Screen.Overlay.About -> { is Screen.Overlay.About -> {
LaunchedEffect(Unit) {
aboutViewModel.refreshLastCapturedImageState()
}
val aboutUiState by aboutViewModel.uiState.collectAsStateWithLifecycle()
AboutScreen( AboutScreen(
aboutUiState = aboutUiState,
onBack = navigation.back, onBack = navigation.back,
onCopyLogs = { aboutViewModel.onCopyLogsClicked() }, onCopyLogs = { aboutViewModel.onCopyLogsClicked() },
onContactWithLastImageClicked =
{ aboutViewModel.onContactWithLastImageClicked() },
onViewLibraries = navigation.toLibrariesScreen) onViewLibraries = navigation.toLibrariesScreen)
} }
is Screen.Overlay.Libraries -> { is Screen.Overlay.Libraries -> {
@@ -262,6 +276,7 @@ class MainActivity : ComponentActivity() {
private fun CollectAboutEvents( private fun CollectAboutEvents(
context: Context, context: Context,
aboutViewModel: AboutViewModel, aboutViewModel: AboutViewModel,
imageRepository: ImageRepository,
) { ) {
val clipboard = LocalClipboard.current val clipboard = LocalClipboard.current
val msgCopiedLogs = stringResource(R.string.copied_logs) val msgCopiedLogs = stringResource(R.string.copied_logs)
@@ -274,6 +289,12 @@ class MainActivity : ComponentActivity() {
) )
Toast.makeText(context, msgCopiedLogs, Toast.LENGTH_SHORT).show() Toast.makeText(context, msgCopiedLogs, Toast.LENGTH_SHORT).show()
} }
is AboutEvent.PrepareEmailWithLastImage -> {
val file = imageRepository.lastAddedSourceFile()
if (file != null) {
startActivity(createEmailWithImageIntent(context, file))
}
}
} }
} }
} }
@@ -370,8 +391,7 @@ class MainActivity : ComponentActivity() {
} }
private fun uriForFile(file: File): Uri { private fun uriForFile(file: File): Uri {
val authority = "${applicationContext.packageName}.fileprovider" return org.fairscan.app.ui.uriForFile(this, file)
return FileProvider.getUriForFile(this, authority, file)
} }
private fun checkPermissionThen( private fun checkPermissionThen(

View File

@@ -298,6 +298,14 @@ class ImageRepository(
} }
} }
} }
fun lastAddedSourceFile(): File? {
val sourceFiles = sourceDir.listFiles()?.filter { it.extension == "jpg" }
if (sourceFiles.isNullOrEmpty()) {
return null
}
return sourceFiles.maxByOrNull { it.lastModified() }
}
} }
fun Quad.toSerializable(): NormalizedQuad = fun Quad.toSerializable(): NormalizedQuad =

View File

@@ -0,0 +1,25 @@
/*
* 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
import android.content.Context
import android.net.Uri
import androidx.core.content.FileProvider
import java.io.File
fun uriForFile(context: Context, file: File): Uri {
val authority = "${context.packageName}.fileprovider"
return FileProvider.getUriForFile(context, authority, file)
}

View File

@@ -57,6 +57,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -76,8 +77,10 @@ import org.fairscan.app.ui.theme.FairScanTheme
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AboutScreen( fun AboutScreen(
aboutUiState: AboutUiState,
onBack: () -> Unit, onBack: () -> Unit,
onCopyLogs: () -> Unit, onCopyLogs: () -> Unit,
onContactWithLastImageClicked: () -> Unit,
onViewLibraries: () -> Unit, onViewLibraries: () -> Unit,
) { ) {
val showLicenseDialog = rememberSaveable { mutableStateOf(false) } val showLicenseDialog = rememberSaveable { mutableStateOf(false) }
@@ -93,7 +96,9 @@ fun AboutScreen(
) { paddingValues -> ) { paddingValues ->
AboutContent( AboutContent(
modifier = Modifier.padding(paddingValues), modifier = Modifier.padding(paddingValues),
aboutUiState,
onCopyLogs, onCopyLogs,
onContactWithLastImageClicked,
showLicenseDialog, showLicenseDialog,
onViewLibraries) onViewLibraries)
} }
@@ -105,7 +110,9 @@ fun AboutScreen(
@Composable @Composable
fun AboutContent( fun AboutContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
aboutUiState: AboutUiState,
onCopyLogs: () -> Unit, onCopyLogs: () -> Unit,
onContactWithLastImageClicked: () -> Unit,
showLicenseDialog: MutableState<Boolean>, showLicenseDialog: MutableState<Boolean>,
onViewLibraries: () -> Unit, onViewLibraries: () -> Unit,
) { ) {
@@ -145,16 +152,10 @@ fun AboutContent(
} }
Section(title = stringResource(R.string.contact)) { Section(title = stringResource(R.string.contact)) {
val emailAddress = "contact@fairscan.org"
ContactLink( ContactLink(
icon = Icons.Default.Email, icon = Icons.Default.Email,
text = emailAddress, text = EMAIL_ADDRESS,
onClick = { onClick = { context.startActivity(createContactEmailIntent()) }
val intent = Intent(Intent.ACTION_SENDTO).apply {
data = "mailto:$emailAddress".toUri()
}
context.startActivity(intent)
}
) )
val websiteUrl = "https://fairscan.org" val websiteUrl = "https://fairscan.org"
ContactLink( ContactLink(
@@ -165,6 +166,10 @@ fun AboutContent(
context.startActivity(intent) context.startActivity(intent)
} }
) )
}
Section(title = stringResource(R.string.support)) {
EmailImageButton(aboutUiState, onContactWithLastImageClicked)
CopyLogsButton (onClick = onCopyLogs) CopyLogsButton (onClick = onCopyLogs)
} }
@@ -295,11 +300,33 @@ fun CopyLogsButton(onClick: () -> Unit) {
} }
} }
@Composable
fun EmailImageButton(
aboutUiState: AboutUiState,
onContactWithLastImageClicked: () -> Unit,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(
enabled = aboutUiState.hasLastCapturedImage,
onClick = onContactWithLastImageClicked
)
.alpha(if (aboutUiState.hasLastCapturedImage) 1f else 0.5f)
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(Icons.Default.Email, contentDescription = null)
Spacer(Modifier.width(16.dp))
Text(stringResource(R.string.support_last_image))
}
}
@Preview @Preview
@Composable @Composable
fun AboutScreenPreview() { fun AboutScreenPreview() {
FairScanTheme { FairScanTheme {
AboutScreen(onBack = {}, onCopyLogs = {}, onViewLibraries = {}) val state = AboutUiState(true)
AboutScreen(state, {}, {}, {}, {})
} }
} }

View File

@@ -0,0 +1,22 @@
/*
* 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 androidx.compose.runtime.Immutable
@Immutable
data class AboutUiState(
val hasLastCapturedImage: Boolean = false,
)

View File

@@ -18,23 +18,31 @@ import android.os.Build
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.fairscan.app.AppContainer import org.fairscan.app.AppContainer
import org.fairscan.app.BuildConfig import org.fairscan.app.BuildConfig
import org.fairscan.app.data.ImageRepository
import java.time.LocalDateTime import java.time.LocalDateTime
sealed interface AboutEvent { sealed interface AboutEvent {
data class CopyLogs(val logs: String) : AboutEvent data class CopyLogs(val logs: String) : AboutEvent
object PrepareEmailWithLastImage : AboutEvent
} }
class AboutViewModel(container: AppContainer): ViewModel() { class AboutViewModel(container: AppContainer, val imageRepository: ImageRepository): ViewModel() {
private val logRepository = container.logRepository private val logRepository = container.logRepository
private val _events = MutableSharedFlow<AboutEvent>() private val _events = MutableSharedFlow<AboutEvent>()
val events = _events.asSharedFlow() val events = _events.asSharedFlow()
private val _uiState = MutableStateFlow(AboutUiState())
val uiState = _uiState.asStateFlow()
fun onCopyLogsClicked() { fun onCopyLogsClicked() {
viewModelScope.launch { viewModelScope.launch {
val logs = buildFullLogs() val logs = buildFullLogs()
@@ -55,4 +63,15 @@ class AboutViewModel(container: AppContainer): ViewModel() {
return header + logRepository.getLogs() return header + logRepository.getLogs()
} }
fun refreshLastCapturedImageState() {
_uiState.update {
it.copy(hasLastCapturedImage = imageRepository.lastAddedSourceFile() != null)
}
}
fun onContactWithLastImageClicked() {
viewModelScope.launch {
_events.emit(AboutEvent.PrepareEmailWithLastImage)
}
}
} }

View File

@@ -0,0 +1,46 @@
/*
* 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.content.Context
import android.content.Intent
import androidx.core.net.toUri
import org.fairscan.app.BuildConfig
import org.fairscan.app.ui.uriForFile
import java.io.File
const val EMAIL_ADDRESS = "contact@fairscan.org"
fun createContactEmailIntent(): Intent =
Intent(Intent.ACTION_SENDTO).apply {
data = "mailto:$EMAIL_ADDRESS".toUri()
}
fun createEmailWithImageIntent(context: Context, imageFile: File?): Intent {
val intent = Intent(Intent.ACTION_SEND).apply {
type = "image/jpeg"
putExtra(Intent.EXTRA_EMAIL, arrayOf(EMAIL_ADDRESS))
putExtra(
Intent.EXTRA_SUBJECT,
"FairScan ${BuildConfig.VERSION_NAME}"
)
if (imageFile != null) {
val uri = uriForFile(context, imageFile)
putExtra(Intent.EXTRA_STREAM, uri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
}
return Intent.createChooser(intent, null)
}

View File

@@ -52,6 +52,8 @@
<string name="share">Sdílet</string> <string name="share">Sdílet</string>
<string name="share_document">Sdílet dokument</string> <string name="share_document">Sdílet dokument</string>
<string name="storage_permission_denied">Nelze uložit soubor: oprávnění bylo odmítnuto</string> <string name="storage_permission_denied">Nelze uložit soubor: oprávnění bylo odmítnuto</string>
<string name="support">Podpora</string>
<string name="support_last_image">Nahlásit problém s posledním zachyceným obrázkem</string>
<string name="turn_off_torch">Vypnout svítilnu</string> <string name="turn_off_torch">Vypnout svítilnu</string>
<string name="turn_on_torch">Zapnout svítilnu</string> <string name="turn_on_torch">Zapnout svítilnu</string>
<string name="unknown_size">Neznámá velikost</string> <string name="unknown_size">Neznámá velikost</string>

View File

@@ -52,6 +52,8 @@
<string name="share">Teilen</string> <string name="share">Teilen</string>
<string name="share_document">Dokument teilen</string> <string name="share_document">Dokument teilen</string>
<string name="storage_permission_denied">Datei kann nicht gespeichert werden: Berechtigung verweigert</string> <string name="storage_permission_denied">Datei kann nicht gespeichert werden: Berechtigung verweigert</string>
<string name="support">Support</string>
<string name="support_last_image">Problem mit dem zuletzt aufgenommenen Bild melden</string>
<string name="turn_off_torch">Taschenlampe ausschalten</string> <string name="turn_off_torch">Taschenlampe ausschalten</string>
<string name="turn_on_torch">Taschenlampe einschalten</string> <string name="turn_on_torch">Taschenlampe einschalten</string>
<string name="unknown_size">Unbekannte Größe</string> <string name="unknown_size">Unbekannte Größe</string>

View File

@@ -52,6 +52,8 @@
<string name="share">Compartir</string> <string name="share">Compartir</string>
<string name="share_document">Compartir documento</string> <string name="share_document">Compartir documento</string>
<string name="storage_permission_denied">No se puede guardar el archivo: permiso denegado</string> <string name="storage_permission_denied">No se puede guardar el archivo: permiso denegado</string>
<string name="support">Soporte</string>
<string name="support_last_image">Informar de un problema con la última imagen capturada</string>
<string name="turn_off_torch">Apagar linterna</string> <string name="turn_off_torch">Apagar linterna</string>
<string name="turn_on_torch">Encender linterna</string> <string name="turn_on_torch">Encender linterna</string>
<string name="unknown_size">Tamaño desconocido</string> <string name="unknown_size">Tamaño desconocido</string>

View File

@@ -52,6 +52,8 @@
<string name="share">Partager</string> <string name="share">Partager</string>
<string name="share_document">Partager le document</string> <string name="share_document">Partager le document</string>
<string name="storage_permission_denied">Impossible denregistrer le fichier : permission refusée</string> <string name="storage_permission_denied">Impossible denregistrer le fichier : permission refusée</string>
<string name="support">Support</string>
<string name="support_last_image">Signaler un problème avec la dernière image capturée</string>
<string name="turn_off_torch">Éteindre la torche</string> <string name="turn_off_torch">Éteindre la torche</string>
<string name="turn_on_torch">Allumer la torche</string> <string name="turn_on_torch">Allumer la torche</string>
<string name="unknown_size">Taille inconnue</string> <string name="unknown_size">Taille inconnue</string>

View File

@@ -52,6 +52,8 @@
<string name="share">Condividi</string> <string name="share">Condividi</string>
<string name="share_document">Condividi documento</string> <string name="share_document">Condividi documento</string>
<string name="storage_permission_denied">Impossibile salvare il file: permesso negato</string> <string name="storage_permission_denied">Impossibile salvare il file: permesso negato</string>
<string name="support">Supporto</string>
<string name="support_last_image">Segnala un problema con lultima immagine acquisita</string>
<string name="turn_off_torch">Spegni la torcia</string> <string name="turn_off_torch">Spegni la torcia</string>
<string name="turn_on_torch">Accendi la torcia</string> <string name="turn_on_torch">Accendi la torcia</string>
<string name="unknown_size">Dimensione sconosciuta</string> <string name="unknown_size">Dimensione sconosciuta</string>

View File

@@ -52,6 +52,8 @@
<string name="share">Compartilhar</string> <string name="share">Compartilhar</string>
<string name="share_document">Compartilhar documento</string> <string name="share_document">Compartilhar documento</string>
<string name="storage_permission_denied">Não foi possível salvar o arquivo: permissão negada</string> <string name="storage_permission_denied">Não foi possível salvar o arquivo: permissão negada</string>
<string name="support">Suporte</string>
<string name="support_last_image">Relatar um problema com a última imagem capturada</string>
<string name="turn_off_torch">Desligar lanterna</string> <string name="turn_off_torch">Desligar lanterna</string>
<string name="turn_on_torch">Ligar lanterna</string> <string name="turn_on_torch">Ligar lanterna</string>
<string name="unknown_size">Tamanho desconhecido</string> <string name="unknown_size">Tamanho desconhecido</string>

View File

@@ -52,6 +52,8 @@
<string name="share">Поделиться</string> <string name="share">Поделиться</string>
<string name="share_document">Поделиться документом</string> <string name="share_document">Поделиться документом</string>
<string name="storage_permission_denied">Невозможно сохранить файл: доступ запрещён</string> <string name="storage_permission_denied">Невозможно сохранить файл: доступ запрещён</string>
<string name="support">Поддержка</string>
<string name="support_last_image">Сообщить о проблеме с последним сделанным изображением</string>
<string name="turn_off_torch">Выключить фонарик</string> <string name="turn_off_torch">Выключить фонарик</string>
<string name="turn_on_torch">Включить фонарик</string> <string name="turn_on_torch">Включить фонарик</string>
<string name="unknown_size">Неизвестный размер</string> <string name="unknown_size">Неизвестный размер</string>

View File

@@ -52,6 +52,8 @@
<string name="share">分享</string> <string name="share">分享</string>
<string name="share_document">分享文件</string> <string name="share_document">分享文件</string>
<string name="storage_permission_denied">無法儲存檔案:權限遭拒</string> <string name="storage_permission_denied">無法儲存檔案:權限遭拒</string>
<string name="support">支援</string>
<string name="support_last_image">回報最近拍攝影像的問題</string>
<string name="turn_off_torch">關閉閃光燈</string> <string name="turn_off_torch">關閉閃光燈</string>
<string name="turn_on_torch">開啟閃光燈</string> <string name="turn_on_torch">開啟閃光燈</string>
<string name="unknown_size">未知大小</string> <string name="unknown_size">未知大小</string>

View File

@@ -52,6 +52,8 @@
<string name="share">共享</string> <string name="share">共享</string>
<string name="share_document">分享文档</string> <string name="share_document">分享文档</string>
<string name="storage_permission_denied">无法保存文件:权限被拒绝</string> <string name="storage_permission_denied">无法保存文件:权限被拒绝</string>
<string name="support">支持</string>
<string name="support_last_image">报告最近拍摄图像的问题</string>
<string name="turn_off_torch">关闭手电筒</string> <string name="turn_off_torch">关闭手电筒</string>
<string name="turn_on_torch">打开手电筒</string> <string name="turn_on_torch">打开手电筒</string>
<string name="unknown_size">未知大小</string> <string name="unknown_size">未知大小</string>

View File

@@ -56,6 +56,8 @@
<string name="share">Share</string> <string name="share">Share</string>
<string name="share_document">Share document</string> <string name="share_document">Share document</string>
<string name="storage_permission_denied">Cannot save file: permission was denied</string> <string name="storage_permission_denied">Cannot save file: permission was denied</string>
<string name="support">Support</string>
<string name="support_last_image">Report a problem with last captured image</string>
<string name="turn_off_torch">Turn off torch</string> <string name="turn_off_torch">Turn off torch</string>
<string name="turn_on_torch">Turn on torch</string> <string name="turn_on_torch">Turn on torch</string>
<string name="unknown_size">Unknown size</string> <string name="unknown_size">Unknown size</string>

View File

@@ -6,4 +6,8 @@
<external-path <external-path
name="external_files" name="external_files"
path="." /> path="." />
<!-- source images (to send the last captured image) -->
<files-path
name="sources"
path="sources/" />
</paths> </paths>

View File

@@ -242,6 +242,29 @@ class ImageRepositoryTest {
assertThat(PageV2("1", 42, 0, quad, true).toMetadata()).isNull() assertThat(PageV2("1", 42, 0, quad, true).toMetadata()).isNull()
} }
@Test
fun last_added_source_file() {
val repo = repo()
assertThat(repo.lastAddedSourceFile()).isNull()
repo.add(byteArrayOf(101), byteArrayOf(51), metadata1)
assertThat(repo.lastAddedSourceFile()).hasBinaryContent(byteArrayOf(51))
Thread.sleep(1)
repo.add(byteArrayOf(102), byteArrayOf(52), metadata1)
assertThat(repo.lastAddedSourceFile()).hasBinaryContent(byteArrayOf(52))
val id = repo.imageIds().last()
repo.movePage(id, 0)
assertThat(repo.lastAddedSourceFile()).hasBinaryContent(byteArrayOf(52))
repo.delete(id)
assertThat(repo.lastAddedSourceFile()).hasBinaryContent(byteArrayOf(51))
val repo2 = repo()
assertThat(repo2.lastAddedSourceFile()).hasBinaryContent(byteArrayOf(51))
repo2.clear()
assertThat(repo2.lastAddedSourceFile()).isNull()
}
private fun scanDir(): File = File(getFilesDir(), SCAN_DIR_NAME) private fun scanDir(): File = File(getFilesDir(), SCAN_DIR_NAME)
private fun sourceDir(): File = File(getFilesDir(), SOURCE_DIR_NAME) private fun sourceDir(): File = File(getFilesDir(), SOURCE_DIR_NAME)