Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,26 +1,44 @@
package com.github.kr328.clash.settings.ui

import android.content.Context
import android.content.Intent
import android.net.Uri
import android.net.VpnService
import android.os.PowerManager
import android.provider.Settings
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
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.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewWrapper
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.github.kr328.clash.glue.model.DarkMode
import com.github.kr328.clash.settings.R
import com.github.kr328.clash.settings.vm.AppSettingsViewModel
import com.github.kr328.clash.ui.component.TabbyScaffold
import com.github.kr328.clash.ui.icon.BaselineBatterySaver
import com.github.kr328.clash.ui.icon.BaselineBrightness4
import com.github.kr328.clash.ui.icon.BaselineDomain
import com.github.kr328.clash.ui.icon.BaselineHide
import com.github.kr328.clash.ui.icon.BaselineRestore
import com.github.kr328.clash.ui.icon.BaselineStack
import com.github.kr328.clash.ui.icon.BaselineVpnLock
import com.github.kr328.clash.ui.icon.TabbyIcons
import com.github.kr328.clash.ui.theme.PreviewTabby
import com.github.kr328.clash.ui.theme.TabbyThemeWrapper
Expand All @@ -34,17 +52,63 @@ internal fun AppSettingsScreen(
modifier: Modifier = Modifier,
viewModel: AppSettingsViewModel = viewModel(),
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val clashRunning by viewModel.clashRunning.collectAsStateWithLifecycle()
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
var vpnPermissionGranted by remember(context) { mutableStateOf(isVpnPermissionGranted(context)) }
var batteryOptimizationIgnored by
remember(context) { mutableStateOf(isBatteryOptimizationIgnored(context)) }

val vpnPermissionLauncher =
rememberLauncherForActivityResult(StartActivityForResult()) {
vpnPermissionGranted = isVpnPermissionGranted(context)
}
val batteryOptimizationLauncher =
rememberLauncherForActivityResult(StartActivityForResult()) {
batteryOptimizationIgnored = isBatteryOptimizationIgnored(context)
}

DisposableEffect(context, lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) {
vpnPermissionGranted = isVpnPermissionGranted(context)
batteryOptimizationIgnored = isBatteryOptimizationIgnored(context)
}
}
lifecycleOwner.lifecycle.addObserver(observer)

onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
}

AppSettingsContent(
clashRunning = clashRunning,
uiState = uiState,
vpnPermissionGranted = vpnPermissionGranted,
batteryOptimizationIgnored = batteryOptimizationIgnored,
onAutoRestartChange = viewModel::updateAutoRestart,
onDarkModeChange = viewModel::updateDarkMode,
onHideAppIconChange = viewModel::updateHideAppIcon,
onHideFromRecentsChange = viewModel::updateHideFromRecents,
onDynamicNotificationChange = viewModel::updateDynamicNotification,
onAlwaysOnVpnChange = { enabled ->
if (!enabled) return@AppSettingsContent

val permissionIntent = VpnService.prepare(context)
if (permissionIntent != null) {
vpnPermissionLauncher.launch(permissionIntent)
} else {
context.startActivity(Intent(Settings.ACTION_VPN_SETTINGS))
}
},
onIgnoreBatteryOptimizationChange = { enabled ->
if (!enabled) return@AppSettingsContent

batteryOptimizationLauncher.launch(
Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
.setData(Uri.parse("package:${context.packageName}"))
)
},
modifier = modifier,
)
}
Expand All @@ -53,11 +117,15 @@ internal fun AppSettingsScreen(
private fun AppSettingsContent(
clashRunning: Boolean,
uiState: AppSettingsViewModel.UiState,
vpnPermissionGranted: Boolean,
batteryOptimizationIgnored: Boolean,
onAutoRestartChange: (Boolean) -> Unit,
onDarkModeChange: (DarkMode) -> Unit,
onHideAppIconChange: (Boolean) -> Unit,
onHideFromRecentsChange: (Boolean) -> Unit,
onDynamicNotificationChange: (Boolean) -> Unit,
onAlwaysOnVpnChange: (Boolean) -> Unit,
onIgnoreBatteryOptimizationChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
TabbyScaffold(title = stringResource(R.string.app), modifier = modifier) { innerPadding ->
Expand Down Expand Up @@ -115,6 +183,24 @@ private fun AppSettingsContent(
title = { Text(stringResource(R.string.show_traffic)) },
summary = { Text(stringResource(R.string.show_traffic_summary)) },
)
switchPreference(
key = "always_on_vpn",
value = vpnPermissionGranted,
onValueChange = onAlwaysOnVpnChange,
enabled = !vpnPermissionGranted,
icon = { Icon(imageVector = TabbyIcons.BaselineVpnLock, contentDescription = null) },
title = { Text(stringResource(R.string.always_on_vpn)) },
summary = { Text(stringResource(R.string.always_on_vpn_summary)) },
)
switchPreference(
key = "ignore_battery_optimizations",
value = batteryOptimizationIgnored,
onValueChange = onIgnoreBatteryOptimizationChange,
enabled = !batteryOptimizationIgnored,
icon = { Icon(imageVector = TabbyIcons.BaselineBatterySaver, contentDescription = null) },
title = { Text(stringResource(R.string.ignore_battery_optimizations)) },
summary = { Text(stringResource(R.string.ignore_battery_optimizations_summary)) },
)
}
}
}
Expand Down Expand Up @@ -143,11 +229,15 @@ private fun AppSettingsScreenPreview() {
hideFromRecents = false,
dynamicNotification = true,
),
vpnPermissionGranted = false,
batteryOptimizationIgnored = false,
onAutoRestartChange = {},
onDarkModeChange = {},
onHideAppIconChange = {},
onHideFromRecentsChange = {},
onDynamicNotificationChange = {},
onAlwaysOnVpnChange = {},
onIgnoreBatteryOptimizationChange = {},
)
}

