package io.nekohasekai.sagernet.ui.compose.screens
import android.content.Intent
import android.net.Uri
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.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Apps
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Cable
import androidx.compose.material.icons.filled.Construction
import androidx.compose.material.icons.filled.Dashboard
import androidx.compose.material.icons.filled.Dns
import androidx.compose.material.icons.filled.Face
import androidx.compose.material.icons.filled.Lan
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Palette
import androidx.compose.material.icons.filled.Public
import androidx.compose.material.icons.filled.RocketLaunch
import androidx.compose.material.icons.filled.Route
import androidx.compose.material.icons.filled.Security
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Speed
import androidx.compose.material.icons.filled.Tune
import androidx.compose.material.icons.filled.Translate
import androidx.compose.material.icons.filled.WbSunny
import androidx.compose.material.icons.filled.Wifi
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.ui.input.nestedscroll.nestedScroll
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.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import io.nekohasekai.sagernet.GroupOrder
import io.nekohasekai.sagernet.LogLevel
import io.nekohasekai.sagernet.RouteMode
import io.nekohasekai.sagernet.TunImplementation
import io.nekohasekai.sagernet.database.DataStore
import io.nekohasekai.sagernet.ui.compose.ComposeAssetsActivity
import io.nekohasekai.sagernet.ui.compose.ComposeStunActivity
import io.nekohasekai.sagernet.ui.compose.components.DividerItem
import io.nekohasekai.sagernet.ui.compose.components.OwenclaveTopAppBar
import io.nekohasekai.sagernet.ui.compose.components.PreferenceGroup
import io.nekohasekai.sagernet.ui.compose.components.PreferenceHeader
import io.nekohasekai.sagernet.ui.compose.components.PreferenceItem
import io.nekohasekai.sagernet.ui.compose.components.SectionCard
import io.nekohasekai.sagernet.ui.compose.components.SwitchPreferenceItem
import io.nekohasekai.sagernet.utils.Theme
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
onMenuClick: () -> Unit,
onThemeChanged: (Int) -> Unit = {},
onNightThemeChanged: (Int) -> Unit = {},
) {
val context = LocalContext.current
var showThemePicker by remember { mutableStateOf(false) }
var showLogoPicker by remember { mutableStateOf(false) }
var currentLogo by remember { mutableStateOf(DataStore.appLogo) }
var showNightModePicker by remember { mutableStateOf(false) }
var showServiceModePicker by remember { mutableStateOf(false) }
var showTunPicker by remember { mutableStateOf(false) }
var showMtuPicker by remember { mutableStateOf(false) }
var showLogLevelPicker by remember { mutableStateOf(false) }
var showRootCaPicker by remember { mutableStateOf(false) }
var showDomainStrategyPicker by remember { mutableStateOf(false) }
var showRouteModePicker by remember { mutableStateOf(false) }
var showSpeedIntervalPicker by remember { mutableStateOf(false) }
var showOutboundStrategyPicker by remember { mutableStateOf(false) }
var showOutboundDirectStrategyPicker by remember { mutableStateOf(false) }
var showOutboundServerStrategyPicker by remember { mutableStateOf(false) }
var showRemoteDnsQueryPicker by remember { mutableStateOf(false) }
var showDirectDnsQueryPicker by remember { mutableStateOf(false) }
var showRulesProviderPicker by remember { mutableStateOf(false) }
var showFabStylePicker by remember { mutableStateOf(false) }
var showFragmentMethodPicker by remember { mutableStateOf(false) }
var editingText by remember { mutableStateOf
>(Pair("", "")) }
var editingTextKey by remember { mutableStateOf("") }
var editingTextValue by remember { mutableStateOf("") }
var showTextEditDialog by remember { mutableStateOf(false) }
var socksProxyChain by remember { mutableStateOf(DataStore.socksProxyChainEnabled) }
var enableTwps2 by remember { mutableStateOf(DataStore.enableTwps2) }
var enableFragment by remember { mutableStateOf(DataStore.enableFragment) }
var interruptReusedConnections by remember { mutableStateOf(DataStore.interruptReusedConnections) }
var profileSecurityAdvisory by remember { mutableStateOf(DataStore.profileSecurityAdvisory) }
var enableUnlockRu by remember { mutableStateOf(DataStore.enableUnlockRu) }
var directProxyMode by remember { mutableStateOf(DataStore.directProxyMode) }
var realityDisableX25519Mlkem768 by remember { mutableStateOf(DataStore.realityDisableX25519Mlkem768) }
var hysteria2OmitMaxDatagramFrameSize by remember { mutableStateOf(DataStore.hysteria2OmitMaxDatagramFrameSize) }
var grpcServiceNameCompat by remember { mutableStateOf(DataStore.grpcServiceNameCompat) }
var enableFragmentForDirect by remember { mutableStateOf(DataStore.enableFragmentForDirect) }
var persistAcrossReboot by remember { mutableStateOf(DataStore.persistAcrossReboot) }
var trafficSniffing by remember { mutableStateOf(DataStore.trafficSniffing) }
var destinationOverride by remember { mutableStateOf(DataStore.destinationOverride) }
var enableDnsRouting by remember { mutableStateOf(DataStore.enableDnsRouting) }
var enableFakeDns by remember { mutableStateOf(DataStore.enableFakeDns) }
var hijackDns by remember { mutableStateOf(DataStore.hijackDns) }
var bypassLan by remember { mutableStateOf(DataStore.bypassLan) }
var proxyApps by remember { mutableStateOf(DataStore.proxyApps) }
var allowAppsBypassVpn by remember { mutableStateOf(DataStore.allowAppsBypassVpn) }
var enableVPNIPv6 by remember { mutableStateOf(DataStore.enableVPNInterfaceIPv6Address) }
var meteredNetwork by remember { mutableStateOf(DataStore.meteredNetwork) }
var enablePcap by remember { mutableStateOf(DataStore.enablePcap) }
var discardICMP by remember { mutableStateOf(DataStore.discardICMP) }
var showDirectSpeed by remember { mutableStateOf(DataStore.showDirectSpeed) }
var appTrafficStats by remember { mutableStateOf(DataStore.appTrafficStatistics) }
var profileTrafficStats by remember { mutableStateOf(DataStore.profileTrafficStatistics) }
var requireSocks by remember { mutableStateOf(DataStore.requireSocks) }
var socksUDP by remember { mutableStateOf(DataStore.socksUDP) }
var requireHttp by remember { mutableStateOf(DataStore.requireHttp) }
var appendHttpProxy by remember { mutableStateOf(DataStore.appendHttpProxy) }
var requireTransproxy by remember { mutableStateOf(DataStore.requireTransproxy) }
var requireDnsInbound by remember { mutableStateOf(DataStore.requireDnsInbound) }
var allowAccess by remember { mutableStateOf(DataStore.allowAccess) }
var useLocalDnsAsDirectDns by remember { mutableStateOf(DataStore.useLocalDnsAsDirectDns) }
var useLocalDnsAsBootstrapDns by remember { mutableStateOf(DataStore.useLocalDnsAsBootstrapDns) }
var showGroupName by remember { mutableStateOf(DataStore.showGroupName) }
var alwaysShowAddress by remember { mutableStateOf(DataStore.alwaysShowAddress) }
var acquireWakeLock by remember { mutableStateOf(DataStore.acquireWakeLock) }
var useIECUnit by remember { mutableStateOf(DataStore.useIECUnit) }
var queryAllPackagesAlternativeMethod by remember { mutableStateOf(DataStore.queryAllPackagesAlternativeMethod) }
// ── Dialogs ──
if (showThemePicker) {
val themes = listOf(
"Pink" to Theme.PINK, "Red" to Theme.RED, "Purple" to Theme.PURPLE,
"Deep Purple" to Theme.DEEP_PURPLE, "Indigo" to Theme.INDIGO, "Blue" to Theme.BLUE,
"Light Blue" to Theme.LIGHT_BLUE, "Cyan" to Theme.CYAN, "Teal" to Theme.TEAL,
"Green" to Theme.GREEN, "Light Green" to Theme.LIGHT_GREEN, "Lime" to Theme.LIME,
"Yellow" to Theme.YELLOW, "Amber" to Theme.AMBER, "Orange" to Theme.ORANGE,
"Deep Orange" to Theme.DEEP_ORANGE, "Brown" to Theme.BROWN, "Grey" to Theme.GREY,
"Blue Grey" to Theme.BLUE_GREY, "Black" to Theme.BLACK, "Dynamic" to Theme.DYNAMIC,
"Unrecovery" to Theme.UNRECOVERY,
)
SingleChoiceDialog(
title = "Theme",
items = themes,
selected = DataStore.appTheme,
onSelect = { DataStore.appTheme = it; onThemeChanged(it); showThemePicker = false },
onDismiss = { showThemePicker = false },
)
}
if (showLogoPicker) {
LogoPickerDialog(
selected = currentLogo,
onSelect = {
DataStore.appLogo = it
currentLogo = it
showLogoPicker = false
},
onDismiss = { showLogoPicker = false },
)
}
if (showNightModePicker) {
SingleChoiceDialog(
title = "Night Mode",
items = listOf("Follow system" to 0, "Always dark" to 1, "Always light" to 2),
selected = DataStore.nightTheme,
onSelect = { DataStore.nightTheme = it; onNightThemeChanged(it); showNightModePicker = false },
onDismiss = { showNightModePicker = false },
)
}
if (showServiceModePicker) {
SingleChoiceDialog(
title = "Service Mode",
items = listOf("VPN" to "vpn", "Proxy" to "proxy"),
selected = DataStore.serviceMode,
onSelect = { DataStore.serviceMode = it; showServiceModePicker = false },
onDismiss = { showServiceModePicker = false },
)
}
if (showTunPicker) {
SingleChoiceDialog(
title = "TUN Implementation",
items = listOf("gVisor" to TunImplementation.GVISOR, "System" to TunImplementation.SYSTEM),
selected = DataStore.tunImplementation,
onSelect = { DataStore.tunImplementation = it; showTunPicker = false },
onDismiss = { showTunPicker = false },
)
}
if (showMtuPicker) {
var mtuValue by remember { mutableStateOf(DataStore.mtu.toString()) }
io.nekohasekai.sagernet.ui.compose.components.ExpressiveDialog(onDismissRequest = { showMtuPicker = false }) {
Text(
text = "MTU",
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(bottom = 16.dp),
)
io.nekohasekai.sagernet.ui.compose.components.ExpressiveTextField(
value = mtuValue,
onValueChange = { mtuValue = it.filter { c -> c.isDigit() } },
singleLine = true,
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(
keyboardType = androidx.compose.ui.text.input.KeyboardType.Number
),
)
Spacer(Modifier.height(16.dp))
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
TextButton(onClick = { showMtuPicker = false }) { Text("Cancel") }
Spacer(Modifier.width(8.dp))
TextButton(onClick = {
DataStore.mtu = mtuValue.toIntOrNull() ?: 1500
showMtuPicker = false
}) { Text("OK") }
}
}
}
if (showLogLevelPicker) {
SingleChoiceDialog(
title = "Log Level",
items = listOf("None" to LogLevel.NONE, "Error" to LogLevel.ERROR, "Warning" to LogLevel.WARNING, "Info" to LogLevel.INFO, "Debug" to LogLevel.DEBUG),
selected = DataStore.logLevel,
onSelect = { DataStore.logLevel = it; showLogLevelPicker = false },
onDismiss = { showLogLevelPicker = false },
)
}
if (showRootCaPicker) {
SingleChoiceDialog(
title = "Root CA Provider",
items = listOf("Mozilla" to 0, "System" to 1, "System & User" to 2, "Custom" to 3),
selected = DataStore.providerRootCA,
onSelect = { DataStore.providerRootCA = it; showRootCaPicker = false },
onDismiss = { showRootCaPicker = false },
)
}
if (showDomainStrategyPicker) {
SingleChoiceDialog(
title = "Domain Strategy",
items = listOf("AsIs" to "AsIs", "IPIfNonMatch" to "IPIfNonMatch", "IPOnDemand" to "IPOnDemand"),
selected = DataStore.domainStrategy,
onSelect = { DataStore.domainStrategy = it; showDomainStrategyPicker = false },
onDismiss = { showDomainStrategyPicker = false },
)
}
if (showRouteModePicker) {
SingleChoiceDialog(
title = "Route Mode",
items = listOf("Rule" to RouteMode.RULE, "Global" to RouteMode.GLOBAL, "Direct" to RouteMode.DIRECT),
selected = DataStore.routeMode,
onSelect = { DataStore.routeMode = it; showRouteModePicker = false },
onDismiss = { showRouteModePicker = false },
)
}
if (showSpeedIntervalPicker) {
SingleChoiceDialog(
title = "Speed Interval",
items = listOf(
"0.5s" to 500,
"1s" to 1000,
"3s" to 3000,
"10s" to 10000,
"60s" to 60000,
"180s" to 180000,
),
selected = DataStore.speedInterval,
onSelect = { DataStore.speedInterval = it; showSpeedIntervalPicker = false },
onDismiss = { showSpeedIntervalPicker = false },
)
}
if (showOutboundStrategyPicker) {
SingleChoiceDialog(
title = "Resolve Destination",
items = listOf("Disable" to "AsIs", "IPv4 only" to "UseIPv4", "Prefer IPv4" to "PreferIPv4", "IPv4 and IPv6" to "UseIP", "Prefer IPv6" to "PreferIPv6", "IPv6 only" to "UseIPv6"),
selected = DataStore.outboundDomainStrategy,
onSelect = { DataStore.outboundDomainStrategy = it; showOutboundStrategyPicker = false },
onDismiss = { showOutboundStrategyPicker = false },
)
}
if (showOutboundDirectStrategyPicker) {
SingleChoiceDialog(
title = "Resolve Destination (direct)",
items = listOf("Disable" to "AsIs", "IPv4 only" to "UseIPv4", "Prefer IPv4" to "PreferIPv4", "IPv4 and IPv6" to "UseIP", "Prefer IPv6" to "PreferIPv6", "IPv6 only" to "UseIPv6"),
selected = DataStore.outboundDomainStrategyForDirect,
onSelect = { DataStore.outboundDomainStrategyForDirect = it; showOutboundDirectStrategyPicker = false },
onDismiss = { showOutboundDirectStrategyPicker = false },
)
}
if (showOutboundServerStrategyPicker) {
SingleChoiceDialog(
title = "Resolve Destination (server)",
items = listOf("Disable" to "AsIs", "IPv4 only" to "UseIPv4", "Prefer IPv4" to "PreferIPv4", "IPv4 and IPv6" to "UseIP", "Prefer IPv6" to "PreferIPv6", "IPv6 only" to "UseIPv6"),
selected = DataStore.outboundDomainStrategyForServer,
onSelect = { DataStore.outboundDomainStrategyForServer = it; showOutboundServerStrategyPicker = false },
onDismiss = { showOutboundServerStrategyPicker = false },
)
}
if (showRemoteDnsQueryPicker) {
SingleChoiceDialog(
title = "Remote DNS Query Strategy",
items = listOf("IPv4 and IPv6" to "UseIP", "IPv4 only" to "UseIPv4", "IPv6 only" to "UseIPv6"),
selected = DataStore.remoteDnsQueryStrategy,
onSelect = { DataStore.remoteDnsQueryStrategy = it; showRemoteDnsQueryPicker = false },
onDismiss = { showRemoteDnsQueryPicker = false },
)
}
if (showDirectDnsQueryPicker) {
SingleChoiceDialog(
title = "Direct DNS Query Strategy",
items = listOf("IPv4 and IPv6" to "UseIP", "IPv4 only" to "UseIPv4", "IPv6 only" to "UseIPv6"),
selected = DataStore.directDnsQueryStrategy,
onSelect = { DataStore.directDnsQueryStrategy = it; showDirectDnsQueryPicker = false },
onDismiss = { showDirectDnsQueryPicker = false },
)
}
if (showRulesProviderPicker) {
SingleChoiceDialog(
title = "Route Assets Provider",
items = listOf("v2fly" to 0, "Loyalsoldier/v2ray-rules-dat" to 1, "Chocolate4U/Iran-v2ray-rules" to 2, "Custom" to 3),
selected = DataStore.rulesProvider,
onSelect = { DataStore.rulesProvider = it; showRulesProviderPicker = false },
onDismiss = { showRulesProviderPicker = false },
)
}
if (showFabStylePicker) {
SingleChoiceDialog(
title = "FAB Style",
items = listOf("SagerNet" to 0, "Shadowsocks" to 1),
selected = DataStore.fabStyle,
onSelect = { DataStore.fabStyle = it; showFabStylePicker = false },
onDismiss = { showFabStylePicker = false },
)
}
if (showFragmentMethodPicker) {
SingleChoiceDialog(
title = "Fragmentation Method",
items = listOf("TLS record" to 0, "TCP segmentation" to 1, "TLS + TCP" to 2),
selected = DataStore.fragmentMethod,
onSelect = { DataStore.fragmentMethod = it; showFragmentMethodPicker = false },
onDismiss = { showFragmentMethodPicker = false },
)
}
if (showTextEditDialog) {
io.nekohasekai.sagernet.ui.compose.components.ExpressiveDialog(onDismissRequest = { showTextEditDialog = false }) {
Text(
text = editingText.first,
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(bottom = 16.dp),
)
io.nekohasekai.sagernet.ui.compose.components.ExpressiveTextField(
value = editingTextValue,
onValueChange = { editingTextValue = it },
singleLine = editingTextKey != "hosts" && editingTextKey != "httpProxyException",
)
Spacer(Modifier.height(16.dp))
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
TextButton(onClick = { showTextEditDialog = false }) { Text("Cancel") }
Spacer(Modifier.width(8.dp))
TextButton(onClick = {
when (editingTextKey) {
"connectionTestURL" -> DataStore.connectionTestURL = editingTextValue
"remoteDns" -> DataStore.remoteDns = editingTextValue
"directDns" -> DataStore.directDns = editingTextValue
"bootstrapDns" -> DataStore.bootstrapDns = editingTextValue
"ednsClientIp" -> DataStore.ednsClientIp = editingTextValue
"hosts" -> DataStore.hosts = editingTextValue
"socksUsername" -> DataStore.socksUsername = editingTextValue
"socksPassword" -> DataStore.socksPassword = editingTextValue
"httpUsername" -> DataStore.httpUsername = editingTextValue
"httpPassword" -> DataStore.httpPassword = editingTextValue
"httpProxyException" -> DataStore.httpProxyException = editingTextValue
"socksPort" -> DataStore.socksPort = editingTextValue.toIntOrNull() ?: 2080
"httpPort" -> DataStore.httpPort = editingTextValue.toIntOrNull() ?: 9080
"transproxyPort" -> DataStore.transproxyPort = editingTextValue.toIntOrNull() ?: 9200
"localDNSPort" -> DataStore.localDNSPort = editingTextValue.toIntOrNull() ?: 6450
"stunServers" -> DataStore.stunServers = editingTextValue
"pprofServer" -> DataStore.pprofServer = editingTextValue
"experimentalFlags" -> DataStore.experimentalFlags = editingTextValue
"rulesGeositeUrl" -> DataStore.rulesGeositeUrl = editingTextValue
"rulesGeoipUrl" -> DataStore.rulesGeoipUrl = editingTextValue
"socksProxyChainHost" -> DataStore.socksProxyChainHost = editingTextValue
"socksProxyChainPort" -> DataStore.socksProxyChainPort = editingTextValue.toIntOrNull() ?: 0
"socksProxyChainUsername" -> DataStore.socksProxyChainUsername = editingTextValue
"socksProxyChainPassword" -> DataStore.socksProxyChainPassword = editingTextValue
"subscriptionAutoUpdateDelay" -> {}
}
showTextEditDialog = false
}) { Text("OK") }
}
}
}
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState())
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
containerColor = MaterialTheme.colorScheme.surfaceContainer,
topBar = {
OwenclaveTopAppBar(
title = "Settings",
navigationIcon = Icons.Filled.Menu,
onNavigationClick = onMenuClick,
scrollBehavior = scrollBehavior,
)
},
) { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.verticalScroll(rememberScrollState())
) {
Column(modifier = Modifier.padding(bottom = io.nekohasekai.sagernet.ui.compose.components.StatsBarBottomInset)) {
// ── General ──
PreferenceHeader("General")
PreferenceGroup {
item { shape ->
SwitchPreferenceItem(
title = "Auto Connect",
subtitle = "Restore connection after device boot",
checked = persistAcrossReboot,
onCheckedChange = { persistAcrossReboot = it; DataStore.persistAcrossReboot = it },
shape = shape,
)
}
item { shape ->
PreferenceItem(
title = "Theme",
subtitle = "Choose app color theme",
icon = Icons.Filled.Palette,
onClick = { showThemePicker = true },
shape = shape,
)
}
item { shape ->
PreferenceItem(
title = "App Logo",
subtitle = io.nekohasekai.sagernet.ui.compose.components.AppLogoStyle.fromId(currentLogo).label,
icon = Icons.Filled.Face,
onClick = { showLogoPicker = true },
shape = shape,
)
}
item { shape ->
PreferenceItem(
title = "Night Mode",
subtitle = when (DataStore.nightTheme) { 0 -> "Follow system"; 1 -> "Always dark"; 2 -> "Always light"; else -> "Auto" },
icon = Icons.Filled.WbSunny,
onClick = { showNightModePicker = true },
shape = shape,
)
}
item { shape ->
PreferenceItem(
title = "Language",
subtitle = "System default",
icon = Icons.Filled.Translate,
onClick = {
try {
val intent = Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
.setData(Uri.parse("package:" + context.packageName))
context.startActivity(intent)
} catch (_: Exception) {
}
},
shape = shape,
)
}
item { shape ->
PreferenceItem(
title = "Service Mode",
subtitle = DataStore.serviceMode,
icon = Icons.Filled.Dashboard,
onClick = { showServiceModePicker = true },
shape = shape,
)
}
item { shape ->
PreferenceItem(
title = "TUN Implementation",
subtitle = if (DataStore.tunImplementation == TunImplementation.GVISOR) "gVisor" else "System",
icon = Icons.Filled.Lan,
onClick = { showTunPicker = true },
shape = shape,
)
}
item { shape ->
PreferenceItem(
title = "MTU",
subtitle = "${DataStore.mtu}",
icon = Icons.Filled.Public,
onClick = { showMtuPicker = true },
shape = shape,
)
}
item { shape ->
SwitchPreferenceItem(
title = "Metered Network",
subtitle = "Treat VPN as metered",
icon = Icons.Filled.Wifi,
checked = meteredNetwork,
onCheckedChange = { meteredNetwork = it; DataStore.meteredNetwork = it },
shape = shape,
)
}
item { shape ->
SwitchPreferenceItem(
title = "Enable PCAP",
subtitle = "Capture packets to file",
icon = Icons.Filled.Construction,
checked = enablePcap,
onCheckedChange = { enablePcap = it; DataStore.enablePcap = it },
shape = shape,
)
}
item { shape ->
SwitchPreferenceItem(
title = "Discard ICMP",
checked = discardICMP,
onCheckedChange = { discardICMP = it; DataStore.discardICMP = it },
shape = shape,
)
}
item { shape ->
SwitchPreferenceItem(
title = "App Traffic Statistics",
icon = Icons.Filled.Speed,
checked = appTrafficStats,
onCheckedChange = { appTrafficStats = it; DataStore.appTrafficStatistics = it },
shape = shape,
)
}
item { shape ->
SwitchPreferenceItem(
title = "Profile Traffic Statistics",
checked = profileTrafficStats,
onCheckedChange = { profileTrafficStats = it; DataStore.profileTrafficStatistics = it },
shape = shape,
)
}
item { shape ->
SwitchPreferenceItem(
title = "Show Direct Speed",
checked = showDirectSpeed,
onCheckedChange = { showDirectSpeed = it; DataStore.showDirectSpeed = it },
shape = shape,
)
}
item { shape ->
PreferenceItem(
title = "Speed Interval",
subtitle = when (DataStore.speedInterval) {
500 -> "0.5s"
1000 -> "1s"
3000 -> "3s"
10000 -> "10s"
60000 -> "60s"
180000 -> "180s"
else -> "${DataStore.speedInterval}ms"
},
icon = Icons.Filled.Speed,
onClick = { showSpeedIntervalPicker = true },
shape = shape,
)
}
item { shape ->
PreferenceItem(
title = "Log Level",
subtitle = when (DataStore.logLevel) { 0 -> "None"; 1 -> "Error"; 2 -> "Warning"; 3 -> "Info"; 4 -> "Debug"; else -> "Unknown" },
icon = Icons.Filled.Tune,
onClick = { showLogLevelPicker = true },
shape = shape,
)
}
item { shape ->
PreferenceItem(
title = "Root CA Provider",
subtitle = when (DataStore.providerRootCA) { 0 -> "Mozilla"; 1 -> "System"; 2 -> "System & User"; 3 -> "Custom"; else -> "Unknown" },
icon = Icons.Filled.Security,
onClick = { showRootCaPicker = true },
shape = shape,
)
}
}
// ── Route ──
PreferenceHeader("Route")
PreferenceGroup {
item { shape ->
SwitchPreferenceItem(
title = "IPv6 Address on VPN Interface",
checked = enableVPNIPv6,
onCheckedChange = { enableVPNIPv6 = it; DataStore.enableVPNInterfaceIPv6Address = it },
shape = shape,
)
}
item { shape ->
PreferenceItem(
title = "Per-App Proxy",
subtitle = if (proxyApps) {
if (DataStore.bypass) "Bypass mode" else "Proxy mode"
} else {
"Disabled"
},
icon = Icons.Filled.Apps,
onClick = {
proxyApps = true; DataStore.proxyApps = true
context.startActivity(Intent(context, io.nekohasekai.sagernet.ui.compose.ComposeAppListActivity::class.java))
},
shape = shape,
)
}
item { shape ->
SwitchPreferenceItem(
title = "Allow Apps to Bypass VPN",
checked = allowAppsBypassVpn,
onCheckedChange = { allowAppsBypassVpn = it; DataStore.allowAppsBypassVpn = it },
shape = shape,
)
}
item { shape ->
SwitchPreferenceItem(
title = "Bypass LAN",
checked = bypassLan,
onCheckedChange = { bypassLan = it; DataStore.bypassLan = it },
shape = shape,
)
}
item { shape ->
SwitchPreferenceItem(
title = "Traffic Sniffing",
checked = trafficSniffing,
onCheckedChange = { trafficSniffing = it; DataStore.trafficSniffing = it },
shape = shape,
)
}
item { shape ->
SwitchPreferenceItem(
title = "Destination Override",
checked = destinationOverride,
onCheckedChange = { destinationOverride = it; DataStore.destinationOverride = it },
shape = shape,
)
}
item { shape ->
SwitchPreferenceItem(
title = "Hijack DNS",
checked = hijackDns,
onCheckedChange = { hijackDns = it; DataStore.hijackDns = it },
shape = shape,
)
}
item { shape ->
PreferenceItem(
title = "Domain Strategy",
subtitle = DataStore.domainStrategy,
onClick = { showDomainStrategyPicker = true },
shape = shape,
)
}
item { shape ->
PreferenceItem(
title = "Resolve Destination",
subtitle = DataStore.outboundDomainStrategy,
onClick = { showOutboundStrategyPicker = true },
shape = shape,
)
}
item { shape ->
PreferenceItem(
title = "Resolve Destination (direct)",
subtitle = DataStore.outboundDomainStrategyForDirect,
onClick = { showOutboundDirectStrategyPicker = true },
shape = shape,
)
}
item { shape ->
PreferenceItem(
title = "Resolve Destination (server)",
subtitle = DataStore.outboundDomainStrategyForServer,
onClick = { showOutboundServerStrategyPicker = true },
shape = shape,
)
}
item { shape ->
PreferenceItem(
title = "Route Assets Provider",
subtitle = when (DataStore.rulesProvider) { 0 -> "v2fly"; 1 -> "Loyalsoldier"; 2 -> "Iran-v2ray-rules"; 3 -> "Custom"; else -> "Unknown" },
onClick = { showRulesProviderPicker = true },
shape = shape,
)
}
item { shape ->
PreferenceItem(
title = "Route Mode",
subtitle = when (DataStore.routeMode) { 0 -> "Rule"; 1 -> "Global"; 2 -> "Direct"; else -> "Unknown" },
icon = Icons.Filled.Route,
onClick = { showRouteModePicker = true },
shape = shape,
)
}
}
// ── Protocol ──
PreferenceHeader("Protocol")
PreferenceGroup {
item { shape ->
PreferenceItem(
title = "Connection Test URL",
subtitle = DataStore.connectionTestURL,
icon = Icons.Filled.Public,
onClick = {
editingTextKey = "connectionTestURL"
editingText = Pair("Connection Test URL", DataStore.connectionTestURL)
editingTextValue = DataStore.connectionTestURL
showTextEditDialog = true
},
shape = shape,
)
}
item { shape ->
SwitchPreferenceItem(
title = "SOCKS Proxy Chain",
subtitle = "Route SOCKS through another proxy",
checked = socksProxyChain,
onCheckedChange = { socksProxyChain = it; DataStore.socksProxyChainEnabled = it },
shape = shape,
)
}
if (socksProxyChain) {
item { shape ->
PreferenceItem(
title = "Chain Host",
subtitle = DataStore.socksProxyChainHost ?: "None",
onClick = {
editingTextKey = "socksProxyChainHost"
editingTextValue = DataStore.socksProxyChainHost ?: ""
editingText = Pair("Chain Host", editingTextValue)
showTextEditDialog = true
},
shape = shape,
)
}
item { shape ->
PreferenceItem(
title = "Chain Port",
subtitle = "${DataStore.socksProxyChainPort}",
onClick = {
editingTextKey = "socksProxyChainPort"
editingTextValue = DataStore.socksProxyChainPort.toString()
editingText = Pair("Chain Port", editingTextValue)
showTextEditDialog = true
},
shape = shape,
)
}
item { shape ->
PreferenceItem(
title = "Chain Username",
subtitle = DataStore.socksProxyChainUsername ?: "None",
onClick = {
editingTextKey = "socksProxyChainUsername"
editingTextValue = DataStore.socksProxyChainUsername ?: ""
editingText = Pair("Chain Username", editingTextValue)
showTextEditDialog = true
},
shape = shape,
)
}
item { shape ->
PreferenceItem(
title = "Chain Password",
subtitle = if (DataStore.socksProxyChainPassword.isNullOrEmpty()) "None" else "****",
onClick = {
editingTextKey = "socksProxyChainPassword"
editingTextValue = DataStore.socksProxyChainPassword ?: ""
editingText = Pair("Chain Password", editingTextValue)
showTextEditDialog = true
},
shape = shape,
)
}
}
item { shape ->
SwitchPreferenceItem(
title = "Enable TWPS2 (zapret2)",
subtitle = "Global DPI bypass using twps2",
checked = enableTwps2,
onCheckedChange = { enableTwps2 = it; DataStore.enableTwps2 = it },
shape = shape,
)
}
item { shape ->
SwitchPreferenceItem(
title = "Unlock AI and EN Services (Russia)",
checked = enableUnlockRu,
onCheckedChange = { enableUnlockRu = it; DataStore.enableUnlockRu = it },
shape = shape,
)
}
item { shape ->
SwitchPreferenceItem(
title = "Direct Proxy Mode",
subtitle = "Direct traffic through device using zapret2",
checked = directProxyMode,
onCheckedChange = { directProxyMode = it; DataStore.directProxyMode = it },
shape = shape,
)
}
item { shape ->
SwitchPreferenceItem(
title = "Disable REALITY X25519MLKEM768",
checked = realityDisableX25519Mlkem768,
onCheckedChange = { realityDisableX25519Mlkem768 = it; DataStore.realityDisableX25519Mlkem768 = it },
shape = shape,
)
}
item { shape ->
SwitchPreferenceItem(
title = "Hysteria 2 OmitMaxDatagramFrameSize",
checked = hysteria2OmitMaxDatagramFrameSize,
onCheckedChange = { hysteria2OmitMaxDatagramFrameSize = it; DataStore.hysteria2OmitMaxDatagramFrameSize = it },
shape = shape,
)
}
item { shape ->
SwitchPreferenceItem(
title = "gRPC Service Name Compatibility",
checked = grpcServiceNameCompat,
onCheckedChange = { grpcServiceNameCompat = it; DataStore.grpcServiceNameCompat = it },
shape = shape,
)
}
item { shape ->
SwitchPreferenceItem(
title = "Enable TLS Fragment",
subtitle = "May help circumvent SNI censorship",
checked = enableFragment,
onCheckedChange = { enableFragment = it; DataStore.enableFragment = it },
shape = shape,
)
}
if (enableFragment) {
item { shape ->
PreferenceItem(
title = "Fragmentation Method",
subtitle = when (DataStore.fragmentMethod) { 0 -> "TLS record"; 1 -> "TCP segmentation"; 2 -> "TLS + TCP"; else -> "Unknown" },
onClick = { showFragmentMethodPicker = true },
shape = shape,
)
}
item { shape ->
SwitchPreferenceItem(
title = "Fragment for Direct",
checked = enableFragmentForDirect,
onCheckedChange = { enableFragmentForDirect = it; DataStore.enableFragmentForDirect = it },
shape = shape,
)
}
}
item { shape ->
SwitchPreferenceItem(
title = "Interrupt Reused Connections",
checked = interruptReusedConnections,
onCheckedChange = { interruptReusedConnections = it; DataStore.interruptReusedConnections = it },
shape = shape,
)
}
item { shape ->
SwitchPreferenceItem(
title = "Profile Security Advisory",
checked = profileSecurityAdvisory,
onCheckedChange = { profileSecurityAdvisory = it; DataStore.profileSecurityAdvisory = it },
shape = shape,
)
}
}
// ── DNS ──
PreferenceHeader("DNS")
PreferenceGroup {
item { shape ->
PreferenceItem(
title = "Remote DNS",
subtitle = DataStore.remoteDns,
icon = Icons.Filled.Dns,
onClick = {
editingTextKey = "remoteDns"
editingTextValue = DataStore.remoteDns
editingText = Pair("Remote DNS", editingTextValue)
showTextEditDialog = true
},
shape = shape,
)
}
item { shape ->
PreferenceItem(
title = "Remote DNS Query Strategy",
subtitle = DataStore.remoteDnsQueryStrategy,
onClick = { showRemoteDnsQueryPicker = true },
shape = shape,
)
}
item { shape ->
PreferenceItem(
title = "EDNS Client IP",
subtitle = DataStore.ednsClientIp ?: "None",
onClick = {
editingTextKey = "ednsClientIp"
editingTextValue = DataStore.ednsClientIp ?: ""
editingText = Pair("EDNS Client IP", editingTextValue)
showTextEditDialog = true
},
shape = shape,
)
}
item { shape ->
PreferenceItem(
title = "Direct DNS",
subtitle = DataStore.directDns,
onClick = {
editingTextKey = "directDns"
editingTextValue = DataStore.directDns
editingText = Pair("Direct DNS", editingTextValue)
showTextEditDialog = true
},
shape = shape,
)
}
item { shape ->
PreferenceItem(
title = "Direct DNS Query Strategy",
subtitle = DataStore.directDnsQueryStrategy,
onClick = { showDirectDnsQueryPicker = true },
shape = shape,
)
}
item { shape ->
PreferenceItem(
title = "Bootstrap DNS",
subtitle = DataStore.bootstrapDns ?: "Auto",
onClick = {
editingTextKey = "bootstrapDns"
editingTextValue = DataStore.bootstrapDns ?: ""
editingText = Pair("Bootstrap DNS", editingTextValue)
showTextEditDialog = true
},
shape = shape,
)
}
item { shape ->
PreferenceItem(
title = "Domain Rewriting",
subtitle = DataStore.hosts ?: "None",
onClick = {
editingTextKey = "hosts"
editingTextValue = DataStore.hosts ?: ""
editingText = Pair("Domain Rewriting", editingTextValue)
showTextEditDialog = true
},
shape = shape,
)
}
item { shape ->
SwitchPreferenceItem(
title = "Use Local DNS as Direct DNS",
checked = useLocalDnsAsDirectDns,
onCheckedChange = { useLocalDnsAsDirectDns = it; DataStore.useLocalDnsAsDirectDns = it },
shape = shape,
)
}
item { shape ->
SwitchPreferenceItem(
title = "Use Local DNS as Bootstrap DNS",
checked = useLocalDnsAsBootstrapDns,
onCheckedChange = { useLocalDnsAsBootstrapDns = it; DataStore.useLocalDnsAsBootstrapDns = it },
shape = shape,
)
}
item { shape ->
SwitchPreferenceItem(
title = "Enable DNS Routing",
checked = enableDnsRouting,
onCheckedChange = { enableDnsRouting = it; DataStore.enableDnsRouting = it },
shape = shape,
)
}
item { shape ->
SwitchPreferenceItem(
title = "Enable FakeDNS",
checked = enableFakeDns,
onCheckedChange = { enableFakeDns = it; DataStore.enableFakeDns = it },
shape = shape,
)
}
}
// ── Inbound ──
PreferenceHeader("Inbound")
PreferenceGroup {
item { shape ->
SwitchPreferenceItem(
title = "Require SOCKS",
checked = requireSocks,
onCheckedChange = { requireSocks = it; DataStore.requireSocks = it },
shape = shape,
)
}
if (requireSocks) {
item { shape ->
SwitchPreferenceItem(
title = "SOCKS UDP",
checked = socksUDP,
onCheckedChange = { socksUDP = it; DataStore.socksUDP = it },
shape = shape,
)
}
}
item { shape ->
PreferenceItem(
title = "SOCKS Port",
subtitle = "${DataStore.socksPort}",
icon = Icons.Filled.Cable,
onClick = {
editingTextKey = "socksPort"
editingTextValue = DataStore.socksPort.toString()
editingText = Pair("SOCKS Port", editingTextValue)
showTextEditDialog = true
},
shape = shape,
)
}
item { shape ->
PreferenceItem(
title = "SOCKS Username",
subtitle = DataStore.socksUsername ?: "None",
onClick = {
editingTextKey = "socksUsername"
editingTextValue = DataStore.socksUsername ?: ""
editingText = Pair("SOCKS Username", editingTextValue)
showTextEditDialog = true
},
shape = shape,
)
}
item { shape ->
PreferenceItem(
title = "SOCKS Password",
subtitle = if (DataStore.socksPassword.isNullOrEmpty()) "None" else "****",
onClick = {
editingTextKey = "socksPassword"
editingTextValue = DataStore.socksPassword ?: ""
editingText = Pair("SOCKS Password", editingTextValue)
showTextEditDialog = true
},
shape = shape,
)
}
item { shape ->
SwitchPreferenceItem(
title = "Require HTTP",
checked = requireHttp,
onCheckedChange = { requireHttp = it; DataStore.requireHttp = it },
shape = shape,
)
}
if (requireHttp) {
item { shape ->
SwitchPreferenceItem(
title = "Append HTTP Proxy",
checked = appendHttpProxy,
onCheckedChange = { appendHttpProxy = it; DataStore.appendHttpProxy = it },
shape = shape,
)
}
}
item { shape ->
PreferenceItem(
title = "HTTP Port",
subtitle = "${DataStore.httpPort}",
onClick = {
editingTextKey = "httpPort"
editingTextValue = DataStore.httpPort.toString()
editingText = Pair("HTTP Port", editingTextValue)
showTextEditDialog = true
},
shape = shape,
)
}
item { shape ->
PreferenceItem(
title = "HTTP Username",
subtitle = DataStore.httpUsername ?: "None",
onClick = {
editingTextKey = "httpUsername"
editingTextValue = DataStore.httpUsername ?: ""
editingText = Pair("HTTP Username", editingTextValue)
showTextEditDialog = true
},
shape = shape,
)
}
item { shape ->
PreferenceItem(
title = "HTTP Password",
subtitle = if (DataStore.httpPassword.isNullOrEmpty()) "None" else "****",
onClick = {
editingTextKey = "httpPassword"
editingTextValue = DataStore.httpPassword ?: ""
editingText = Pair("HTTP Password", editingTextValue)
showTextEditDialog = true
},
shape = shape,
)
}
if (requireHttp) {
item { shape ->
PreferenceItem(
title = "HTTP Proxy Exception",
subtitle = DataStore.httpProxyException ?: "None",
onClick = {
editingTextKey = "httpProxyException"
editingTextValue = DataStore.httpProxyException ?: ""
editingText = Pair("HTTP Proxy Exception", editingTextValue)
showTextEditDialog = true
},
shape = shape,
)
}
}
item { shape ->
SwitchPreferenceItem(
title = "Require Transproxy",
checked = requireTransproxy,
onCheckedChange = { requireTransproxy = it; DataStore.requireTransproxy = it },
shape = shape,
)
}
if (requireTransproxy) {
item { shape ->
PreferenceItem(
title = "Transproxy Port",
subtitle = "${DataStore.transproxyPort}",
onClick = {
editingTextKey = "transproxyPort"
editingTextValue = DataStore.transproxyPort.toString()
editingText = Pair("Transproxy Port", editingTextValue)
showTextEditDialog = true
},
shape = shape,
)
}
}
item { shape ->
SwitchPreferenceItem(
title = "Require DNS Inbound",
checked = requireDnsInbound,
onCheckedChange = { requireDnsInbound = it; DataStore.requireDnsInbound = it },
shape = shape,
)
}
item { shape ->
PreferenceItem(
title = "Local DNS Port",
subtitle = "${DataStore.localDNSPort}",
onClick = {
editingTextKey = "localDNSPort"
editingTextValue = DataStore.localDNSPort.toString()
editingText = Pair("Local DNS Port", editingTextValue)
showTextEditDialog = true
},
shape = shape,
)
}
item { shape ->
SwitchPreferenceItem(
title = "Allow Access from Other Devices",
checked = allowAccess,
onCheckedChange = { allowAccess = it; DataStore.allowAccess = it },
shape = shape,
)
}
}
// ── Misc ──
PreferenceHeader("Misc")
PreferenceGroup {
item { shape ->
SwitchPreferenceItem(
title = "Show Group Name",
checked = showGroupName,
onCheckedChange = { showGroupName = it; DataStore.showGroupName = it },
shape = shape,
)
}
item { shape ->
SwitchPreferenceItem(
title = "Always Show Address",
checked = alwaysShowAddress,
onCheckedChange = { alwaysShowAddress = it; DataStore.alwaysShowAddress = it },
shape = shape,
)
}
item { shape ->
SwitchPreferenceItem(
title = "Acquire Wake Lock",
checked = acquireWakeLock,
onCheckedChange = { acquireWakeLock = it; DataStore.acquireWakeLock = it },
shape = shape,
)
}
item { shape ->
SwitchPreferenceItem(
title = "Use IEC Unit",
checked = useIECUnit,
onCheckedChange = { useIECUnit = it; DataStore.useIECUnit = it },
shape = shape,
)
}
item { shape ->
SwitchPreferenceItem(
title = "Query All Packages (Alternative Method)",
checked = queryAllPackagesAlternativeMethod,
onCheckedChange = { queryAllPackagesAlternativeMethod = it; DataStore.queryAllPackagesAlternativeMethod = it },
shape = shape,
)
}
item { shape ->
PreferenceItem(
title = "STUN Servers",
subtitle = DataStore.stunServers ?: "None",
onClick = {
editingTextKey = "stunServers"
editingTextValue = DataStore.stunServers ?: ""
editingText = Pair("STUN Servers", editingTextValue)
showTextEditDialog = true
},
shape = shape,
)
}
item { shape ->
PreferenceItem(
title = "pprof Server",
subtitle = DataStore.pprofServer ?: "None",
onClick = {
editingTextKey = "pprofServer"
editingTextValue = DataStore.pprofServer ?: ""
editingText = Pair("pprof Server", editingTextValue)
showTextEditDialog = true
},
shape = shape,
)
}
item { shape ->
PreferenceItem(
title = "FAB Style",
subtitle = when (DataStore.fabStyle) { 0 -> "SagerNet"; 1 -> "Shadowsocks"; else -> "Unknown" },
onClick = { showFabStylePicker = true },
shape = shape,
)
}
item { shape ->
PreferenceItem(
title = "geosite.dat URL",
subtitle = DataStore.rulesGeositeUrl,
onClick = {
editingTextKey = "rulesGeositeUrl"
editingTextValue = DataStore.rulesGeositeUrl
editingText = Pair("geosite.dat URL", editingTextValue)
showTextEditDialog = true
},
shape = shape,
)
}
item { shape ->
PreferenceItem(
title = "geoip.dat URL",
subtitle = DataStore.rulesGeoipUrl,
onClick = {
editingTextKey = "rulesGeoipUrl"
editingTextValue = DataStore.rulesGeoipUrl
editingText = Pair("geoip.dat URL", editingTextValue)
showTextEditDialog = true
},
shape = shape,
)
}
item { shape ->
PreferenceItem(
title = "Experimental Flags",
subtitle = DataStore.experimentalFlags ?: "None",
onClick = {
editingTextKey = "experimentalFlags"
editingTextValue = DataStore.experimentalFlags ?: ""
editingText = Pair("Experimental Flags", editingTextValue)
showTextEditDialog = true
},
shape = shape,
)
}
}
}
}
}
}
@Composable
fun SingleChoiceDialog(
title: String,
items: List>,
selected: T,
onSelect: (T) -> Unit,
onDismiss: () -> Unit,
) {
io.nekohasekai.sagernet.ui.compose.components.ExpressiveDialog(onDismissRequest = onDismiss) {
Text(
text = title,
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(bottom = 16.dp),
)
Column(
modifier = Modifier
.heightIn(max = 420.dp)
.clip(io.nekohasekai.sagernet.ui.compose.components.GroupContainerShape)
.verticalScroll(rememberScrollState()),
verticalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(2.dp),
) {
items.forEachIndexed { index, (label, value) ->
val isSelected = value == selected
androidx.compose.material3.Surface(
onClick = { onSelect(value) },
shape = androidx.compose.foundation.shape.RoundedCornerShape(6.dp),
color = if (isSelected)
MaterialTheme.colorScheme.primaryContainer
else
MaterialTheme.colorScheme.surface,
modifier = Modifier.fillMaxWidth(),
) {
androidx.compose.foundation.layout.Row(
modifier = Modifier.padding(horizontal = 18.dp, vertical = 16.dp),
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically,
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.SpaceBetween,
) {
Text(
text = label,
style = MaterialTheme.typography.titleMedium,
color = if (isSelected)
MaterialTheme.colorScheme.onPrimaryContainer
else
MaterialTheme.colorScheme.onSurface,
modifier = Modifier.weight(1f),
)
if (isSelected) {
Icon(
Icons.Filled.Check,
contentDescription = null,
tint = MaterialTheme.colorScheme.onPrimaryContainer,
)
}
}
}
}
}
Spacer(Modifier.height(12.dp))
androidx.compose.foundation.layout.Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.End,
) {
TextButton(onClick = onDismiss) { Text("Close") }
}
}
}
@Composable
private fun LogoPickerDialog(
selected: Int,
onSelect: (Int) -> Unit,
onDismiss: () -> Unit,
) {
io.nekohasekai.sagernet.ui.compose.components.ExpressiveDialog(onDismissRequest = onDismiss) {
Text(
text = "App Logo",
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(bottom = 4.dp),
)
Text(
text = "Pick a logo shape",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 16.dp),
)
val styles = io.nekohasekai.sagernet.ui.compose.components.AppLogoStyle.entries
Column(
modifier = Modifier
.heightIn(max = 440.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(10.dp),
) {
styles.chunked(3).forEach { rowStyles ->
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(10.dp),
) {
rowStyles.forEach { style ->
val isSelected = style.id == selected
androidx.compose.material3.Surface(
onClick = { onSelect(style.id) },
shape = androidx.compose.foundation.shape.RoundedCornerShape(20.dp),
color = if (isSelected)
MaterialTheme.colorScheme.primaryContainer
else
MaterialTheme.colorScheme.surfaceContainerHigh,
border = if (isSelected)
androidx.compose.foundation.BorderStroke(
2.dp, MaterialTheme.colorScheme.primary,
)
else null,
modifier = Modifier.weight(1f),
) {
Column(
modifier = Modifier.padding(vertical = 14.dp),
horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally,
) {
io.nekohasekai.sagernet.ui.compose.components.AppLogo(
style = style,
size = 56.dp,
)
Spacer(Modifier.height(8.dp))
Text(
text = style.label,
style = MaterialTheme.typography.labelMedium,
color = if (isSelected)
MaterialTheme.colorScheme.onPrimaryContainer
else
MaterialTheme.colorScheme.onSurface,
)
}
}
}
// pad incomplete rows so cells keep equal width
repeat(3 - rowStyles.size) {
Spacer(Modifier.weight(1f))
}
}
}
}
Spacer(Modifier.height(16.dp))
androidx.compose.foundation.layout.Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.End,
) {
TextButton(onClick = onDismiss) { Text("Close") }
}
}
}