Export: detailed error reporting
This commit is contained in:
@@ -14,12 +14,15 @@
|
|||||||
*/
|
*/
|
||||||
package org.fairscan.app.ui.screens.export
|
package org.fairscan.app.ui.screens.export
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.text.format.Formatter
|
import android.text.format.Formatter
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -30,9 +33,11 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.OpenInNew
|
import androidx.compose.material.icons.automirrored.filled.OpenInNew
|
||||||
import androidx.compose.material.icons.filled.Clear
|
import androidx.compose.material.icons.filled.Clear
|
||||||
|
import androidx.compose.material.icons.filled.ContentCopy
|
||||||
import androidx.compose.material.icons.filled.Done
|
import androidx.compose.material.icons.filled.Done
|
||||||
import androidx.compose.material.icons.filled.Download
|
import androidx.compose.material.icons.filled.Download
|
||||||
import androidx.compose.material.icons.filled.Share
|
import androidx.compose.material.icons.filled.Share
|
||||||
@@ -81,6 +86,7 @@ import org.fairscan.app.ui.dummyNavigation
|
|||||||
import org.fairscan.app.ui.screens.settings.ExportFormat.PDF
|
import org.fairscan.app.ui.screens.settings.ExportFormat.PDF
|
||||||
import org.fairscan.app.ui.theme.FairScanTheme
|
import org.fairscan.app.ui.theme.FairScanTheme
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@@ -243,8 +249,8 @@ private fun TextFieldAndPdfInfos(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
SaveStatusBar(uiState, onOpen)
|
SaveStatusBar(uiState, onOpen)
|
||||||
if (uiState.errorMessage != null) {
|
if (uiState.error != null) {
|
||||||
ErrorBar(uiState.errorMessage)
|
ErrorBar(uiState.error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,7 +382,7 @@ fun ExportButton(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SaveInfoBar(savedBundle: SavedBundle, onOpen: (SavedItem) -> Unit) {
|
private fun SaveInfoBar(savedBundle: SavedBundle, onOpen: (SavedItem) -> Unit) {
|
||||||
val dirName = savedBundle.exportDirName?:stringResource(R.string.download_dirname)
|
val dirName = savedBundle.saveDir?.name?:stringResource(R.string.download_dirname)
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.Absolute.SpaceBetween,
|
horizontalArrangement = Arrangement.Absolute.SpaceBetween,
|
||||||
@@ -409,18 +415,128 @@ private fun SaveInfoBar(savedBundle: SavedBundle, onOpen: (SavedItem) -> Unit) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ErrorBar(errorMessage: String) {
|
private fun ErrorBar(error: ExportError) {
|
||||||
Text(
|
val (summary, details) = error.toDisplayText()
|
||||||
text = stringResource(R.string.error, errorMessage),
|
val context = LocalContext.current
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.error,
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(MaterialTheme.colorScheme.errorContainer)
|
.border(
|
||||||
.padding(16.dp),
|
1.dp,
|
||||||
|
MaterialTheme.colorScheme.error,
|
||||||
|
RoundedCornerShape(12.dp)
|
||||||
)
|
)
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = summary,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (details != null) {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
val clipboard =
|
||||||
|
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
val text = buildString {
|
||||||
|
append(summary)
|
||||||
|
append("\n\n")
|
||||||
|
append(details)
|
||||||
|
}
|
||||||
|
clipboard.setPrimaryClip(
|
||||||
|
ClipData.newPlainText("Export error", text)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.ContentCopy,
|
||||||
|
contentDescription = stringResource(R.string.copy_logs),
|
||||||
|
modifier = Modifier.size(18.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details != null) {
|
||||||
|
Text(
|
||||||
|
text = details,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.error.copy(alpha = 0.8f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ExportError.toDisplayText(): Pair<String, String?> {
|
||||||
|
return when (this) {
|
||||||
|
is ExportError.OnPrepare -> {
|
||||||
|
val summary = message
|
||||||
|
val details = throwable.message
|
||||||
|
summary to details
|
||||||
|
}
|
||||||
|
|
||||||
|
is ExportError.OnSave -> {
|
||||||
|
val summary = stringResource(messageRes)
|
||||||
|
val contextLines = buildErrorContextLines(saveDir)
|
||||||
|
val details = buildString {
|
||||||
|
if (contextLines.isNotEmpty()) {
|
||||||
|
append(contextLines.joinToString("\n"))
|
||||||
|
}
|
||||||
|
throwable?.message?.let {
|
||||||
|
if (isNotEmpty()) append("\n\n")
|
||||||
|
append(it)
|
||||||
|
}
|
||||||
|
}.ifEmpty { null }
|
||||||
|
|
||||||
|
summary to details
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun buildErrorContextLines(
|
||||||
|
saveDir: SaveDir?,
|
||||||
|
): List<String> {
|
||||||
|
val defaultDirName = stringResource(R.string.download_dirname)
|
||||||
|
|
||||||
|
val folderLine = when {
|
||||||
|
saveDir == null ->
|
||||||
|
stringResource(R.string.error_context_folder, defaultDirName)
|
||||||
|
|
||||||
|
saveDir.name != null ->
|
||||||
|
stringResource(R.string.error_context_folder, saveDir.name)
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
val providerLine = saveDir?.uri?.authority
|
||||||
|
?.let(::providerLabel)
|
||||||
|
?.let { stringResource(R.string.error_context_provider, it) }
|
||||||
|
|
||||||
|
return listOfNotNull(folderLine, providerLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun providerLabel(authority: String): String =
|
||||||
|
when {
|
||||||
|
authority.contains("nextcloud", ignoreCase = true) ->
|
||||||
|
"Nextcloud"
|
||||||
|
authority == "com.android.externalstorage.documents" ->
|
||||||
|
"Local storage"
|
||||||
|
else ->
|
||||||
|
authority
|
||||||
|
}
|
||||||
|
|
||||||
fun defaultFilename(): String {
|
fun defaultFilename(): String {
|
||||||
val timestamp = SimpleDateFormat("yyyy-MM-dd HH.mm.ss", Locale.getDefault()).format(Date())
|
val timestamp = SimpleDateFormat("yyyy-MM-dd HH.mm.ss", Locale.getDefault()).format(Date())
|
||||||
return "Scan $timestamp"
|
return "Scan $timestamp"
|
||||||
@@ -468,7 +584,8 @@ fun PreviewExportScreenAfterSave() {
|
|||||||
@Composable
|
@Composable
|
||||||
fun ExportScreenPreviewWithError() {
|
fun ExportScreenPreviewWithError() {
|
||||||
ExportPreviewToCustomize(
|
ExportPreviewToCustomize(
|
||||||
ExportUiState(errorMessage = "PDF generation failed")
|
ExportUiState(error =
|
||||||
|
ExportError.OnPrepare("PDF generation failed", IOException("Boom")))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,7 +598,7 @@ fun PreviewExportScreenAfterSaveHorizontal() {
|
|||||||
result = ExportResult.Pdf(file, 442897L, 3),
|
result = ExportResult.Pdf(file, 442897L, 3),
|
||||||
savedBundle = SavedBundle(
|
savedBundle = SavedBundle(
|
||||||
listOf(SavedItem(file.toUri(), "my_file.pdf", PDF)),
|
listOf(SavedItem(file.toUri(), "my_file.pdf", PDF)),
|
||||||
exportDirName="MyVeryVeryLongDirectoryName"),
|
SaveDir("fil:///root/dir".toUri() ,"MyVeryVeryLongDirectoryName")),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ data class ExportUiState(
|
|||||||
val result: ExportResult? = null,
|
val result: ExportResult? = null,
|
||||||
val savedBundle: SavedBundle? = null,
|
val savedBundle: SavedBundle? = null,
|
||||||
val hasShared: Boolean = false,
|
val hasShared: Boolean = false,
|
||||||
val errorMessage: String? = null,
|
val error: ExportError? = null,
|
||||||
) {
|
) {
|
||||||
val hasSavedOrShared get() = savedBundle != null || hasShared
|
val hasSavedOrShared get() = savedBundle != null || hasShared
|
||||||
}
|
}
|
||||||
@@ -37,6 +37,24 @@ data class SavedItem(
|
|||||||
|
|
||||||
data class SavedBundle(
|
data class SavedBundle(
|
||||||
val items: List<SavedItem>,
|
val items: List<SavedItem>,
|
||||||
val exportDir: Uri? = null,
|
val saveDir: SaveDir? = null,
|
||||||
val exportDirName: String? = null,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class SaveDir(
|
||||||
|
val uri: Uri,
|
||||||
|
val name: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed class ExportError {
|
||||||
|
|
||||||
|
data class OnPrepare(
|
||||||
|
val message: String,
|
||||||
|
val throwable: Throwable,
|
||||||
|
) : ExportError()
|
||||||
|
|
||||||
|
data class OnSave(
|
||||||
|
val messageRes: Int,
|
||||||
|
val saveDir: SaveDir?,
|
||||||
|
val throwable: Throwable? = null,
|
||||||
|
): ExportError()
|
||||||
|
}
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ class ExportViewModel(container: AppContainer, val imageRepository: ImageReposit
|
|||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isGenerating = false,
|
isGenerating = false,
|
||||||
errorMessage = message
|
error = ExportError.OnPrepare(message, e),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,23 +192,24 @@ class ExportViewModel(container: AppContainer, val imageRepository: ImageReposit
|
|||||||
|
|
||||||
fun onRequestSave(context: Context) {
|
fun onRequestSave(context: Context) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_uiState.update {it.copy(isSaving = true, errorMessage = null, savedBundle = null) }
|
_uiState.update {it.copy(isSaving = true, error = null, savedBundle = null) }
|
||||||
|
val saveDir = saveDir(context)
|
||||||
try {
|
try {
|
||||||
// Must not run on the main thread: some SAF providers (e.g. Nextcloud)
|
// Must not run on the main thread: some SAF providers (e.g. Nextcloud)
|
||||||
// may perform network I/O
|
// may perform network I/O
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
save(context)
|
save(context, saveDir)
|
||||||
}
|
}
|
||||||
} catch (e: MissingExportDirPermissionException) {
|
} catch (e: MissingExportDirPermissionException) {
|
||||||
logger.e("FairScan", "Missing export dir permission", e)
|
logger.e("FairScan", "Missing export dir permission", e)
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(errorMessage =
|
it.copy(error =
|
||||||
context.getString(R.string.error_export_dir_permission_lost))
|
ExportError.OnSave(R.string.error_export_dir_permission_lost, saveDir))
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.e("FairScan", "Failed to save PDF", e)
|
logger.e("FairScan", "Failed to save PDF", e)
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(errorMessage = context.getString(R.string.error_save))
|
it.copy(error = ExportError.OnSave(R.string.error_save, saveDir, e))
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
_uiState.update { it.copy(isSaving = false) }
|
_uiState.update { it.copy(isSaving = false) }
|
||||||
@@ -216,14 +217,19 @@ class ExportViewModel(container: AppContainer, val imageRepository: ImageReposit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun save(context:Context) {
|
private suspend fun saveDir(context:Context): SaveDir? {
|
||||||
|
val uri = settingsRepository.exportDirUri.first()?.toUri() ?: return null
|
||||||
|
val name = resolveExportDirName(context, uri)
|
||||||
|
return SaveDir(uri, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun save(context: Context, saveDir: SaveDir?) {
|
||||||
val result = applyRenaming() ?: return
|
val result = applyRenaming() ?: return
|
||||||
val exportDir = settingsRepository.exportDirUri.first()?.toUri()
|
|
||||||
val savedItems = mutableListOf<SavedItem>()
|
val savedItems = mutableListOf<SavedItem>()
|
||||||
val filesForMediaScan = mutableListOf<File>()
|
val filesForMediaScan = mutableListOf<File>()
|
||||||
|
|
||||||
for (file in result.files) {
|
for (file in result.files) {
|
||||||
val saved = if (exportDir == null) {
|
val saved = if (saveDir == null) {
|
||||||
// No export dir defined -> save to Downloads
|
// No export dir defined -> save to Downloads
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
// Android 10+: use MediaStore API
|
// Android 10+: use MediaStore API
|
||||||
@@ -239,18 +245,17 @@ class ExportViewModel(container: AppContainer, val imageRepository: ImageReposit
|
|||||||
} else {
|
} else {
|
||||||
// Use Storage Access Framework to save to the chosen directory
|
// Use Storage Access Framework to save to the chosen directory
|
||||||
if (!context.contentResolver.persistedUriPermissions.any { perm ->
|
if (!context.contentResolver.persistedUriPermissions.any { perm ->
|
||||||
perm.uri == exportDir && perm.isWritePermission
|
perm.uri == saveDir.uri && perm.isWritePermission
|
||||||
}) {
|
}) {
|
||||||
throw MissingExportDirPermissionException(exportDir)
|
throw MissingExportDirPermissionException(saveDir.uri)
|
||||||
}
|
}
|
||||||
val safFile = saveViaSaf(context, file, exportDir, exportFormat)
|
val safFile = saveViaSaf(context, file, saveDir.uri, exportFormat)
|
||||||
SavedItem(safFile.uri, safFile.name ?: file.name, exportFormat)
|
SavedItem(safFile.uri, safFile.name ?: file.name, exportFormat)
|
||||||
}
|
}
|
||||||
savedItems += saved
|
savedItems += saved
|
||||||
}
|
}
|
||||||
|
|
||||||
val exportDirName = resolveExportDirName(context, exportDir)
|
val bundle = SavedBundle(savedItems, saveDir)
|
||||||
val bundle = SavedBundle(savedItems, exportDir, exportDirName)
|
|
||||||
_uiState.update { it.copy(savedBundle = bundle) }
|
_uiState.update { it.copy(savedBundle = bundle) }
|
||||||
|
|
||||||
if (exportFormat == ExportFormat.PDF) {
|
if (exportFormat == ExportFormat.PDF) {
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
<string name="download_dirname">stažených</string>
|
<string name="download_dirname">stažených</string>
|
||||||
<string name="end_scan">Ukončit skenování</string>
|
<string name="end_scan">Ukončit skenování</string>
|
||||||
<string name="error">Chyba: %1$s</string>
|
<string name="error">Chyba: %1$s</string>
|
||||||
|
<string name="error_context_folder">Složka exportu: %1$s</string>
|
||||||
|
<string name="error_context_provider">Poskytovatel: %1$s</string>
|
||||||
<string name="error_export_dir_permission_lost">Vybraná složka pro export již není dostupná. Vyberte prosím jinou složku.</string>
|
<string name="error_export_dir_permission_lost">Vybraná složka pro export již není dostupná. Vyberte prosím jinou složku.</string>
|
||||||
<string name="error_no_app">Nebyla nalezena žádná aplikace pro otevření tohoto souboru</string>
|
<string name="error_no_app">Nebyla nalezena žádná aplikace pro otevření tohoto souboru</string>
|
||||||
<string name="error_no_document">Nebyl rozpoznán žádná dokument</string>
|
<string name="error_no_document">Nebyl rozpoznán žádná dokument</string>
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
<string name="download_dirname">Downloads</string>
|
<string name="download_dirname">Downloads</string>
|
||||||
<string name="end_scan">Scan beenden</string>
|
<string name="end_scan">Scan beenden</string>
|
||||||
<string name="error">Fehler: %1$s</string>
|
<string name="error">Fehler: %1$s</string>
|
||||||
|
<string name="error_context_folder">Exportordner: %1$s</string>
|
||||||
|
<string name="error_context_provider">Anbieter: %1$s</string>
|
||||||
<string name="error_export_dir_permission_lost">Der ausgewählte Exportordner ist nicht mehr zugänglich. Bitte wählen Sie einen anderen Ordner.</string>
|
<string name="error_export_dir_permission_lost">Der ausgewählte Exportordner ist nicht mehr zugänglich. Bitte wählen Sie einen anderen Ordner.</string>
|
||||||
<string name="error_no_app">Keine App zum Öffnen dieser Datei gefunden</string>
|
<string name="error_no_app">Keine App zum Öffnen dieser Datei gefunden</string>
|
||||||
<string name="error_no_document">Kein Dokument erkannt</string>
|
<string name="error_no_document">Kein Dokument erkannt</string>
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
<string name="download_dirname">Descargas</string>
|
<string name="download_dirname">Descargas</string>
|
||||||
<string name="end_scan">Finalizar escaneo</string>
|
<string name="end_scan">Finalizar escaneo</string>
|
||||||
<string name="error">Error: %1$s</string>
|
<string name="error">Error: %1$s</string>
|
||||||
|
<string name="error_context_folder">Carpeta de exportación: %1$s</string>
|
||||||
|
<string name="error_context_provider">Proveedor: %1$s</string>
|
||||||
<string name="error_export_dir_permission_lost">La carpeta de exportación seleccionada ya no es accesible. Por favor, elija otra carpeta.</string>
|
<string name="error_export_dir_permission_lost">La carpeta de exportación seleccionada ya no es accesible. Por favor, elija otra carpeta.</string>
|
||||||
<string name="error_no_app">No se encontró ninguna aplicación para abrir este archivo</string>
|
<string name="error_no_app">No se encontró ninguna aplicación para abrir este archivo</string>
|
||||||
<string name="error_no_document">No se detectó ningún documento</string>
|
<string name="error_no_document">No se detectó ningún documento</string>
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
<string name="download_dirname">Téléchargements</string>
|
<string name="download_dirname">Téléchargements</string>
|
||||||
<string name="end_scan">Terminer le scan</string>
|
<string name="end_scan">Terminer le scan</string>
|
||||||
<string name="error">Erreur : %1$s</string>
|
<string name="error">Erreur : %1$s</string>
|
||||||
|
<string name="error_context_folder">Dossier d’export : %1$s</string>
|
||||||
|
<string name="error_context_provider">Fournisseur : %1$s</string>
|
||||||
<string name="error_export_dir_permission_lost">Le dossier d’export sélectionné n’est plus accessible. Veuillez choisir un autre dossier.</string>
|
<string name="error_export_dir_permission_lost">Le dossier d’export sélectionné n’est plus accessible. Veuillez choisir un autre dossier.</string>
|
||||||
<string name="error_no_app">Aucune application trouvée pour ouvrir ce fichier</string>
|
<string name="error_no_app">Aucune application trouvée pour ouvrir ce fichier</string>
|
||||||
<string name="error_no_document">Aucun document détecté</string>
|
<string name="error_no_document">Aucun document détecté</string>
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
<string name="download_dirname">Download</string>
|
<string name="download_dirname">Download</string>
|
||||||
<string name="end_scan">Termina scansione</string>
|
<string name="end_scan">Termina scansione</string>
|
||||||
<string name="error">Errore: %1$s</string>
|
<string name="error">Errore: %1$s</string>
|
||||||
|
<string name="error_context_folder">Cartella di esportazione: %1$s</string>
|
||||||
|
<string name="error_context_provider">Provider: %1$s</string>
|
||||||
<string name="error_export_dir_permission_lost">La cartella di esportazione selezionata non è più accessibile. Scegli un’altra cartella.</string>
|
<string name="error_export_dir_permission_lost">La cartella di esportazione selezionata non è più accessibile. Scegli un’altra cartella.</string>
|
||||||
<string name="error_no_app">Nessuna app trovata per aprire questo file</string>
|
<string name="error_no_app">Nessuna app trovata per aprire questo file</string>
|
||||||
<string name="error_no_document">Nessun documento rilevato</string>
|
<string name="error_no_document">Nessun documento rilevato</string>
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
<string name="download_dirname">Downloads</string>
|
<string name="download_dirname">Downloads</string>
|
||||||
<string name="end_scan">Finalizar digitalização</string>
|
<string name="end_scan">Finalizar digitalização</string>
|
||||||
<string name="error">Erro: %1$s</string>
|
<string name="error">Erro: %1$s</string>
|
||||||
|
<string name="error_context_folder">Pasta de exportação: %1$s</string>
|
||||||
|
<string name="error_context_provider">Provedor: %1$s</string>
|
||||||
<string name="error_export_dir_permission_lost">A pasta de exportação selecionada não está mais acessível. Escolha outra pasta.</string>
|
<string name="error_export_dir_permission_lost">A pasta de exportação selecionada não está mais acessível. Escolha outra pasta.</string>
|
||||||
<string name="error_no_app">Nenhum app encontrado para abrir este arquivo</string>
|
<string name="error_no_app">Nenhum app encontrado para abrir este arquivo</string>
|
||||||
<string name="error_no_document">Nenhum documento detectado</string>
|
<string name="error_no_document">Nenhum documento detectado</string>
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
<string name="download_dirname">Download</string>
|
<string name="download_dirname">Download</string>
|
||||||
<string name="end_scan">Закончить</string>
|
<string name="end_scan">Закончить</string>
|
||||||
<string name="error">Ошибка: %1$s</string>
|
<string name="error">Ошибка: %1$s</string>
|
||||||
|
<string name="error_context_folder">Папка экспорта: %1$s</string>
|
||||||
|
<string name="error_context_provider">Провайдер: %1$s</string>
|
||||||
<string name="error_export_dir_permission_lost">Выбранная папка экспорта больше недоступна. Пожалуйста, выберите другую папку.</string>
|
<string name="error_export_dir_permission_lost">Выбранная папка экспорта больше недоступна. Пожалуйста, выберите другую папку.</string>
|
||||||
<string name="error_no_app">Не найдено приложение для открытия этого файла</string>
|
<string name="error_no_app">Не найдено приложение для открытия этого файла</string>
|
||||||
<string name="error_no_document">Документ не обнаружен</string>
|
<string name="error_no_document">Документ не обнаружен</string>
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
<string name="download_dirname">İndirilenler</string>
|
<string name="download_dirname">İndirilenler</string>
|
||||||
<string name="end_scan">Taramayı bitir</string>
|
<string name="end_scan">Taramayı bitir</string>
|
||||||
<string name="error">Error: %1$s</string>
|
<string name="error">Error: %1$s</string>
|
||||||
|
<string name="error_context_folder">Dışa aktarma klasörü: %1$s</string>
|
||||||
|
<string name="error_context_provider">Sağlayıcı: %1$s</string>
|
||||||
<string name="error_export_dir_permission_lost">Seçilen dışa aktarma dizini artık erişilebilir değil. Lütfen başka bir dizin seçin.</string>
|
<string name="error_export_dir_permission_lost">Seçilen dışa aktarma dizini artık erişilebilir değil. Lütfen başka bir dizin seçin.</string>
|
||||||
<string name="error_no_app">No app found to open this file</string>
|
<string name="error_no_app">No app found to open this file</string>
|
||||||
<string name="error_no_document">No document detected</string>
|
<string name="error_no_document">No document detected</string>
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
<string name="download_dirname">下載 (Downloads)</string>
|
<string name="download_dirname">下載 (Downloads)</string>
|
||||||
<string name="end_scan">結束掃描</string>
|
<string name="end_scan">結束掃描</string>
|
||||||
<string name="error">錯誤:%1$s</string>
|
<string name="error">錯誤:%1$s</string>
|
||||||
|
<string name="error_context_folder">匯出資料夾:%1$s</string>
|
||||||
|
<string name="error_context_provider">提供者:%1$s</string>
|
||||||
<string name="error_export_dir_permission_lost">所選的匯出目錄已無法存取。請選擇其他目錄。</string>
|
<string name="error_export_dir_permission_lost">所選的匯出目錄已無法存取。請選擇其他目錄。</string>
|
||||||
<string name="error_no_app">找不到可開啟此檔案的應用程式</string>
|
<string name="error_no_app">找不到可開啟此檔案的應用程式</string>
|
||||||
<string name="error_no_document">未偵測到文件</string>
|
<string name="error_no_document">未偵測到文件</string>
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
<string name="download_dirname">下载</string>
|
<string name="download_dirname">下载</string>
|
||||||
<string name="end_scan">结束扫描</string>
|
<string name="end_scan">结束扫描</string>
|
||||||
<string name="error">错误: %1$s</string>
|
<string name="error">错误: %1$s</string>
|
||||||
|
<string name="error_context_folder">导出文件夹:%1$s</string>
|
||||||
|
<string name="error_context_provider">提供方:%1$s</string>
|
||||||
<string name="error_export_dir_permission_lost">所选的导出目录已无法访问。请选择其他目录。</string>
|
<string name="error_export_dir_permission_lost">所选的导出目录已无法访问。请选择其他目录。</string>
|
||||||
<string name="error_no_app">未找到可打开此文件的应用</string>
|
<string name="error_no_app">未找到可打开此文件的应用</string>
|
||||||
<string name="error_no_document">未检测到任何文档</string>
|
<string name="error_no_document">未检测到任何文档</string>
|
||||||
|
|||||||
@@ -20,6 +20,8 @@
|
|||||||
<string name="download_dirname">Downloads</string>
|
<string name="download_dirname">Downloads</string>
|
||||||
<string name="end_scan">End scan</string>
|
<string name="end_scan">End scan</string>
|
||||||
<string name="error">Error: %1$s</string>
|
<string name="error">Error: %1$s</string>
|
||||||
|
<string name="error_context_folder">Export folder: %1$s</string>
|
||||||
|
<string name="error_context_provider">Provider: %1$s</string>
|
||||||
<string name="error_export_dir_permission_lost">The selected export folder is no longer accessible. Please choose another folder.</string>
|
<string name="error_export_dir_permission_lost">The selected export folder is no longer accessible. Please choose another folder.</string>
|
||||||
<!-- Rare error messages should not be translated -->
|
<!-- Rare error messages should not be translated -->
|
||||||
<string name="error_file_picker_launch" translatable="false">Failed to launch system file picker on this device</string>
|
<string name="error_file_picker_launch" translatable="false">Failed to launch system file picker on this device</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user