Expand All @@ -165,10 +255,24 @@ private fun AppSettingsScreenRunningPreview() {
hideFromRecents = true,
dynamicNotification = true,
),
vpnPermissionGranted = true,
batteryOptimizationIgnored = true,
onAutoRestartChange = {},
onDarkModeChange = {},
onHideAppIconChange = {},
onHideFromRecentsChange = {},
onDynamicNotificationChange = {},
onAlwaysOnVpnChange = {},
onIgnoreBatteryOptimizationChange = {},
)
}

private fun isVpnPermissionGranted(context: Context): Boolean {
return VpnService.prepare(context) == null
}

private fun isBatteryOptimizationIgnored(context: Context): Boolean {
return context
.getSystemService(PowerManager::class.java)
?.isIgnoringBatteryOptimizations(context.packageName) == true
}
4 changes: 4 additions & 0 deletions ui/settings/src/main/res/values-ja-rJP/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
<string name="always">Always</string>
<string name="always_dark">常にダークモード</string>
<string name="always_light">常にライトモード</string>
<string name="always_on_vpn">常時 VPN 接続</string>
<string name="always_on_vpn_summary">VPN 権限をリクエストするか VPN 設定を開きます</string>
<string name="app">アプリ</string>
<string name="append_system_dns">システムDNSを追加</string>
<string name="authentication">認証</string>
Expand Down Expand Up @@ -80,6 +82,8 @@
<string name="info">情報</string>
<string name="install_time">インストール日時</string>
<string name="interface_">インターフェイス</string>
<string name="ignore_battery_optimizations">電池の最適化を無視</string>
<string name="ignore_battery_optimizations_summary">電池の最適化の対象外にリクエストします</string>
<string name="ipcidr_fallback">IPCIDRフォールバック</string>
<string name="ipv6">IPv6</string>
<string name="key">キー</string>
Expand Down
4 changes: 4 additions & 0 deletions ui/settings/src/main/res/values-ko-rKR/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
<string name="always">강제로 켜기</string>
<string name="always_dark">다크</string>
<string name="always_light">라이트</string>
<string name="always_on_vpn">항상 켜져 있는 VPN</string>
<string name="always_on_vpn_summary">VPN 권한 요청 또는 VPN 설정 열기</string>
<string name="app">앱</string>
<string name="append_system_dns">시스템 DNS 허용</string>
<string name="authentication">인증</string>
Expand Down Expand Up @@ -80,6 +82,8 @@
<string name="info">정보</string>
<string name="install_time">설치 시각</string>
<string name="interface_">테마</string>
<string name="ignore_battery_optimizations">배터리 최적화 무시</string>
<string name="ignore_battery_optimizations_summary">배터리 최적화에서 제외 요청</string>
<string name="ipcidr_fallback">IPCIDR 폴백</string>
<string name="ipv6">IPv6</string>
<string name="key">키</string>
Expand Down
4 changes: 4 additions & 0 deletions ui/settings/src/main/res/values-ru/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
<string name="always">Принудительно включить</string>
<string name="always_dark">Всегда тёмная</string>
<string name="always_light">Всегда светлая</string>
<string name="always_on_vpn">Постоянное VPN-соединение</string>
<string name="always_on_vpn_summary">Запросить разрешение VPN или открыть настройки VPN</string>
<string name="app">Приложение</string>
<string name="append_system_dns">Добавить системный DNS</string>
<string name="authentication">Аутентификация</string>
Expand Down Expand Up @@ -80,6 +82,8 @@
<string name="info">Инфо</string>
<string name="install_time">Время установки</string>
<string name="interface_">Интерфейс</string>
<string name="ignore_battery_optimizations">Игнорировать оптимизацию батареи</string>
<string name="ignore_battery_optimizations_summary">Запросить исключение из оптимизации батареи</string>
<string name="ipcidr_fallback">Резервный IPCIDR</string>
<string name="ipv6">IPv6</string>
<string name="key">Ключ</string>
Expand Down
4 changes: 4 additions & 0 deletions ui/settings/src/main/res/values-vi/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
<string name="always">Luôn luôn</string>
<string name="always_dark">Luôn tối</string>
<string name="always_light">Luôn sáng</string>
<string name="always_on_vpn">VPN luôn bật</string>
<string name="always_on_vpn_summary">Yêu cầu quyền VPN hoặc mở cài đặt VPN</string>
<string name="app">Ứng dụng</string>
<string name="append_system_dns">Nối hệ thống DNS</string>
<string name="authentication">Xác thực</string>
Expand Down Expand Up @@ -80,6 +82,8 @@
<string name="info">Thông tin</string>
<string name="install_time">Thời gian cài đặt</string>
<string name="interface_">Giao diện</string>
<string name="ignore_battery_optimizations">Bỏ qua tối ưu hóa pin</string>
<string name="ignore_battery_optimizations_summary">Yêu cầu loại trừ khỏi tối ưu hóa pin</string>
<string name="ipcidr_fallback">Dự phòng IPCIDR</string>
<string name="ipv6">IPv6</string>
<string name="key">Khoá</string>
Expand Down
4 changes: 4 additions & 0 deletions ui/settings/src/main/res/values-zh-rHK/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
<string name="always">強制開啟</string>
<string name="always_dark">總是暗黑模式</string>
<string name="always_light">總是明亮模式</string>
<string name="always_on_vpn">始終開啟 VPN</string>
<string name="always_on_vpn_summary">請求 VPN 權限或打開 VPN 設置</string>
<string name="app">應用</string>
<string name="append_system_dns">追加系統 DNS</string>
<string name="authentication">認證</string>
Expand Down Expand Up @@ -80,6 +82,8 @@
<string name="info">消息</string>
<string name="install_time">安裝時間</string>
<string name="interface_">界面</string>
<string name="ignore_battery_optimizations">忽略電池優化</string>
<string name="ignore_battery_optimizations_summary">請求將此應用排除在電池優化之外</string>
<string name="ipcidr_fallback">IPCIDR Fallback</string>
<string name="ipv6">IPv6</string>
<string name="key">鍵</string>
Expand Down
4 changes: 4 additions & 0 deletions ui/settings/src/main/res/values-zh-rTW/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
<string name="always">強制開啟</string>
<string name="always_dark">深色</string>
<string name="always_light">淺色</string>
<string name="always_on_vpn">永遠開啟 VPN</string>
<string name="always_on_vpn_summary">請求 VPN 權限或開啟 VPN 設定</string>
<string name="app">應用</string>
<string name="append_system_dns">附加作業系統 DNS</string>
<string name="authentication">認證</string>
Expand Down Expand Up @@ -80,6 +82,8 @@
<string name="info">資訊</string>
<string name="install_time">安裝時間</string>
<string name="interface_">介面</string>
<string name="ignore_battery_optimizations">忽略電池最佳化</string>
<string name="ignore_battery_optimizations_summary">請求將此應用程式排除於電池最佳化之外</string>
<string name="ipcidr_fallback">IPCIDR 後饋</string>
<string name="ipv6">IPv6</string>
<string name="key">鍵</string>
Expand Down
4 changes: 4 additions & 0 deletions ui/settings/src/main/res/values-zh/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
<string name="always">强制开启</string>
<string name="always_dark">总是暗黑模式</string>
<string name="always_light">总是明亮模式</string>
<string name="always_on_vpn">始终开启 VPN</string>
<string name="always_on_vpn_summary">请求 VPN 权限或打开 VPN 设置</string>
<string name="app">应用</string>
<string name="append_system_dns">追加系统 DNS</string>
<string name="authentication">认证</string>
Expand Down Expand Up @@ -80,6 +82,8 @@
<string name="info">消息</string>
<string name="install_time">安装时间</string>
<string name="interface_">界面</string>
<string name="ignore_battery_optimizations">忽略电池优化</string>
<string name="ignore_battery_optimizations_summary">请求将此应用排除在电池优化之外</string>
<string name="ipcidr_fallback">IPCIDR Fallback</string>
<string name="ipv6">IPv6</string>
<string name="key">键</string>
Expand Down
4 changes: 4 additions & 0 deletions ui/settings/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
<string name="always">Always</string>
<string name="always_dark">Always Dark</string>
<string name="always_light">Always Light</string>
<string name="always_on_vpn">Always-on VPN</string>
<string name="always_on_vpn_summary">Request VPN permission or open VPN settings</string>
<string name="app">App</string>
<string name="append_system_dns">Append System DNS</string>
<string name="authentication">Authentication</string>
Expand Down Expand Up @@ -78,6 +80,8 @@
<string name="import_geoip_file">Import GeoIP Database</string>
<string name="import_geosite_file">Import GeoSite Database</string>
<string name="info">Info</string>
<string name="ignore_battery_optimizations">Ignore Battery Optimizations</string>
<string name="ignore_battery_optimizations_summary">Request battery optimization exclusion</string>
<string name="install_time">Install Time</string>
<string name="interface_">Interface</string>
<string name="ipcidr_fallback">IPCIDR Fallback</string>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.github.kr328.clash.ui.icon

