Improve report export
This commit is contained in:
@@ -200,10 +200,10 @@ object CrashReportManager {
|
||||
|
||||
fun hasConfigFile(report: CrashReport): Boolean = File(report.directory, CONFIG_FILE_NAME).exists()
|
||||
|
||||
suspend fun createZipArchive(report: CrashReport, includeConfig: Boolean): File = withContext(Dispatchers.IO) {
|
||||
suspend fun createZipArchive(report: CrashReport, includeConfig: Boolean, includeLog: Boolean, useAgeEncryption: Boolean): File = withContext(Dispatchers.IO) {
|
||||
val cacheDir = File(Application.application.cacheDir, CRASH_REPORTS_DIR_NAME)
|
||||
cacheDir.mkdirs()
|
||||
val zipFile = File(cacheDir, "${report.id}.zip")
|
||||
val zipFile = File(cacheDir, if (useAgeEncryption) "${report.id}.zip.age" else "${report.id}.zip")
|
||||
zipFile.delete()
|
||||
val strippedDir = File(cacheDir, report.id)
|
||||
strippedDir.deleteRecursively()
|
||||
@@ -212,7 +212,11 @@ object CrashReportManager {
|
||||
if (!includeConfig) {
|
||||
File(strippedDir, CONFIG_FILE_NAME).delete()
|
||||
}
|
||||
Libbox.createZipArchive(strippedDir.path, zipFile.path)
|
||||
if (!includeLog) {
|
||||
File(strippedDir, GO_LOG_FILE_NAME).delete()
|
||||
File(strippedDir, JVM_LOG_FILE_NAME).delete()
|
||||
}
|
||||
Libbox.createZipArchive(strippedDir.path, zipFile.path, useAgeEncryption)
|
||||
zipFile
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ data class OOMReportFile(
|
||||
enum class Kind {
|
||||
METADATA,
|
||||
CONFIG,
|
||||
GO_LOG,
|
||||
PROFILE,
|
||||
}
|
||||
}
|
||||
@@ -36,6 +37,7 @@ data class OOMReportFile(
|
||||
object OOMReportManager {
|
||||
private const val METADATA_FILE_NAME = "metadata.json"
|
||||
private const val CONFIG_FILE_NAME = "configuration.json"
|
||||
private const val GO_LOG_FILE_NAME = "go.log"
|
||||
private const val CMDLINE_FILE_NAME = "cmdline"
|
||||
private const val READ_MARKER_FILE_NAME = ".read"
|
||||
private const val OOM_REPORTS_DIR_NAME = "oom_reports"
|
||||
@@ -86,10 +88,15 @@ object OOMReportManager {
|
||||
if (configFile.exists()) {
|
||||
files.add(OOMReportFile(OOMReportFile.Kind.CONFIG, "Configuration", configFile))
|
||||
}
|
||||
val goLogFile = File(report.directory, GO_LOG_FILE_NAME)
|
||||
if (goLogFile.exists()) {
|
||||
files.add(OOMReportFile(OOMReportFile.Kind.GO_LOG, "Log", goLogFile))
|
||||
}
|
||||
report.directory.listFiles()?.filter { file ->
|
||||
file.isFile &&
|
||||
file.name != METADATA_FILE_NAME &&
|
||||
file.name != CONFIG_FILE_NAME &&
|
||||
file.name != GO_LOG_FILE_NAME &&
|
||||
file.name != CMDLINE_FILE_NAME &&
|
||||
file.name != READ_MARKER_FILE_NAME
|
||||
}?.sortedBy { it.name }?.forEach { file ->
|
||||
@@ -133,10 +140,12 @@ object OOMReportManager {
|
||||
|
||||
fun hasConfigFile(report: OOMReport): Boolean = File(report.directory, CONFIG_FILE_NAME).exists()
|
||||
|
||||
suspend fun createZipArchive(report: OOMReport, includeConfig: Boolean): File = withContext(Dispatchers.IO) {
|
||||
fun hasLogFile(report: OOMReport): Boolean = File(report.directory, GO_LOG_FILE_NAME).exists()
|
||||
|
||||
suspend fun createZipArchive(report: OOMReport, includeConfig: Boolean, includeLog: Boolean, useAgeEncryption: Boolean): File = withContext(Dispatchers.IO) {
|
||||
val cacheDir = File(Application.application.cacheDir, OOM_REPORTS_DIR_NAME)
|
||||
cacheDir.mkdirs()
|
||||
val zipFile = File(cacheDir, "${report.id}.zip")
|
||||
val zipFile = File(cacheDir, if (useAgeEncryption) "${report.id}.zip.age" else "${report.id}.zip")
|
||||
zipFile.delete()
|
||||
val strippedDir = File(cacheDir, report.id)
|
||||
strippedDir.deleteRecursively()
|
||||
@@ -145,7 +154,10 @@ object OOMReportManager {
|
||||
if (!includeConfig) {
|
||||
File(strippedDir, CONFIG_FILE_NAME).delete()
|
||||
}
|
||||
Libbox.createZipArchive(strippedDir.path, zipFile.path)
|
||||
if (!includeLog) {
|
||||
File(strippedDir, GO_LOG_FILE_NAME).delete()
|
||||
}
|
||||
Libbox.createZipArchive(strippedDir.path, zipFile.path, useAgeEncryption)
|
||||
zipFile
|
||||
}
|
||||
|
||||
|
||||
+45
-33
@@ -1,6 +1,8 @@
|
||||
package io.nekohasekai.sfa.compose.screen.tools
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -23,8 +25,6 @@ import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@@ -61,6 +61,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.text.DateFormat
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -70,10 +71,27 @@ fun CrashReportDetailScreen(navController: NavController, reportId: String) {
|
||||
val report = reports.find { it.id == reportId }
|
||||
var files by remember { mutableStateOf<List<CrashReportFile>>(emptyList()) }
|
||||
var isLoading by remember { mutableStateOf(true) }
|
||||
var shareMenuExpanded by remember { mutableStateOf(false) }
|
||||
var showShareDialog by remember { mutableStateOf(false) }
|
||||
var pendingZipFile by remember { mutableStateOf<File?>(null) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
|
||||
val saveLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.CreateDocument("application/zip"),
|
||||
) { uri ->
|
||||
val zipFile = pendingZipFile
|
||||
pendingZipFile = null
|
||||
if (uri != null && zipFile != null) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
runCatching {
|
||||
context.contentResolver.openOutputStream(uri)?.use { output ->
|
||||
zipFile.inputStream().use { input -> input.copyTo(output) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(report) {
|
||||
if (report != null) {
|
||||
withContext(Dispatchers.IO) {
|
||||
@@ -92,13 +110,13 @@ fun CrashReportDetailScreen(navController: NavController, reportId: String) {
|
||||
|
||||
val hasConfig = report != null && CrashReportManager.hasConfigFile(report)
|
||||
|
||||
fun shareReport(includeConfig: Boolean) {
|
||||
fun shareReport(includeConfig: Boolean, includeLog: Boolean, useAgeEncryption: Boolean) {
|
||||
val currentReport = report ?: return
|
||||
scope.launch {
|
||||
val zipFile = CrashReportManager.createZipArchive(currentReport, includeConfig)
|
||||
val zipFile = CrashReportManager.createZipArchive(currentReport, includeConfig, includeLog, useAgeEncryption)
|
||||
val uri = FileProvider.getUriForFile(context, "${context.packageName}.cache", zipFile)
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = "application/zip"
|
||||
type = if (useAgeEncryption) "application/octet-stream" else "application/zip"
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
@@ -116,33 +134,8 @@ fun CrashReportDetailScreen(navController: NavController, reportId: String) {
|
||||
},
|
||||
actions = {
|
||||
if (!isLoading && files.isNotEmpty()) {
|
||||
if (hasConfig) {
|
||||
IconButton(onClick = { shareMenuExpanded = true }) {
|
||||
Icon(Icons.Default.Share, contentDescription = null)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = shareMenuExpanded,
|
||||
onDismissRequest = { shareMenuExpanded = false },
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.report_share)) },
|
||||
onClick = {
|
||||
shareMenuExpanded = false
|
||||
shareReport(includeConfig = false)
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.report_share_with_config)) },
|
||||
onClick = {
|
||||
shareMenuExpanded = false
|
||||
shareReport(includeConfig = true)
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
IconButton(onClick = { shareReport(includeConfig = false) }) {
|
||||
Icon(Icons.Default.Share, contentDescription = null)
|
||||
}
|
||||
IconButton(onClick = { showShareDialog = true }) {
|
||||
Icon(Icons.Default.Share, contentDescription = null)
|
||||
}
|
||||
IconButton(onClick = {
|
||||
scope.launch {
|
||||
@@ -253,6 +246,25 @@ fun CrashReportDetailScreen(navController: NavController, reportId: String) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showShareDialog && report != null) {
|
||||
ReportShareDialog(
|
||||
hasConfig = hasConfig,
|
||||
hasLog = false,
|
||||
onSave = { includeConfig, includeLog, useAgeEncryption ->
|
||||
showShareDialog = false
|
||||
scope.launch {
|
||||
pendingZipFile = CrashReportManager.createZipArchive(report, includeConfig, includeLog, useAgeEncryption)
|
||||
saveLauncher.launch(if (useAgeEncryption) "${report.id}.zip.age" else "${report.id}.zip")
|
||||
}
|
||||
},
|
||||
onShare = { includeConfig, includeLog, useAgeEncryption ->
|
||||
showShareDialog = false
|
||||
shareReport(includeConfig, includeLog, useAgeEncryption)
|
||||
},
|
||||
onDismiss = { showShareDialog = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
+48
-33
@@ -1,6 +1,8 @@
|
||||
package io.nekohasekai.sfa.compose.screen.tools
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -16,14 +18,13 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.DataObject
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Description
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material.icons.filled.Terminal
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@@ -60,6 +61,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.text.DateFormat
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -69,10 +71,27 @@ fun OOMReportDetailScreen(navController: NavController, reportId: String) {
|
||||
val report = reports.find { it.id == reportId }
|
||||
var files by remember { mutableStateOf<List<OOMReportFile>>(emptyList()) }
|
||||
var isLoading by remember { mutableStateOf(true) }
|
||||
var shareMenuExpanded by remember { mutableStateOf(false) }
|
||||
var showShareDialog by remember { mutableStateOf(false) }
|
||||
var pendingZipFile by remember { mutableStateOf<File?>(null) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
|
||||
val saveLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.CreateDocument("application/zip"),
|
||||
) { uri ->
|
||||
val zipFile = pendingZipFile
|
||||
pendingZipFile = null
|
||||
if (uri != null && zipFile != null) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
runCatching {
|
||||
context.contentResolver.openOutputStream(uri)?.use { output ->
|
||||
zipFile.inputStream().use { input -> input.copyTo(output) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(report) {
|
||||
if (report != null) {
|
||||
withContext(Dispatchers.IO) {
|
||||
@@ -90,14 +109,15 @@ fun OOMReportDetailScreen(navController: NavController, reportId: String) {
|
||||
}
|
||||
|
||||
val hasConfig = report != null && OOMReportManager.hasConfigFile(report)
|
||||
val hasLog = report != null && OOMReportManager.hasLogFile(report)
|
||||
|
||||
fun shareReport(includeConfig: Boolean) {
|
||||
fun shareReport(includeConfig: Boolean, includeLog: Boolean, useAgeEncryption: Boolean) {
|
||||
val currentReport = report ?: return
|
||||
scope.launch {
|
||||
val zipFile = OOMReportManager.createZipArchive(currentReport, includeConfig)
|
||||
val zipFile = OOMReportManager.createZipArchive(currentReport, includeConfig, includeLog, useAgeEncryption)
|
||||
val uri = FileProvider.getUriForFile(context, "${context.packageName}.cache", zipFile)
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = "application/zip"
|
||||
type = if (useAgeEncryption) "application/octet-stream" else "application/zip"
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
@@ -115,33 +135,8 @@ fun OOMReportDetailScreen(navController: NavController, reportId: String) {
|
||||
},
|
||||
actions = {
|
||||
if (!isLoading && files.isNotEmpty()) {
|
||||
if (hasConfig) {
|
||||
IconButton(onClick = { shareMenuExpanded = true }) {
|
||||
Icon(Icons.Default.Share, contentDescription = null)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = shareMenuExpanded,
|
||||
onDismissRequest = { shareMenuExpanded = false },
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.report_share)) },
|
||||
onClick = {
|
||||
shareMenuExpanded = false
|
||||
shareReport(includeConfig = false)
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.report_share_with_config)) },
|
||||
onClick = {
|
||||
shareMenuExpanded = false
|
||||
shareReport(includeConfig = true)
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
IconButton(onClick = { shareReport(includeConfig = false) }) {
|
||||
Icon(Icons.Default.Share, contentDescription = null)
|
||||
}
|
||||
IconButton(onClick = { showShareDialog = true }) {
|
||||
Icon(Icons.Default.Share, contentDescription = null)
|
||||
}
|
||||
IconButton(onClick = {
|
||||
scope.launch {
|
||||
@@ -219,6 +214,7 @@ fun OOMReportDetailScreen(navController: NavController, reportId: String) {
|
||||
val icon = when (file.kind) {
|
||||
OOMReportFile.Kind.METADATA -> Icons.Default.DataObject
|
||||
OOMReportFile.Kind.CONFIG -> Icons.Outlined.Settings
|
||||
OOMReportFile.Kind.GO_LOG -> Icons.Default.Description
|
||||
OOMReportFile.Kind.PROFILE -> Icons.Default.Terminal
|
||||
}
|
||||
ListItem(
|
||||
@@ -257,6 +253,25 @@ fun OOMReportDetailScreen(navController: NavController, reportId: String) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showShareDialog && report != null) {
|
||||
ReportShareDialog(
|
||||
hasConfig = hasConfig,
|
||||
hasLog = hasLog,
|
||||
onSave = { includeConfig, includeLog, useAgeEncryption ->
|
||||
showShareDialog = false
|
||||
scope.launch {
|
||||
pendingZipFile = OOMReportManager.createZipArchive(report, includeConfig, includeLog, useAgeEncryption)
|
||||
saveLauncher.launch(if (useAgeEncryption) "${report.id}.zip.age" else "${report.id}.zip")
|
||||
}
|
||||
},
|
||||
onShare = { includeConfig, includeLog, useAgeEncryption ->
|
||||
showShareDialog = false
|
||||
shareReport(includeConfig, includeLog, useAgeEncryption)
|
||||
},
|
||||
onDismiss = { showShareDialog = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
package io.nekohasekai.sfa.compose.screen.tools
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.jeziellago.compose.markdowntext.MarkdownText
|
||||
import io.nekohasekai.sfa.R
|
||||
|
||||
@Composable
|
||||
fun ReportShareDialog(
|
||||
hasConfig: Boolean,
|
||||
hasLog: Boolean,
|
||||
onSave: (includeConfig: Boolean, includeLog: Boolean, useAgeEncryption: Boolean) -> Unit,
|
||||
onShare: (includeConfig: Boolean, includeLog: Boolean, useAgeEncryption: Boolean) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
var includeConfig by remember { mutableStateOf(false) }
|
||||
var includeLog by remember { mutableStateOf(true) }
|
||||
var useAgeEncryption by remember { mutableStateOf(false) }
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringResource(R.string.report_share)) },
|
||||
text = {
|
||||
Column {
|
||||
if (hasLog) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.report_with_log),
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Switch(checked = includeLog, onCheckedChange = { includeLog = it })
|
||||
}
|
||||
}
|
||||
if (hasConfig) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.report_with_config),
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Switch(checked = includeConfig, onCheckedChange = { includeConfig = it })
|
||||
}
|
||||
}
|
||||
if (hasConfig || hasLog) {
|
||||
Text(
|
||||
text = if (hasLog) {
|
||||
stringResource(R.string.report_share_privacy_warning)
|
||||
} else {
|
||||
stringResource(R.string.report_share_privacy_warning_config)
|
||||
},
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 16.dp),
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.report_with_age_encryption),
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Switch(checked = useAgeEncryption, onCheckedChange = { useAgeEncryption = it })
|
||||
}
|
||||
MarkdownText(
|
||||
markdown = stringResource(R.string.report_age_encryption_description),
|
||||
style = MaterialTheme.typography.bodySmall.copy(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
TextButton(onClick = { onSave(includeConfig, includeLog, useAgeEncryption) }) {
|
||||
Text(stringResource(R.string.save))
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
TextButton(onClick = { onShare(includeConfig, includeLog, useAgeEncryption) }) {
|
||||
Text(stringResource(R.string.report_share))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -537,6 +537,12 @@
|
||||
<string name="report_delete">حذف</string>
|
||||
<string name="report_share">اشتراکگذاری</string>
|
||||
<string name="report_share_with_config">اشتراکگذاری با پیکربندی</string>
|
||||
<string name="report_with_config">همراه با پیکربندی</string>
|
||||
<string name="report_with_log">همراه با گزارش</string>
|
||||
<string name="report_share_privacy_warning">فایلهای گزارش و پیکربندی ممکن است حاوی محتوای خصوصی باشند و نباید عمومی شوند.</string>
|
||||
<string name="report_share_privacy_warning_config">فایلهای پیکربندی ممکن است حاوی محتوای خصوصی باشند و نباید عمومی شوند.</string>
|
||||
<string name="report_with_age_encryption">رمزگذاری با age برای Project S</string>
|
||||
<string name="report_age_encryption_description">[age](https://github.com/filosottile/age) یک ابزار رمزگذاری نامتقارن مدرن و امن است. با فعالسازی، فایل zip با کلید عمومی این پروژه رمزگذاری میشود تا بتوان آن را بهصورت عمومی، مثلاً در GitHub issues، منتشر کرد.</string>
|
||||
<string name="report_metadata">فراداده</string>
|
||||
<string name="report_configuration">پیکربندی</string>
|
||||
<string name="report_origin_local">محلی</string>
|
||||
|
||||
@@ -543,6 +543,12 @@
|
||||
<string name="report_delete">Удалить</string>
|
||||
<string name="report_share">Поделиться</string>
|
||||
<string name="report_share_with_config">Поделиться с конфигурацией</string>
|
||||
<string name="report_with_config">С конфигурацией</string>
|
||||
<string name="report_with_log">С журналом</string>
|
||||
<string name="report_share_privacy_warning">Журналы и файлы конфигурации могут содержать личные данные и не должны публиковаться.</string>
|
||||
<string name="report_share_privacy_warning_config">Файлы конфигурации могут содержать личные данные и не должны публиковаться.</string>
|
||||
<string name="report_with_age_encryption">Зашифровать с помощью age для Project S</string>
|
||||
<string name="report_age_encryption_description">[age](https://github.com/filosottile/age) — современный и безопасный инструмент асимметричного шифрования. При включении ZIP-файл шифруется открытым ключом этого проекта, поэтому его можно публиковать в открытом доступе, например в GitHub issues.</string>
|
||||
<string name="report_metadata">Метаданные</string>
|
||||
<string name="report_configuration">Конфигурация</string>
|
||||
<string name="report_origin_local">Локальный</string>
|
||||
|
||||
@@ -534,6 +534,12 @@
|
||||
<string name="report_delete">删除</string>
|
||||
<string name="report_share">分享</string>
|
||||
<string name="report_share_with_config">附带配置分享</string>
|
||||
<string name="report_with_config">附带配置</string>
|
||||
<string name="report_with_log">附带日志</string>
|
||||
<string name="report_share_privacy_warning">日志和配置文件可能包含隐私内容,不应该被公开发布。</string>
|
||||
<string name="report_share_privacy_warning_config">配置文件可能包含隐私内容,不应该被公开发布。</string>
|
||||
<string name="report_with_age_encryption">使用 age 加密给 Project S</string>
|
||||
<string name="report_age_encryption_description">[age](https://github.com/filosottile/age) 是一个现代且安全的非对称加密工具。启用后,zip 文件将通过本项目的公钥加密,以便在 GitHub issues 等公共场合发布。</string>
|
||||
<string name="report_metadata">元数据</string>
|
||||
<string name="report_configuration">配置</string>
|
||||
<string name="report_origin_local">本地</string>
|
||||
|
||||
@@ -537,6 +537,12 @@
|
||||
<string name="report_delete">刪除</string>
|
||||
<string name="report_share">分享</string>
|
||||
<string name="report_share_with_config">附帶配置分享</string>
|
||||
<string name="report_with_config">附帶配置</string>
|
||||
<string name="report_with_log">附帶日誌</string>
|
||||
<string name="report_share_privacy_warning">日誌和配置檔案可能包含隱私內容,不應該被公開發佈。</string>
|
||||
<string name="report_share_privacy_warning_config">配置檔案可能包含隱私內容,不應該被公開發佈。</string>
|
||||
<string name="report_with_age_encryption">使用 age 加密給 Project S</string>
|
||||
<string name="report_age_encryption_description">[age](https://github.com/filosottile/age) 是一個現代且安全的非對稱加密工具。啟用後,zip 檔案將透過本專案的公鑰加密,以便在 GitHub issues 等公開場合發佈。</string>
|
||||
<string name="report_metadata">元數據</string>
|
||||
<string name="report_configuration">配置</string>
|
||||
<string name="report_origin_local">本地</string>
|
||||
|
||||
@@ -543,6 +543,12 @@
|
||||
<string name="report_delete">Delete</string>
|
||||
<string name="report_share">Share</string>
|
||||
<string name="report_share_with_config">Share With Configuration</string>
|
||||
<string name="report_with_config">With Configuration</string>
|
||||
<string name="report_with_log">With Log</string>
|
||||
<string name="report_share_privacy_warning">Logs and configuration files may contain private content and should not be made public.</string>
|
||||
<string name="report_share_privacy_warning_config">Configuration files may contain private content and should not be made public.</string>
|
||||
<string name="report_with_age_encryption">Encrypt with age for Project S</string>
|
||||
<string name="report_age_encryption_description">[age](https://github.com/filosottile/age) is a modern and secure asymmetric encryption tool. When enabled, the zip file is encrypted with this project\'s public key so it can be posted publicly, e.g. in GitHub issues.</string>
|
||||
<string name="report_metadata">Metadata</string>
|
||||
<string name="report_configuration">Configuration</string>
|
||||
<string name="report_origin_local">Local</string>
|
||||
|
||||
Reference in New Issue
Block a user