细化+上游代理+1.1

This commit is contained in:
2026-10-25 19:01:45 +08:00
parent 5ec7815a13
commit 070020a296
21 changed files with 1429 additions and 158 deletions
@@ -0,0 +1,67 @@
package net.bluemangoo.indpjsk.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import net.bluemangoo.indpjsk.BuildConfig
import net.bluemangoo.indpjsk.R
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AboutScreen(
readme: String,
onBack: () -> Unit,
) {
Scaffold(
topBar = {
TopAppBar(
title = { Text("关于") },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
painter = painterResource(R.drawable.arrow_back_24px),
contentDescription = "返回"
)
}
}
)
}
) { padding ->
Column(
modifier = Modifier
.padding(padding)
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(horizontal = 16.dp, vertical = 12.dp)
) {
Text(
text = "Version ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})",
style = MaterialTheme.typography.titleMedium
)
Spacer(Modifier.height(8.dp))
HorizontalDivider()
Spacer(Modifier.height(8.dp))
Text(
text = readme,
style = MaterialTheme.typography.bodySmall,
softWrap = true
)
}
}
}
@@ -0,0 +1,25 @@
package net.bluemangoo.indpjsk.ui
import android.graphics.drawable.Drawable
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.core.graphics.drawable.toBitmap
@Composable
fun AppIcon(icon: Drawable, label: String?, size: Dp = 40.dp) {
val image = remember(icon) {
runCatching { icon.toBitmap(96, 96) }.getOrNull()?.asImageBitmap()
}
if (image != null) {
Image(bitmap = image, contentDescription = label, modifier = Modifier.size(size))
} else {
Box(modifier = Modifier.size(size))
}
}
@@ -0,0 +1,168 @@
package net.bluemangoo.indpjsk.ui
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import net.bluemangoo.indpjsk.R
import net.bluemangoo.indpjsk.data.AppEntry
import net.bluemangoo.indpjsk.data.InstalledApps
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppSelectionScreen(
selected: Set<String>,
onToggle: (AppEntry) -> Unit,
onBack: () -> Unit,
) {
val context = LocalContext.current
var allApps by remember { mutableStateOf<List<AppEntry>>(emptyList()) }
var loading by remember { mutableStateOf(true) }
var query by remember { mutableStateOf("") }
LaunchedEffect(Unit) {
val list = withContext(Dispatchers.IO) { InstalledApps.list(context) }
allApps = list
loading = false
}
val filtered = remember(query, allApps) {
if (query.isBlank()) {
allApps
} else {
allApps.filter {
it.label.contains(query, ignoreCase = true) ||
it.packageName.contains(query, ignoreCase = true)
}
}
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("选择忽略的应用") },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
painter = painterResource(R.drawable.arrow_back_24px),
contentDescription = "返回"
)
}
}
)
}
) { padding ->
Column(
modifier = Modifier
.padding(padding)
.fillMaxSize()
) {
OutlinedTextField(
value = query,
onValueChange = { query = it },
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
placeholder = { Text("搜索应用名称或包名…") },
singleLine = true
)
if (loading) {
Box(
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
} else if (filtered.isEmpty()) {
Box(
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
Text(
"没有找到相关的应用",
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
} else {
LazyColumn(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
) {
items(filtered, key = { it.packageName }) { app ->
val checked = app.packageName in selected
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onToggle(app) }
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
AppIcon(icon = app.icon, label = app.label)
Spacer(modifier = Modifier.width(16.dp))
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
Text(
text = app.label,
style = MaterialTheme.typography.bodyLarge,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = app.packageName,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
Checkbox(
checked = checked,
onCheckedChange = { onToggle(app) }
)
}
}
}
}
}
}
}
@@ -1,43 +1,13 @@
package net.bluemangoo.indpjsk.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -58,7 +28,6 @@ import net.bluemangoo.indpjsk.state.CaptureStatus
import net.bluemangoo.indpjsk.vpn.LogEntry
import net.bluemangoo.indpjsk.vpn.LogLevel
import net.bluemangoo.indpjsk.vpn.VpnLog
import java.io.File
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -66,15 +35,18 @@ fun CaptureScreen(
viewModel: CaptureViewModel,
onStartCapture: () -> Unit,
onStopCapture: () -> Unit,
onOpenFile: (File) -> Unit
onOpenProxySettings: () -> Unit,
onOpenFiles: () -> Unit,
onOpenAbout: () -> Unit,
) {
val state by viewModel.uiState.collectAsState()
val logs by VpnLog.logs.collectAsState()
var dropdownExpanded by remember { mutableStateOf(false) }
val context = LocalContext.current
var enabledMap by remember { mutableStateOf(StepSelectionStore.loadAll(context) as Map<String, Boolean>) }
var uploadCode by remember { mutableStateOf(SettingsStore.loadUploadCode(context)) }
var menuExpanded by remember { mutableStateOf(false) }
var profileExpanded by remember { mutableStateOf(false) }
var showUploadCodeDialog by remember { mutableStateOf(false) }
val isRunning = state.status == CaptureStatus.RUNNING
val isCaptured = state.status == CaptureStatus.CAPTURED
@@ -85,40 +57,52 @@ fun CaptureScreen(
TopAppBar(
title = { Text("咖喱烧烤无ROOT抓包") },
actions = {
IconButton(onClick = { VpnLog.clear() }) {
Icon(
painter = painterResource(id = R.drawable.delete_24px),
contentDescription = "清空日志"
)
}
Box {
TextButton(
onClick = { dropdownExpanded = true },
enabled = !isRunning
) {
Text(
text = selected?.name ?: "无配置",
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
IconButton(onClick = { menuExpanded = true }) {
Icon(
painter = painterResource(id = R.drawable.arrow_drop_down_24px),
contentDescription = null
painter = painterResource(id = R.drawable.more_vert_24px),
contentDescription = "菜单"
)
}
DropdownMenu(
expanded = dropdownExpanded,
onDismissRequest = { dropdownExpanded = false }
expanded = menuExpanded,
onDismissRequest = { menuExpanded = false }
) {
state.profiles.forEach { profile ->
DropdownMenuItem(
text = { Text(profile.name) },
onClick = {
viewModel.selectProfile(profile)
dropdownExpanded = false
}
)
}
DropdownMenuItem(
text = { Text("上传码") },
onClick = {
menuExpanded = false
showUploadCodeDialog = true
}
)
DropdownMenuItem(
text = { Text("代理设置") },
onClick = {
menuExpanded = false
onOpenProxySettings()
}
)
DropdownMenuItem(
text = { Text("已抓文件") },
onClick = {
menuExpanded = false
onOpenFiles()
}
)
DropdownMenuItem(
text = { Text("清空日志") },
onClick = {
menuExpanded = false
VpnLog.clear()
}
)
DropdownMenuItem(
text = { Text("关于") },
onClick = {
menuExpanded = false
onOpenAbout()
}
)
}
}
}
@@ -132,19 +116,40 @@ fun CaptureScreen(
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
OutlinedTextField(
value = uploadCode,
onValueChange = {
uploadCode = it.trim()
SettingsStore.saveUploadCode(context, uploadCode)
},
modifier = Modifier.fillMaxWidth(),
enabled = !isRunning,
singleLine = true,
label = { Text("上传码(从 Haruki 工具箱 - 数据上传 - iOS 获取)") },
textStyle = MaterialTheme.typography.bodyMedium,
visualTransformation = PasswordVisualTransformation()
)
Box(modifier = Modifier.fillMaxWidth()) {
OutlinedButton(
onClick = { profileExpanded = true },
enabled = !isRunning,
modifier = Modifier
.fillMaxWidth()
.height(48.dp)
) {
Text(
text = "配置:${selected?.name ?: "未选择"}",
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)
Icon(
painter = painterResource(id = R.drawable.arrow_drop_down_24px),
contentDescription = null
)
}
DropdownMenu(
expanded = profileExpanded,
onDismissRequest = { profileExpanded = false }
) {
state.profiles.forEach { profile ->
DropdownMenuItem(
text = { Text(profile.name) },
onClick = {
viewModel.selectProfile(context, profile)
profileExpanded = false
}
)
}
}
}
if (selected != null) {
StepSelector(
@@ -191,7 +196,11 @@ fun CaptureScreen(
}
OutlinedButton(
onClick = { state.lastCapturedFile?.let(onOpenFile) },
onClick = {
if (state.lastCapturedFile != null) {
onOpenFiles()
}
},
enabled = isCaptured && state.lastCapturedFile != null,
modifier = Modifier
.fillMaxWidth()
@@ -206,6 +215,48 @@ fun CaptureScreen(
}
}
}
if (showUploadCodeDialog) {
UploadCodeDialog(onDismiss = { showUploadCodeDialog = false })
}
}
@Composable
private fun UploadCodeDialog(onDismiss: () -> Unit) {
val context = LocalContext.current
var text by remember { mutableStateOf(SettingsStore.loadUploadCode(context)) }
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("上传码") },
text = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(
value = text,
onValueChange = { text = it.trim() },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
label = { Text("Haruki 上传码") },
visualTransformation = PasswordVisualTransformation(),
textStyle = MaterialTheme.typography.bodyMedium
)
Text(
text = "从 Haruki 工具箱 - 数据上传 - iOS 获取。",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
},
confirmButton = {
TextButton(onClick = {
SettingsStore.saveUploadCode(context, text)
onDismiss()
}) { Text("保存") }
},
dismissButton = {
TextButton(onClick = onDismiss) { Text("取消") }
}
)
}
@Composable
@@ -328,4 +379,4 @@ private fun StatusLine(status: CaptureStatus, error: String?) {
fontWeight = FontWeight.Medium
)
HorizontalDivider()
}
}
@@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import net.bluemangoo.indpjsk.data.CaptureProfile
import net.bluemangoo.indpjsk.data.PredefinedProfiles
import net.bluemangoo.indpjsk.data.SettingsStore
import net.bluemangoo.indpjsk.state.CaptureStateHolder
import net.bluemangoo.indpjsk.state.CaptureStatus
import net.bluemangoo.indpjsk.vpn.CaptureVpnService
@@ -45,8 +46,9 @@ class CaptureViewModel : ViewModel() {
)
)
fun selectProfile(profile: CaptureProfile) {
fun selectProfile(context: Context, profile: CaptureProfile) {
CaptureStateHolder.selectProfile(profile)
SettingsStore.saveSelectedProfileId(context, profile.id)
}
fun startCapture(context: Context) {
@@ -0,0 +1,135 @@
package net.bluemangoo.indpjsk.ui
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
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.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import net.bluemangoo.indpjsk.R
import net.bluemangoo.indpjsk.data.CaptureFiles
import java.io.File
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CapturedFilesScreen(
onOpenFile: (File) -> Unit,
onBack: () -> Unit,
) {
val context = LocalContext.current
var version by remember { mutableIntStateOf(0) }
val files = remember(version) { CaptureFiles.list(context) }
var confirmClear by remember { mutableStateOf(false) }
Scaffold(
topBar = {
TopAppBar(
title = { Text("获取到的文件") },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
painter = painterResource(R.drawable.arrow_back_24px),
contentDescription = "返回"
)
}
},
actions = {
IconButton(
onClick = { confirmClear = true },
enabled = files.isNotEmpty()
) {
Icon(
painter = painterResource(R.drawable.delete_24px),
contentDescription = "清空"
)
}
}
)
}
) { padding ->
if (files.isEmpty()) {
Box(
modifier = Modifier
.padding(padding)
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
"还没有获取到文件",
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
} else {
LazyColumn(
modifier = Modifier
.padding(padding)
.fillMaxSize()
) {
items(files, key = { it.absolutePath }) { file ->
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onOpenFile(file) }
.padding(horizontal = 16.dp, vertical = 14.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(R.drawable.file_open_24px),
contentDescription = null
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = file.name,
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
}
}
if (confirmClear) {
AlertDialog(
onDismissRequest = { confirmClear = false },
title = { Text("清空文件") },
text = { Text("删除全部已获取的文件?") },
confirmButton = {
TextButton(onClick = {
CaptureFiles.clear(context)
version++
confirmClear = false
}) { Text("删除") }
},
dismissButton = {
TextButton(onClick = { confirmClear = false }) { Text("取消") }
}
)
}
}
@@ -0,0 +1,183 @@
package net.bluemangoo.indpjsk.ui
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import net.bluemangoo.indpjsk.R
import net.bluemangoo.indpjsk.data.AppEntry
import net.bluemangoo.indpjsk.data.InstalledApps
import net.bluemangoo.indpjsk.data.SettingsStore
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ProxySettingsScreen(
bypassPackages: Set<String>,
onBypassChange: (Set<String>) -> Unit,
onSelectApps: () -> Unit,
onBack: () -> Unit,
) {
val context = LocalContext.current
var proxy by remember { mutableStateOf(SettingsStore.loadProxy(context)) }
var pendingRemove by remember { mutableStateOf<AppEntry?>(null) }
val entries = remember(bypassPackages) {
bypassPackages.sorted().mapNotNull { InstalledApps.get(context, it) }
}
val looksValid = proxy.isBlank() ||
((proxy.startsWith("http://") || proxy.startsWith("https://")) && proxy.contains(":"))
Scaffold(
topBar = {
TopAppBar(
title = { Text("代理设置") },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
painter = painterResource(R.drawable.arrow_back_24px),
contentDescription = "返回"
)
}
}
)
}
) { padding ->
Column(
modifier = Modifier
.padding(padding)
.verticalScroll(rememberScrollState())
.padding(horizontal = 0.dp, vertical = 12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Column(
modifier = Modifier.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text("上游代理", style = MaterialTheme.typography.labelLarge)
OutlinedTextField(
value = proxy,
onValueChange = {
proxy = it
SettingsStore.saveProxy(context, it)
},
modifier = Modifier.fillMaxWidth(),
singleLine = true,
label = { Text("上游代理地址") },
placeholder = { Text("http://127.0.0.1:7890") },
isError = !looksValid,
supportingText = {
Text(
text = if (looksValid) {
"留空以忽略;只支持 http / https 代理"
} else {
"格式示例:http://127.0.0.1:7890"
}
)
}
)
HorizontalDivider()
Text("绕过本应用代理的应用", style = MaterialTheme.typography.labelLarge)
}
Card(
onClick = onSelectApps,
shape = RectangleShape,
colors = CardDefaults.cardColors(
containerColor = Color.Transparent
),
modifier = Modifier
.fillMaxWidth()
) {
Text(
text = "选择应用(${bypassPackages.size})",
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)
)
}
if (entries.isEmpty()) {
Text(
text = "列表为空",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 0.dp)
)
} else {
entries.forEach { entry ->
BypassAppRow(entry = entry, onLongPress = { pendingRemove = entry })
}
}
}
}
pendingRemove?.let { entry ->
AlertDialog(
onDismissRequest = { pendingRemove = null },
title = { Text("移除应用") },
text = { Text("从应用列表移除「${entry.label}」?") },
confirmButton = {
TextButton(onClick = {
onBypassChange(bypassPackages - entry.packageName)
pendingRemove = null
}) { Text("移除") }
},
dismissButton = {
TextButton(onClick = { pendingRemove = null }) { Text("取消") }
}
)
}
}
@Composable
private fun BypassAppRow(entry: AppEntry, onLongPress: () -> Unit) {
Card(
shape = RectangleShape,
colors = CardDefaults.cardColors(
containerColor = Color.Transparent
),
modifier = Modifier
.fillMaxWidth()
.combinedClickable(
onClick = {},
onLongClick = onLongPress
)
) {
Row(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
AppIcon(icon = entry.icon, label = entry.label, size = 32.dp)
Spacer(modifier = Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = entry.label,
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = entry.packageName,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
}