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()
|
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)
|
val cacheDir = File(Application.application.cacheDir, CRASH_REPORTS_DIR_NAME)
|
||||||
cacheDir.mkdirs()
|
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()
|
zipFile.delete()
|
||||||
val strippedDir = File(cacheDir, report.id)
|
val strippedDir = File(cacheDir, report.id)
|
||||||
strippedDir.deleteRecursively()
|
strippedDir.deleteRecursively()
|
||||||
@@ -212,7 +212,11 @@ object CrashReportManager {
|
|||||||
if (!includeConfig) {
|
if (!includeConfig) {
|
||||||
File(strippedDir, CONFIG_FILE_NAME).delete()
|
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
|
zipFile
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ data class OOMReportFile(
|
|||||||
enum class Kind {
|
enum class Kind {
|
||||||
METADATA,
|
METADATA,
|
||||||
CONFIG,
|
CONFIG,
|
||||||
|
GO_LOG,
|
||||||
PROFILE,
|
PROFILE,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,6 +37,7 @@ data class OOMReportFile(
|
|||||||
object OOMReportManager {
|
object OOMReportManager {
|
||||||
private const val METADATA_FILE_NAME = "metadata.json"
|
private const val METADATA_FILE_NAME = "metadata.json"
|
||||||
private const val CONFIG_FILE_NAME = "configuration.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 CMDLINE_FILE_NAME = "cmdline"
|
||||||
private const val READ_MARKER_FILE_NAME = ".read"
|
private const val READ_MARKER_FILE_NAME = ".read"
|
||||||
private const val OOM_REPORTS_DIR_NAME = "oom_reports"
|
private const val OOM_REPORTS_DIR_NAME = "oom_reports"
|
||||||
@@ -86,10 +88,15 @@ object OOMReportManager {
|
|||||||
if (configFile.exists()) {
|
if (configFile.exists()) {
|
||||||
files.add(OOMReportFile(OOMReportFile.Kind.CONFIG, "Configuration", configFile))
|
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 ->
|
report.directory.listFiles()?.filter { file ->
|
||||||
file.isFile &&
|
file.isFile &&
|
||||||
file.name != METADATA_FILE_NAME &&
|
file.name != METADATA_FILE_NAME &&
|
||||||
file.name != CONFIG_FILE_NAME &&
|
file.name != CONFIG_FILE_NAME &&
|
||||||
|
file.name != GO_LOG_FILE_NAME &&
|
||||||
file.name != CMDLINE_FILE_NAME &&
|
file.name != CMDLINE_FILE_NAME &&
|
||||||
file.name != READ_MARKER_FILE_NAME
|
file.name != READ_MARKER_FILE_NAME
|
||||||
}?.sortedBy { it.name }?.forEach { file ->
|
}?.sortedBy { it.name }?.forEach { file ->
|
||||||
@@ -133,10 +140,12 @@ object OOMReportManager {
|
|||||||
|
|
||||||
fun hasConfigFile(report: OOMReport): Boolean = File(report.directory, CONFIG_FILE_NAME).exists()
|
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)
|
val cacheDir = File(Application.application.cacheDir, OOM_REPORTS_DIR_NAME)
|
||||||
cacheDir.mkdirs()
|
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()
|
zipFile.delete()
|
||||||
val strippedDir = File(cacheDir, report.id)
|
val strippedDir = File(cacheDir, report.id)
|
||||||
strippedDir.deleteRecursively()
|
strippedDir.deleteRecursively()
|
||||||
@@ -145,7 +154,10 @@ object OOMReportManager {
|
|||||||
if (!includeConfig) {
|
if (!includeConfig) {
|
||||||
File(strippedDir, CONFIG_FILE_NAME).delete()
|
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
|
zipFile
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+44
-32
@@ -1,6 +1,8 @@
|
|||||||
package io.nekohasekai.sfa.compose.screen.tools
|
package io.nekohasekai.sfa.compose.screen.tools
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
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.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.DropdownMenu
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
@@ -61,6 +61,7 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
import java.io.File
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -70,10 +71,27 @@ fun CrashReportDetailScreen(navController: NavController, reportId: String) {
|
|||||||
val report = reports.find { it.id == reportId }
|
val report = reports.find { it.id == reportId }
|
||||||
var files by remember { mutableStateOf<List<CrashReportFile>>(emptyList()) }
|
var files by remember { mutableStateOf<List<CrashReportFile>>(emptyList()) }
|
||||||
var isLoading by remember { mutableStateOf(true) }
|
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 scope = rememberCoroutineScope()
|
||||||
val context = LocalContext.current
|
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) {
|
LaunchedEffect(report) {
|
||||||
if (report != null) {
|
if (report != null) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
@@ -92,13 +110,13 @@ fun CrashReportDetailScreen(navController: NavController, reportId: String) {
|
|||||||
|
|
||||||
val hasConfig = report != null && CrashReportManager.hasConfigFile(report)
|
val hasConfig = report != null && CrashReportManager.hasConfigFile(report)
|
||||||
|
|
||||||
fun shareReport(includeConfig: Boolean) {
|
fun shareReport(includeConfig: Boolean, includeLog: Boolean, useAgeEncryption: Boolean) {
|
||||||
val currentReport = report ?: return
|
val currentReport = report ?: return
|
||||||
scope.launch {
|
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 uri = FileProvider.getUriForFile(context, "${context.packageName}.cache", zipFile)
|
||||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||||
type = "application/zip"
|
type = if (useAgeEncryption) "application/octet-stream" else "application/zip"
|
||||||
putExtra(Intent.EXTRA_STREAM, uri)
|
putExtra(Intent.EXTRA_STREAM, uri)
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
}
|
}
|
||||||
@@ -116,34 +134,9 @@ fun CrashReportDetailScreen(navController: NavController, reportId: String) {
|
|||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
if (!isLoading && files.isNotEmpty()) {
|
if (!isLoading && files.isNotEmpty()) {
|
||||||
if (hasConfig) {
|
IconButton(onClick = { showShareDialog = true }) {
|
||||||
IconButton(onClick = { shareMenuExpanded = true }) {
|
|
||||||
Icon(Icons.Default.Share, contentDescription = null)
|
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 = {
|
IconButton(onClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
if (report != null) {
|
if (report != null) {
|
||||||
@@ -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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
|||||||
+47
-32
@@ -1,6 +1,8 @@
|
|||||||
package io.nekohasekai.sfa.compose.screen.tools
|
package io.nekohasekai.sfa.compose.screen.tools
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
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.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.DataObject
|
import androidx.compose.material.icons.filled.DataObject
|
||||||
import androidx.compose.material.icons.filled.Delete
|
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.Share
|
||||||
import androidx.compose.material.icons.filled.Terminal
|
import androidx.compose.material.icons.filled.Terminal
|
||||||
import androidx.compose.material.icons.outlined.Settings
|
import androidx.compose.material.icons.outlined.Settings
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.DropdownMenu
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
@@ -60,6 +61,7 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
import java.io.File
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -69,10 +71,27 @@ fun OOMReportDetailScreen(navController: NavController, reportId: String) {
|
|||||||
val report = reports.find { it.id == reportId }
|
val report = reports.find { it.id == reportId }
|
||||||
var files by remember { mutableStateOf<List<OOMReportFile>>(emptyList()) }
|
var files by remember { mutableStateOf<List<OOMReportFile>>(emptyList()) }
|
||||||
var isLoading by remember { mutableStateOf(true) }
|
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 scope = rememberCoroutineScope()
|
||||||
val context = LocalContext.current
|
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) {
|
LaunchedEffect(report) {
|
||||||
if (report != null) {
|
if (report != null) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
@@ -90,14 +109,15 @@ fun OOMReportDetailScreen(navController: NavController, reportId: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val hasConfig = report != null && OOMReportManager.hasConfigFile(report)
|
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
|
val currentReport = report ?: return
|
||||||
scope.launch {
|
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 uri = FileProvider.getUriForFile(context, "${context.packageName}.cache", zipFile)
|
||||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||||
type = "application/zip"
|
type = if (useAgeEncryption) "application/octet-stream" else "application/zip"
|
||||||
putExtra(Intent.EXTRA_STREAM, uri)
|
putExtra(Intent.EXTRA_STREAM, uri)
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
}
|
}
|
||||||
@@ -115,34 +135,9 @@ fun OOMReportDetailScreen(navController: NavController, reportId: String) {
|
|||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
if (!isLoading && files.isNotEmpty()) {
|
if (!isLoading && files.isNotEmpty()) {
|
||||||
if (hasConfig) {
|
IconButton(onClick = { showShareDialog = true }) {
|
||||||
IconButton(onClick = { shareMenuExpanded = true }) {
|
|
||||||
Icon(Icons.Default.Share, contentDescription = null)
|
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 = {
|
IconButton(onClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
if (report != null) {
|
if (report != null) {
|
||||||
@@ -219,6 +214,7 @@ fun OOMReportDetailScreen(navController: NavController, reportId: String) {
|
|||||||
val icon = when (file.kind) {
|
val icon = when (file.kind) {
|
||||||
OOMReportFile.Kind.METADATA -> Icons.Default.DataObject
|
OOMReportFile.Kind.METADATA -> Icons.Default.DataObject
|
||||||
OOMReportFile.Kind.CONFIG -> Icons.Outlined.Settings
|
OOMReportFile.Kind.CONFIG -> Icons.Outlined.Settings
|
||||||
|
OOMReportFile.Kind.GO_LOG -> Icons.Default.Description
|
||||||
OOMReportFile.Kind.PROFILE -> Icons.Default.Terminal
|
OOMReportFile.Kind.PROFILE -> Icons.Default.Terminal
|
||||||
}
|
}
|
||||||
ListItem(
|
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)
|
@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_delete">حذف</string>
|
||||||
<string name="report_share">اشتراکگذاری</string>
|
<string name="report_share">اشتراکگذاری</string>
|
||||||
<string name="report_share_with_config">اشتراکگذاری با پیکربندی</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_metadata">فراداده</string>
|
||||||
<string name="report_configuration">پیکربندی</string>
|
<string name="report_configuration">پیکربندی</string>
|
||||||
<string name="report_origin_local">محلی</string>
|
<string name="report_origin_local">محلی</string>
|
||||||
|
|||||||
@@ -543,6 +543,12 @@
|
|||||||
<string name="report_delete">Удалить</string>
|
<string name="report_delete">Удалить</string>
|
||||||
<string name="report_share">Поделиться</string>
|
<string name="report_share">Поделиться</string>
|
||||||
<string name="report_share_with_config">Поделиться с конфигурацией</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_metadata">Метаданные</string>
|
||||||
<string name="report_configuration">Конфигурация</string>
|
<string name="report_configuration">Конфигурация</string>
|
||||||
<string name="report_origin_local">Локальный</string>
|
<string name="report_origin_local">Локальный</string>
|
||||||
|
|||||||
@@ -534,6 +534,12 @@
|
|||||||
<string name="report_delete">删除</string>
|
<string name="report_delete">删除</string>
|
||||||
<string name="report_share">分享</string>
|
<string name="report_share">分享</string>
|
||||||
<string name="report_share_with_config">附带配置分享</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_metadata">元数据</string>
|
||||||
<string name="report_configuration">配置</string>
|
<string name="report_configuration">配置</string>
|
||||||
<string name="report_origin_local">本地</string>
|
<string name="report_origin_local">本地</string>
|
||||||
|
|||||||
@@ -537,6 +537,12 @@
|
|||||||
<string name="report_delete">刪除</string>
|
<string name="report_delete">刪除</string>
|
||||||
<string name="report_share">分享</string>
|
<string name="report_share">分享</string>
|
||||||
<string name="report_share_with_config">附帶配置分享</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_metadata">元數據</string>
|
||||||
<string name="report_configuration">配置</string>
|
<string name="report_configuration">配置</string>
|
||||||
<string name="report_origin_local">本地</string>
|
<string name="report_origin_local">本地</string>
|
||||||
|
|||||||
@@ -543,6 +543,12 @@
|
|||||||
<string name="report_delete">Delete</string>
|
<string name="report_delete">Delete</string>
|
||||||
<string name="report_share">Share</string>
|
<string name="report_share">Share</string>
|
||||||
<string name="report_share_with_config">Share With Configuration</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_metadata">Metadata</string>
|
||||||
<string name="report_configuration">Configuration</string>
|
<string name="report_configuration">Configuration</string>
|
||||||
<string name="report_origin_local">Local</string>
|
<string name="report_origin_local">Local</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user