manager: Add detailed information about the module / anykernel3 compressed package from sharing and direct flashing
- Treat certain XP module APK files as modules for processing
This commit is contained in:
@@ -35,17 +35,20 @@
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:mimeType="application/zip" />
|
||||
<data android:mimeType="application/vnd.android.package-archive" />
|
||||
<data android:scheme="content" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="application/zip" />
|
||||
<data android:mimeType="application/vnd.android.package-archive" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="application/zip" />
|
||||
<data android:mimeType="application/vnd.android.package-archive" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
|
||||
@@ -11,13 +11,10 @@ import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
@@ -40,14 +37,13 @@ import com.sukisu.ultra.ui.viewmodel.HomeViewModel
|
||||
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
|
||||
import com.sukisu.ultra.ui.webui.initPlatform
|
||||
import com.sukisu.ultra.ui.screen.FlashIt
|
||||
import com.sukisu.ultra.ui.component.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import zako.zako.zako.zakoui.activity.component.BottomBar
|
||||
import zako.zako.zako.zakoui.activity.util.*
|
||||
import java.util.zip.ZipInputStream
|
||||
import java.io.IOException
|
||||
import androidx.core.content.edit
|
||||
import com.sukisu.ultra.ui.util.rootAvailable
|
||||
|
||||
@@ -61,6 +57,9 @@ class MainActivity : ComponentActivity() {
|
||||
val showKpmInfo: Boolean = false
|
||||
)
|
||||
|
||||
private var showConfirmationDialog = mutableStateOf(false)
|
||||
private var pendingZipFiles = mutableStateOf<List<ZipFileInfo>>(emptyList())
|
||||
|
||||
private lateinit var themeChangeObserver: ThemeChangeContentObserver
|
||||
|
||||
// 添加标记避免重复初始化
|
||||
@@ -102,7 +101,7 @@ class MainActivity : ComponentActivity() {
|
||||
intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
|
||||
intent.getParcelableExtra(Intent.EXTRA_STREAM)
|
||||
}
|
||||
uri?.let { arrayListOf(it) }
|
||||
}
|
||||
@@ -140,10 +139,24 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
val navigator = navController.rememberDestinationsNavigator()
|
||||
|
||||
InstallConfirmationDialog(
|
||||
show = showConfirmationDialog.value,
|
||||
zipFiles = pendingZipFiles.value,
|
||||
onConfirm = { confirmedFiles ->
|
||||
showConfirmationDialog.value = false
|
||||
navigateToFlashScreen(confirmedFiles, navigator)
|
||||
},
|
||||
onDismiss = {
|
||||
showConfirmationDialog.value = false
|
||||
pendingZipFiles.value = emptyList()
|
||||
finish()
|
||||
}
|
||||
)
|
||||
|
||||
LaunchedEffect(zipUri) {
|
||||
if (!zipUri.isNullOrEmpty()) {
|
||||
// 检测 ZIP 文件类型并导航到相应界面
|
||||
detectZipTypeAndNavigate(zipUri, navigator)
|
||||
// 检测 ZIP 文件类型并显示确认对话框
|
||||
detectZipTypeAndShowConfirmation(zipUri)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,74 +236,34 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private enum class ZipType {
|
||||
MODULE,
|
||||
KERNEL,
|
||||
UNKNOWN
|
||||
}
|
||||
private suspend fun detectZipTypeAndShowConfirmation(zipUris: ArrayList<Uri>) {
|
||||
try {
|
||||
val zipFileInfos = ZipFileDetector.detectAndParseZipFiles(this, zipUris)
|
||||
|
||||
private fun detectZipType(uri: Uri): ZipType {
|
||||
return try {
|
||||
contentResolver.openInputStream(uri)?.use { inputStream ->
|
||||
ZipInputStream(inputStream).use { zipStream ->
|
||||
var hasModuleProp = false
|
||||
var hasToolsFolder = false
|
||||
var hasAnykernelSh = false
|
||||
|
||||
var entry = zipStream.nextEntry
|
||||
while (entry != null) {
|
||||
val entryName = entry.name.lowercase()
|
||||
|
||||
when {
|
||||
entryName == "module.prop" || entryName.endsWith("/module.prop") -> {
|
||||
hasModuleProp = true
|
||||
}
|
||||
entryName.startsWith("tools/") || entryName == "tools" -> {
|
||||
hasToolsFolder = true
|
||||
}
|
||||
entryName == "anykernel.sh" || entryName.endsWith("/anykernel.sh") -> {
|
||||
hasAnykernelSh = true
|
||||
withContext(Dispatchers.Main) {
|
||||
if (zipFileInfos.isNotEmpty()) {
|
||||
pendingZipFiles.value = zipFileInfos
|
||||
showConfirmationDialog.value = true
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
zipStream.closeEntry()
|
||||
entry = zipStream.nextEntry
|
||||
} catch (e: Exception) {
|
||||
withContext(Dispatchers.Main) {
|
||||
finish()
|
||||
}
|
||||
|
||||
when {
|
||||
hasModuleProp -> ZipType.MODULE
|
||||
hasToolsFolder && hasAnykernelSh -> ZipType.KERNEL
|
||||
else -> ZipType.UNKNOWN
|
||||
}
|
||||
}
|
||||
} ?: ZipType.UNKNOWN
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
ZipType.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun detectZipTypeAndNavigate(
|
||||
zipUris: ArrayList<Uri>,
|
||||
private fun navigateToFlashScreen(
|
||||
zipFiles: List<ZipFileInfo>,
|
||||
navigator: com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
) {
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val moduleUris = mutableListOf<Uri>()
|
||||
val kernelUris = mutableListOf<Uri>()
|
||||
lifecycleScope.launch {
|
||||
val moduleUris = zipFiles.filter { it.type == ZipType.MODULE }.map { it.uri }
|
||||
val kernelUris = zipFiles.filter { it.type == ZipType.KERNEL }.map { it.uri }
|
||||
|
||||
for (uri in zipUris) {
|
||||
val zipType = detectZipType(uri)
|
||||
when (zipType) {
|
||||
ZipType.MODULE -> moduleUris.add(uri)
|
||||
ZipType.KERNEL -> kernelUris.add(uri)
|
||||
ZipType.UNKNOWN -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 根据检测结果导航
|
||||
withContext(Dispatchers.Main) {
|
||||
when {
|
||||
// 内核文件
|
||||
kernelUris.isNotEmpty() && moduleUris.isEmpty() -> {
|
||||
@@ -312,15 +285,6 @@ class MainActivity : ComponentActivity() {
|
||||
)
|
||||
setAutoExitAfterFlash()
|
||||
}
|
||||
// 如果没有识别出任何类型的文件,则直接退出
|
||||
else -> {
|
||||
(this@MainActivity as? ComponentActivity)?.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
(this@MainActivity as? ComponentActivity)?.finish()
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,441 @@
|
||||
package com.sukisu.ultra.ui.component
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Help
|
||||
import androidx.compose.material.icons.filled.Extension
|
||||
import androidx.compose.material.icons.filled.GetApp
|
||||
import androidx.compose.material.icons.filled.Memory
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.sukisu.ultra.R
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.BufferedReader
|
||||
import java.io.IOException
|
||||
import java.io.InputStreamReader
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
enum class ZipType {
|
||||
MODULE,
|
||||
KERNEL,
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
data class ZipFileInfo(
|
||||
val uri: Uri,
|
||||
val type: ZipType,
|
||||
val name: String = "",
|
||||
val version: String = "",
|
||||
val versionCode: String = "",
|
||||
val author: String = "",
|
||||
val description: String = "",
|
||||
val kernelVersion: String = "",
|
||||
val supported: String = ""
|
||||
)
|
||||
|
||||
object ZipFileDetector {
|
||||
|
||||
fun detectZipType(context: Context, uri: Uri): ZipType {
|
||||
return try {
|
||||
context.contentResolver.openInputStream(uri)?.use { inputStream ->
|
||||
ZipInputStream(inputStream).use { zipStream ->
|
||||
var hasModuleProp = false
|
||||
var hasToolsFolder = false
|
||||
var hasAnykernelSh = false
|
||||
|
||||
var entry = zipStream.nextEntry
|
||||
while (entry != null) {
|
||||
val entryName = entry.name.lowercase()
|
||||
|
||||
when {
|
||||
entryName == "module.prop" || entryName.endsWith("/module.prop") -> {
|
||||
hasModuleProp = true
|
||||
}
|
||||
entryName.startsWith("tools/") || entryName == "tools" -> {
|
||||
hasToolsFolder = true
|
||||
}
|
||||
entryName == "anykernel.sh" || entryName.endsWith("/anykernel.sh") -> {
|
||||
hasAnykernelSh = true
|
||||
}
|
||||
}
|
||||
|
||||
zipStream.closeEntry()
|
||||
entry = zipStream.nextEntry
|
||||
}
|
||||
|
||||
when {
|
||||
hasModuleProp -> ZipType.MODULE
|
||||
hasToolsFolder && hasAnykernelSh -> ZipType.KERNEL
|
||||
else -> ZipType.UNKNOWN
|
||||
}
|
||||
}
|
||||
} ?: ZipType.UNKNOWN
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
ZipType.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
fun parseModuleInfo(context: Context, uri: Uri): ZipFileInfo {
|
||||
var zipInfo = ZipFileInfo(uri = uri, type = ZipType.MODULE)
|
||||
|
||||
try {
|
||||
context.contentResolver.openInputStream(uri)?.use { inputStream ->
|
||||
ZipInputStream(inputStream).use { zipStream ->
|
||||
var entry = zipStream.nextEntry
|
||||
while (entry != null) {
|
||||
if (entry.name.lowercase() == "module.prop" || entry.name.endsWith("/module.prop")) {
|
||||
val reader = BufferedReader(InputStreamReader(zipStream))
|
||||
val props = mutableMapOf<String, String>()
|
||||
|
||||
var line = reader.readLine()
|
||||
while (line != null) {
|
||||
if (line.contains("=") && !line.startsWith("#")) {
|
||||
val parts = line.split("=", limit = 2)
|
||||
if (parts.size == 2) {
|
||||
props[parts[0].trim()] = parts[1].trim()
|
||||
}
|
||||
}
|
||||
line = reader.readLine()
|
||||
}
|
||||
|
||||
zipInfo = zipInfo.copy(
|
||||
name = props["name"] ?: context.getString(R.string.unknown_module),
|
||||
version = props["version"] ?: "",
|
||||
versionCode = props["versionCode"] ?: "",
|
||||
author = props["author"] ?: "",
|
||||
description = props["description"] ?: ""
|
||||
)
|
||||
break
|
||||
}
|
||||
zipStream.closeEntry()
|
||||
entry = zipStream.nextEntry
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
return zipInfo
|
||||
}
|
||||
|
||||
fun parseKernelInfo(context: Context, uri: Uri): ZipFileInfo {
|
||||
var zipInfo = ZipFileInfo(uri = uri, type = ZipType.KERNEL)
|
||||
|
||||
try {
|
||||
context.contentResolver.openInputStream(uri)?.use { inputStream ->
|
||||
ZipInputStream(inputStream).use { zipStream ->
|
||||
var entry = zipStream.nextEntry
|
||||
while (entry != null) {
|
||||
if (entry.name.lowercase() == "anykernel.sh" || entry.name.endsWith("/anykernel.sh")) {
|
||||
val reader = BufferedReader(InputStreamReader(zipStream))
|
||||
val props = mutableMapOf<String, String>()
|
||||
|
||||
var inPropertiesBlock = false
|
||||
var line = reader.readLine()
|
||||
while (line != null) {
|
||||
if (line.contains("properties()")) {
|
||||
inPropertiesBlock = true
|
||||
} else if (inPropertiesBlock && line.contains("'; }")) {
|
||||
inPropertiesBlock = false
|
||||
} else if (inPropertiesBlock) {
|
||||
val propertyLine = line.trim()
|
||||
if (propertyLine.contains("=") && !propertyLine.startsWith("#")) {
|
||||
val parts = propertyLine.split("=", limit = 2)
|
||||
if (parts.size == 2) {
|
||||
val key = parts[0].trim()
|
||||
val value = parts[1].trim().removeSurrounding("'").removeSurrounding("\"")
|
||||
when (key) {
|
||||
"kernel.string" -> props["name"] = value
|
||||
"supported.versions" -> props["supported"] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 解析普通变量定义
|
||||
if (line.contains("kernel.string=") && !inPropertiesBlock) {
|
||||
val value = line.substringAfter("kernel.string=").trim().removeSurrounding("\"")
|
||||
props["name"] = value
|
||||
}
|
||||
if (line.contains("supported.versions=") && !inPropertiesBlock) {
|
||||
val value = line.substringAfter("supported.versions=").trim().removeSurrounding("\"")
|
||||
props["supported"] = value
|
||||
}
|
||||
if (line.contains("kernel.version=") && !inPropertiesBlock) {
|
||||
val value = line.substringAfter("kernel.version=").trim().removeSurrounding("\"")
|
||||
props["version"] = value
|
||||
}
|
||||
if (line.contains("kernel.author=") && !inPropertiesBlock) {
|
||||
val value = line.substringAfter("kernel.author=").trim().removeSurrounding("\"")
|
||||
props["author"] = value
|
||||
}
|
||||
|
||||
line = reader.readLine()
|
||||
}
|
||||
|
||||
zipInfo = zipInfo.copy(
|
||||
name = props["name"] ?: context.getString(R.string.unknown_kernel),
|
||||
version = props["version"] ?: "",
|
||||
author = props["author"] ?: "",
|
||||
supported = props["supported"] ?: "",
|
||||
kernelVersion = props["version"] ?: ""
|
||||
)
|
||||
break
|
||||
}
|
||||
zipStream.closeEntry()
|
||||
entry = zipStream.nextEntry
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
return zipInfo
|
||||
}
|
||||
|
||||
suspend fun detectAndParseZipFiles(context: Context, zipUris: List<Uri>): List<ZipFileInfo> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val zipFileInfos = mutableListOf<ZipFileInfo>()
|
||||
|
||||
for (uri in zipUris) {
|
||||
val zipType = detectZipType(context, uri)
|
||||
val zipInfo = when (zipType) {
|
||||
ZipType.MODULE -> parseModuleInfo(context, uri)
|
||||
ZipType.KERNEL -> parseKernelInfo(context, uri)
|
||||
ZipType.UNKNOWN -> ZipFileInfo(
|
||||
uri = uri,
|
||||
type = ZipType.UNKNOWN,
|
||||
name = context.getString(R.string.unknown_file)
|
||||
)
|
||||
}
|
||||
zipFileInfos.add(zipInfo)
|
||||
}
|
||||
|
||||
zipFileInfos.filter { it.type != ZipType.UNKNOWN }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InstallConfirmationDialog(
|
||||
show: Boolean,
|
||||
zipFiles: List<ZipFileInfo>,
|
||||
onConfirm: (List<ZipFileInfo>) -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
if (show && zipFiles.isNotEmpty()) {
|
||||
val context = LocalContext.current
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (zipFiles.any { it.type == ZipType.KERNEL })
|
||||
Icons.Default.Memory else Icons.Default.Extension,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
text = if (zipFiles.size == 1) {
|
||||
context.getString(R.string.confirm_installation)
|
||||
} else {
|
||||
context.getString(R.string.confirm_multiple_installation, zipFiles.size)
|
||||
},
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
}
|
||||
},
|
||||
text = {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 400.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
items(zipFiles.size) { index ->
|
||||
val zipFile = zipFiles[index]
|
||||
InstallItemCard(zipFile = zipFile)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = { onConfirm(zipFiles) },
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.GetApp,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(context.getString(R.string.install_confirm))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(
|
||||
context.getString(android.R.string.cancel),
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.widthIn(min = 320.dp, max = 560.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InstallItemCard(zipFile: ZipFileInfo) {
|
||||
val context = LocalContext.current
|
||||
|
||||
ElevatedCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.elevatedCardColors(
|
||||
containerColor = when (zipFile.type) {
|
||||
ZipType.MODULE -> MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
|
||||
ZipType.KERNEL -> MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.3f)
|
||||
else -> MaterialTheme.colorScheme.surfaceVariant
|
||||
}
|
||||
),
|
||||
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 0.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = when (zipFile.type) {
|
||||
ZipType.MODULE -> Icons.Default.Extension
|
||||
ZipType.KERNEL -> Icons.Default.Memory
|
||||
else -> Icons.AutoMirrored.Filled.Help
|
||||
},
|
||||
contentDescription = null,
|
||||
tint = when (zipFile.type) {
|
||||
ZipType.MODULE -> MaterialTheme.colorScheme.primary
|
||||
ZipType.KERNEL -> MaterialTheme.colorScheme.tertiary
|
||||
else -> MaterialTheme.colorScheme.onSurfaceVariant
|
||||
},
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = zipFile.name.ifEmpty {
|
||||
when (zipFile.type) {
|
||||
ZipType.MODULE -> context.getString(R.string.unknown_module)
|
||||
ZipType.KERNEL -> context.getString(R.string.unknown_kernel)
|
||||
else -> context.getString(R.string.unknown_file)
|
||||
}
|
||||
},
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Text(
|
||||
text = when (zipFile.type) {
|
||||
ZipType.MODULE -> context.getString(R.string.module_package)
|
||||
ZipType.KERNEL -> context.getString(R.string.kernel_package)
|
||||
else -> context.getString(R.string.unknown_package)
|
||||
},
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 详细信息
|
||||
if (zipFile.version.isNotEmpty() || zipFile.author.isNotEmpty() ||
|
||||
zipFile.description.isNotEmpty() || zipFile.supported.isNotEmpty()) {
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
HorizontalDivider(
|
||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f),
|
||||
thickness = 0.5.dp
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// 版本信息
|
||||
if (zipFile.version.isNotEmpty()) {
|
||||
InfoRow(
|
||||
label = context.getString(R.string.version),
|
||||
value = zipFile.version + if (zipFile.versionCode.isNotEmpty()) " (${zipFile.versionCode})" else ""
|
||||
)
|
||||
}
|
||||
|
||||
// 作者信息
|
||||
if (zipFile.author.isNotEmpty()) {
|
||||
InfoRow(
|
||||
label = context.getString(R.string.author),
|
||||
value = zipFile.author
|
||||
)
|
||||
}
|
||||
|
||||
// 描述信息 (仅模块)
|
||||
if (zipFile.description.isNotEmpty() && zipFile.type == ZipType.MODULE) {
|
||||
InfoRow(
|
||||
label = context.getString(R.string.description),
|
||||
value = zipFile.description
|
||||
)
|
||||
}
|
||||
|
||||
// 支持设备 (仅内核)
|
||||
if (zipFile.supported.isNotEmpty() && zipFile.type == ZipType.KERNEL) {
|
||||
InfoRow(
|
||||
label = context.getString(R.string.supported_devices),
|
||||
value = zipFile.supported
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InfoRow(label: String, value: String) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 2.dp),
|
||||
verticalAlignment = Alignment.Top
|
||||
) {
|
||||
Text(
|
||||
text = "$label:",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.widthIn(min = 60.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = value,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -657,4 +657,17 @@
|
||||
<string name="clean_runtime_environment_confirm">您确定要清理运行环境吗?这将停止扫描服务并删除相关文件</string>
|
||||
<string name="clean_runtime_environment_success">运行环境清理成功</string>
|
||||
<string name="clean_runtime_environment_failed">运行环境清理失败</string>
|
||||
<!-- 确认安装相关字符串 -->
|
||||
<string name="confirm_installation">确认安装</string>
|
||||
<string name="confirm_multiple_installation">确认安装(%d 个文件)</string>
|
||||
<string name="install_confirm">确认安装</string>
|
||||
<string name="module_package">模块</string>
|
||||
<string name="kernel_package">内核</string>
|
||||
<string name="unknown_package">未知类型</string>
|
||||
<string name="unknown_kernel">未知内核</string>
|
||||
<string name="unknown_file">未知文件</string>
|
||||
<string name="version">版本</string>
|
||||
<string name="author">作者</string>
|
||||
<string name="description">描述</string>
|
||||
<string name="supported_devices">支持设备</string>
|
||||
</resources>
|
||||
|
||||
@@ -665,4 +665,17 @@ Important Note:\n
|
||||
<string name="clean_runtime_environment_confirm">Are you sure you want to clean the runtime environment? This will stop the scanner service and remove related files.</string>
|
||||
<string name="clean_runtime_environment_success">Runtime environment cleaned successfully</string>
|
||||
<string name="clean_runtime_environment_failed">Failed to clean runtime environment</string>
|
||||
<!-- 确认安装相关字符串 -->
|
||||
<string name="confirm_installation">Confirm Installation</string>
|
||||
<string name="confirm_multiple_installation">Confirm Installation (%d files)</string>
|
||||
<string name="install_confirm">Install</string>
|
||||
<string name="module_package">Module</string>
|
||||
<string name="kernel_package">Kernel</string>
|
||||
<string name="unknown_package">Unknown</string>
|
||||
<string name="unknown_kernel">Unknown Kernel</string>
|
||||
<string name="unknown_file">Unknown File</string>
|
||||
<string name="version">Version</string>
|
||||
<string name="author">Author</string>
|
||||
<string name="description">Description</string>
|
||||
<string name="supported_devices">Supported Devices</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user