import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp

@Suppress("UnusedReceiverParameter")
val TabbyIcons.BaselineBatterySaver: ImageVector
get() {
if (_BaselineBatterySaver != null) {
return _BaselineBatterySaver!!
}
_BaselineBatterySaver =
ImageVector.Builder(
name = "BaselineBatterySaver",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 24f,
viewportHeight = 24f,
)
.apply {
path(fill = SolidColor(Color.White)) {
moveTo(17f, 4f)
horizontalLineToRelative(-3f)
verticalLineTo(2f)
horizontalLineToRelative(-4f)
verticalLineToRelative(2f)
horizontalLineTo(7f)
curveToRelative(-1.1f, 0f, -2f, 0.9f, -2f, 2f)
verticalLineToRelative(15f)
curveToRelative(0f, 1.1f, 0.9f, 2f, 2f, 2f)
horizontalLineToRelative(10f)
curveToRelative(1.1f, 0f, 2f, -0.9f, 2f, -2f)
verticalLineTo(6f)
curveToRelative(0f, -1.1f, -0.9f, -2f, -2f, -2f)
close()
moveTo(16f, 15f)
horizontalLineToRelative(-2f)
verticalLineToRelative(2f)
horizontalLineToRelative(-2f)
verticalLineToRelative(-2f)
horizontalLineToRelative(-2f)
verticalLineToRelative(-2f)
horizontalLineToRelative(2f)
verticalLineToRelative(-2f)
horizontalLineToRelative(2f)
verticalLineToRelative(2f)
horizontalLineToRelative(2f)
verticalLineToRelative(2f)
close()
}
}
.build()

return _BaselineBatterySaver!!
}

@Suppress("ObjectPropertyName") private var _BaselineBatterySaver: ImageVector? = null