Update v1.0.7

This commit is contained in:
amurcanov
2026-04-14 02:25:23 +03:00
parent d1ca465f83
commit a43d61bc38
9 changed files with 693 additions and 394 deletions

View File

@@ -65,9 +65,13 @@ class MainActivity : ComponentActivity() {
val settingsStore = remember { SettingsStore(context) }
val themeMode by settingsStore.themeMode
.collectAsStateWithLifecycle(initialValue = "system")
val isDynamicColor by settingsStore.isDynamicColor
.collectAsStateWithLifecycle(initialValue = true)
val themePalette by settingsStore.themePalette
.collectAsStateWithLifecycle(initialValue = "indigo")
val scope = rememberCoroutineScope()
TgWsProxyTheme(themeMode = themeMode) {
TgWsProxyTheme(themeMode = themeMode, dynamicColor = isDynamicColor, themePalette = themePalette) {
androidx.compose.runtime.CompositionLocalProvider(
androidx.compose.ui.platform.LocalDensity provides androidx.compose.ui.unit.Density(
density = androidx.compose.ui.platform.LocalDensity.current.density,
@@ -85,6 +89,14 @@ class MainActivity : ComponentActivity() {
currentTheme = themeMode,
onThemeChange = { mode ->
scope.launch { settingsStore.saveThemeMode(mode) }
},
isDynamicColor = isDynamicColor,
onDynamicColorChange = { dc ->
scope.launch { settingsStore.saveDynamicColor(dc) }
},
currentPalette = themePalette,
onPaletteChange = { pal ->
scope.launch { settingsStore.saveThemePalette(pal) }
}
)
}
@@ -127,8 +139,9 @@ fun MainContent(settingsStore: SettingsStore) {
)
}
LaunchedEffect(Unit) {
DisposableEffect(Unit) {
LogManager.startListening()
onDispose { LogManager.stopListening() }
}
Scaffold(
@@ -206,19 +219,21 @@ object LogManager {
if (job?.isActive == true) return
job = CoroutineScope(Dispatchers.IO).launch {
// Start logcat reader coroutine
val readerJob = launch {
val readerJob = launch(Dispatchers.IO) {
try {
val pid = android.os.Process.myPid()
val process = Runtime.getRuntime().exec(
arrayOf("logcat", "-v", "tag", "--pid", pid.toString())
)
val process = ProcessBuilder("logcat", "-v", "tag", "--pid", pid.toString())
.redirectErrorStream(true)
.start()
logcatProcess = process
val reader = BufferedReader(InputStreamReader(process.inputStream), 8192)
while (isActive) {
val line = reader.readLine() ?: break
val entry = parseLine(line) ?: continue
logChannel.trySend(entry) // non-blocking send
process.inputStream.bufferedReader().use { reader ->
while (isActive) {
val line = try { reader.readLine() } catch (e: Exception) { null } ?: break
val entry = parseLine(line) ?: continue
logChannel.trySend(entry)
}
}
} catch (_: Exception) {
} finally {
@@ -259,32 +274,26 @@ object LogManager {
* Merges consecutive duplicates and caps at 50 entries.
*/
private fun applyBatch(current: List<LogEntry>, batch: List<LogEntry>): List<LogEntry> {
// Use a pre-sized ArrayList to avoid re-allocation
val result = ArrayList<LogEntry>(minOf(current.size + batch.size, 50))
result.addAll(current)
val result = ArrayDeque(current)
for (entry in batch) {
var merged = false
val searchDepth = minOf(result.size, 10)
for (i in result.lastIndex downTo result.size - searchDepth) {
for (i in result.indices.reversed().take(searchDepth)) {
if (result[i].message == entry.message) {
val existing = result.removeAt(i)
result.add(existing.copy(count = existing.count + 1))
result.addLast(existing.copy(count = existing.count + 1))
merged = true
break
}
}
if (!merged) {
result.add(entry)
result.addLast(entry)
}
}
// Trim to 50 entries from the end
return if (result.size > 50) {
result.subList(result.size - 50, result.size).toList()
} else {
result
while (result.size > 50) {
result.removeFirst()
}
return result.toList()
}
fun stopListening() {

View File

@@ -14,6 +14,8 @@ interface ProxyLibrary : Library {
fun SetPoolSize(size: Int)
fun SetCfProxyConfig(enabled: Int, priority: Int, userDomain: String)
fun SetSecret(secret: String)
fun SetFakeTls(enabled: Int, domain: String)
fun GetSecretWithPrefix(): Pointer?
fun GetStats(): Pointer?
fun FreeString(p: Pointer)
}
@@ -38,6 +40,16 @@ object NativeProxy {
fun setSecret(secret: String) {
ProxyLibrary.INSTANCE.SetSecret(secret)
}
fun setFakeTls(enabled: Boolean, domain: String = "") {
ProxyLibrary.INSTANCE.SetFakeTls(if (enabled) 1 else 0, domain)
}
/** Returns the full secret with correct prefix (dd or ee+domain_hex) */
fun getSecretWithPrefix(): String? {
val ptr = ProxyLibrary.INSTANCE.GetSecretWithPrefix() ?: return null
val res = ptr.getString(0)
ProxyLibrary.INSTANCE.FreeString(ptr)
return res
}
fun getStats(): String? {
val ptr = ProxyLibrary.INSTANCE.GetStats() ?: return null
val res = ptr.getString(0)

View File

@@ -20,6 +20,7 @@ class ProxyService : Service() {
private var wakeLock: PowerManager.WakeLock? = null
private var statsJob: Job? = null
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
companion object {
const val ACTION_START = "com.amurcanov.tgwsproxy.START"
@@ -87,7 +88,7 @@ class ProxyService : Service() {
_isRunning.value = true
statsJob = CoroutineScope(Dispatchers.IO).launch {
statsJob = serviceScope.launch {
while (isActive) {
delay(2000)
if (_isRunning.value) {
@@ -197,6 +198,7 @@ class ProxyService : Service() {
}
override fun onDestroy() {
serviceScope.cancel()
if (_isRunning.value) {
stopProxy()
}

View File

@@ -17,22 +17,30 @@ class SettingsStore(private val context: Context) {
private object Keys {
val THEME_MODE = stringPreferencesKey("theme_mode")
val IS_DYNAMIC_COLOR = booleanPreferencesKey("is_dynamic_color")
val THEME_PALETTE = stringPreferencesKey("theme_palette")
val IS_DC_AUTO = booleanPreferencesKey("is_dc_auto")
val DC2 = stringPreferencesKey("dc2")
val DC4 = stringPreferencesKey("dc4")
val PORT = stringPreferencesKey("port")
val POOL_SIZE = intPreferencesKey("pool_size")
val CFPROXY_ENABLED = booleanPreferencesKey("cfproxy_enabled")
val CUSTOM_CF_DOMAIN_ENABLED = booleanPreferencesKey("custom_cf_domain_enabled")
val CUSTOM_CF_DOMAIN = stringPreferencesKey("custom_cf_domain")
val SECRET_KEY = stringPreferencesKey("secret_key")
}
val themeMode: Flow<String> = context.dataStore.data.map { it[Keys.THEME_MODE] ?: "system" }
val isDynamicColor: Flow<Boolean> = context.dataStore.data.map { it[Keys.IS_DYNAMIC_COLOR] ?: true }
val themePalette: Flow<String> = context.dataStore.data.map { it[Keys.THEME_PALETTE] ?: "indigo" }
val isDcAuto: Flow<Boolean> = context.dataStore.data.map { it[Keys.IS_DC_AUTO] ?: true }
val dc2: Flow<String> = context.dataStore.data.map { it[Keys.DC2] ?: "" }
val dc4: Flow<String> = context.dataStore.data.map { it[Keys.DC4] ?: "149.154.167.220" }
val port: Flow<String> = context.dataStore.data.map { it[Keys.PORT] ?: "1443" }
val poolSize: Flow<Int> = context.dataStore.data.map { it[Keys.POOL_SIZE] ?: 4 }
val cfproxyEnabled: Flow<Boolean> = context.dataStore.data.map { it[Keys.CFPROXY_ENABLED] ?: true }
val customCfDomainEnabled: Flow<Boolean> = context.dataStore.data.map { it[Keys.CUSTOM_CF_DOMAIN_ENABLED] ?: false }
val customCfDomain: Flow<String> = context.dataStore.data.map { it[Keys.CUSTOM_CF_DOMAIN] ?: "" }
val secretKey: Flow<String> = context.dataStore.data.map { it[Keys.SECRET_KEY] ?: "" }
suspend fun saveSecretKey(key: String) {
@@ -43,8 +51,16 @@ class SettingsStore(private val context: Context) {
context.dataStore.edit { it[Keys.THEME_MODE] = mode }
}
suspend fun saveDynamicColor(enabled: Boolean) {
context.dataStore.edit { it[Keys.IS_DYNAMIC_COLOR] = enabled }
}
suspend fun saveThemePalette(palette: String) {
context.dataStore.edit { it[Keys.THEME_PALETTE] = palette }
}
suspend fun saveAll(isDcAuto: Boolean, dc2: String, dc4: String, port: String, poolSize: Int,
cfproxyEnabled: Boolean, secretKey: String) {
cfproxyEnabled: Boolean, customCfDomainEnabled: Boolean, customCfDomain: String, secretKey: String) {
context.dataStore.edit {
it[Keys.IS_DC_AUTO] = isDcAuto
it[Keys.DC2] = dc2
@@ -52,6 +68,8 @@ class SettingsStore(private val context: Context) {
it[Keys.PORT] = port
it[Keys.POOL_SIZE] = poolSize
it[Keys.CFPROXY_ENABLED] = cfproxyEnabled
it[Keys.CUSTOM_CF_DOMAIN_ENABLED] = customCfDomainEnabled
it[Keys.CUSTOM_CF_DOMAIN] = customCfDomain
it[Keys.SECRET_KEY] = secretKey
}
}

View File

@@ -24,11 +24,23 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.amurcanov.tgwsproxy.R
import kotlin.math.roundToInt
import androidx.compose.ui.draw.scale
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.ui.draw.clip
import androidx.compose.foundation.border
import androidx.compose.ui.graphics.Color
import android.os.Build
@Composable
fun FloatingToolbar(
currentTheme: String,
onThemeChange: (String) -> Unit,
isDynamicColor: Boolean,
onDynamicColorChange: (Boolean) -> Unit,
currentPalette: String,
onPaletteChange: (String) -> Unit,
modifier: Modifier = Modifier
) {
val configuration = LocalConfiguration.current
@@ -46,7 +58,7 @@ fun FloatingToolbar(
val tabWidthDp = 42.dp
val tabHeightDp = 52.dp
val panelWidthDp = 180.dp
val panelWidthDp = 220.dp
val tabWidthPx = remember(density) { with(density) { tabWidthDp.toPx() } }
@@ -143,6 +155,52 @@ fun FloatingToolbar(
selected = currentTheme == "dark",
onClick = { onThemeChange("dark"); isExpanded = false }
)
Divider(modifier = Modifier.padding(vertical = 4.dp), color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f))
val supportsDynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
val showDynamicColorOn = isDynamicColor && supportsDynamicColor
val showPalettes = !showDynamicColorOn
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 4.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
"Динамические",
style = MaterialTheme.typography.bodySmall,
fontWeight = FontWeight.Medium,
color = if (supportsDynamicColor) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
)
Switch(
checked = showDynamicColorOn,
onCheckedChange = { onDynamicColorChange(it) },
enabled = supportsDynamicColor,
modifier = Modifier.scale(0.8f)
)
}
AnimatedVisibility(visible = showPalettes) {
Column {
Divider(modifier = Modifier.padding(vertical = 4.dp), color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f))
Text(
"Палитра",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 6.dp, start = 4.dp)
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
PaletteCircle("indigo", 0xFF5B588D, currentPalette, onPaletteChange)
PaletteCircle("forest", 0xFF5F5D68, currentPalette, onPaletteChange)
PaletteCircle("espresso", 0xFF6D4C41, currentPalette, onPaletteChange)
}
Spacer(modifier = Modifier.height(6.dp))
}
}
}
}
}
@@ -185,3 +243,23 @@ private fun ThemeOption(
}
}
}
@Composable
fun PaletteCircle(
paletteId: String,
colorHex: Long,
selectedId: String,
onClick: (String) -> Unit
) {
val isSelected = paletteId == selectedId
Box(
modifier = Modifier
.size(30.dp)
.clip(CircleShape)
.background(Color(colorHex))
.clickable { onClick(paletteId) }
.then(
if (isSelected) Modifier.border(3.dp, MaterialTheme.colorScheme.primary, CircleShape)
else Modifier
)
)
}

View File

@@ -125,7 +125,7 @@ private fun isNewerVersion(local: String, remote: String): Boolean {
@Composable
fun InfoTab() {
val currentVersion = "v1.0.6"
val currentVersion = "v1.0.7"
val scope = rememberCoroutineScope()
var updateResult by remember { mutableStateOf<UpdateCheckResult>(UpdateCheckResult.Idle) }
@@ -271,7 +271,8 @@ fun InfoTab() {
}
}
// ═══ Справка ═══
HelpCard()
Spacer(modifier = Modifier.height(16.dp))
}
@@ -320,3 +321,68 @@ private fun GitHubSection(
}
}
}
@Composable
private fun HelpCard() {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
) {
Column(
modifier = Modifier.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(14.dp)
) {
Text(
"Справка",
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold),
color = MaterialTheme.colorScheme.onSurface
)
HelpSection(
title = "Авто / Адреса датацентров",
text = "При включенном CloudFlare сервера (DC) настраивать не нужно — они переадресовываются автоматически. Если вы отключите CloudFlare, нажмите «Настроить адреса DC» для прямого подключения. По умолчанию DC4 зафиксирован на стабильном лондонском узле (149.154.167.220)."
)
Divider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.3f))
HelpSection(
title = "CloudFlare CDN",
text = "Ваш трафик маскируется под HTTPS WebSockets внутри сети Cloudflare. Это способствует лучшему обходу блокировок на мобильных сетях. При использовании Wi-Fi этот режим можно отключать для повышения скорости. Делает блокировку прокси почти невозможной для DPI."
)
Divider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.3f))
HelpSection(
title = "Пул WS",
text = "Механизм многопоточности (фоновые соединения). По умолчанию: 4 потока. Если видео или медиафайлы грузятся медленно, попробуйте увеличить пул."
)
Divider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.3f))
HelpSection(
title = "Секретный ключ",
text = "Специальный 16-байтовый ключ шифрования MTProto. Меняйте его только в случае, если старой ссылкой для подключения завладели посторонние."
)
}
}
}
@Composable
private fun HelpSection(title: String, text: String) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(
title,
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold
)
Text(
text,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
lineHeight = 20.sp
)
}
}

View File

@@ -20,6 +20,9 @@ import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Stop
import androidx.compose.material.icons.filled.VpnKey
import androidx.compose.material.icons.filled.Public
import androidx.compose.material.icons.filled.Layers
import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
@@ -100,6 +103,8 @@ fun SettingsTab(settingsStore: SettingsStore) {
val savedPort by settingsStore.port.collectAsStateWithLifecycle(initialValue = "1443")
val savedPoolSize by settingsStore.poolSize.collectAsStateWithLifecycle(initialValue = 4)
val savedCfEnabled by settingsStore.cfproxyEnabled.collectAsStateWithLifecycle(initialValue = true)
val savedCustomDomainEnabled by settingsStore.customCfDomainEnabled.collectAsStateWithLifecycle(initialValue = false)
val savedCustomDomain by settingsStore.customCfDomain.collectAsStateWithLifecycle(initialValue = "")
val savedSecretKey by settingsStore.secretKey.collectAsStateWithLifecycle(initialValue = "LOADING")
var isDcAuto by rememberSaveable(savedIsDcAuto) { mutableStateOf(savedIsDcAuto) }
@@ -108,6 +113,8 @@ fun SettingsTab(settingsStore: SettingsStore) {
var portText by rememberSaveable(savedPort) { mutableStateOf(savedPort) }
var selectedPoolSize by rememberSaveable(savedPoolSize) { mutableIntStateOf(savedPoolSize) }
var cfEnabled by rememberSaveable(savedCfEnabled) { mutableStateOf(savedCfEnabled) }
var customCfDomainEnabled by rememberSaveable(savedCustomDomainEnabled) { mutableStateOf(savedCustomDomainEnabled) }
var customCfDomain by rememberSaveable(savedCustomDomain) { mutableStateOf(savedCustomDomain) }
var secretKeyText by remember(savedSecretKey) { mutableStateOf(if (savedSecretKey == "LOADING") "" else savedSecretKey) }
LaunchedEffect(savedSecretKey) {
@@ -128,19 +135,16 @@ fun SettingsTab(settingsStore: SettingsStore) {
delay(300)
settingsStore.saveAll(
isDcAuto, dc2Text, dc4Text, portText, selectedPoolSize,
cfEnabled, secretKeyText
cfEnabled, customCfDomainEnabled, customCfDomain, secretKeyText
)
}
}
var showIpSetupDialog by rememberSaveable { mutableStateOf(false) }
var showHelpDialog by rememberSaveable { mutableStateOf(false) }
val scrollState = rememberScrollState()
if (showIpSetupDialog) {
IpSetupDialog(
isDcAuto = isDcAuto,
onModeChange = { isDcAuto = it; scheduleSave() },
dc2Text = dc2Text,
onDc2Change = { dc2Text = it; scheduleSave() },
dc4Text = dc4Text,
@@ -149,17 +153,19 @@ fun SettingsTab(settingsStore: SettingsStore) {
)
}
if (showHelpDialog) {
HelpDialog(onDismiss = { showHelpDialog = false })
}
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = "Настройки",
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold),
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp)
)
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
@@ -167,23 +173,27 @@ fun SettingsTab(settingsStore: SettingsStore) {
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
modifier = Modifier.padding(14.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
"Подключение",
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.SemiBold
)
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(Icons.Default.Public, contentDescription = null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(20.dp))
Text(
"Подключение",
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.SemiBold
)
}
OutlinedTextField(
value = portText,
onValueChange = { portText = it; scheduleSave() },
label = { Text("Порт") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true,
modifier = Modifier.fillMaxWidth().height(60.dp),
modifier = Modifier.fillMaxWidth().height(52.dp),
shape = RoundedCornerShape(14.dp),
textStyle = MaterialTheme.typography.bodySmall.copy(fontWeight = FontWeight.Medium),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.primary,
unfocusedBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
@@ -191,16 +201,20 @@ fun SettingsTab(settingsStore: SettingsStore) {
)
OutlinedButton(
onClick = { showIpSetupDialog = true },
modifier = Modifier.fillMaxWidth().height(46.dp),
enabled = !cfEnabled && !isRunning,
modifier = Modifier.fillMaxWidth().height(40.dp),
shape = RoundedCornerShape(14.dp),
colors = ButtonDefaults.outlinedButtonColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f),
contentColor = MaterialTheme.colorScheme.primary,
disabledContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f)
),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.5f))
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = if (cfEnabled || isRunning) 0.2f else 0.5f))
) {
Icon(Icons.Default.Settings, null, Modifier.size(18.dp))
Spacer(Modifier.width(8.dp))
Text("Настроить адреса DC", fontWeight = FontWeight.SemiBold)
Text(if (cfEnabled) "Авто" else "Настроить адреса DC", fontWeight = FontWeight.SemiBold)
}
}
}
@@ -212,8 +226,8 @@ fun SettingsTab(settingsStore: SettingsStore) {
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
modifier = Modifier.padding(14.dp),
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
@@ -230,7 +244,7 @@ fun SettingsTab(settingsStore: SettingsStore) {
modifier = Modifier.size(20.dp)
)
Text(
"CloudFlare",
"CloudFlare CDN",
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.SemiBold
@@ -238,19 +252,14 @@ fun SettingsTab(settingsStore: SettingsStore) {
}
Switch(
checked = cfEnabled,
onCheckedChange = { cfEnabled = it; scheduleSave() },
onCheckedChange = {
cfEnabled = it
isDcAuto = it
scheduleSave()
},
enabled = !isRunning
)
}
Text(
if (cfEnabled)
"Трафик проксируется через CloudFlare — улучшает обход блокировок."
else
"Подключение к DC Telegram напрямую.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
lineHeight = 16.sp
)
}
}
@@ -261,15 +270,18 @@ fun SettingsTab(settingsStore: SettingsStore) {
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
modifier = Modifier.padding(14.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
"Пул WS",
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.SemiBold
)
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(Icons.Default.Layers, contentDescription = null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(20.dp))
Text(
"Пул WS",
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.SemiBold
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
@@ -286,7 +298,7 @@ fun SettingsTab(settingsStore: SettingsStore) {
}
}
}
Divider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f))
Divider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f), modifier = Modifier.padding(vertical = 4.dp))
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
@@ -308,7 +320,7 @@ fun SettingsTab(settingsStore: SettingsStore) {
onValueChange = {},
readOnly = true,
singleLine = true,
modifier = Modifier.fillMaxWidth().height(56.dp),
modifier = Modifier.fillMaxWidth().height(52.dp),
shape = RoundedCornerShape(14.dp),
textStyle = MaterialTheme.typography.bodySmall.copy(fontWeight = FontWeight.Medium),
trailingIcon = {
@@ -364,7 +376,7 @@ fun SettingsTab(settingsStore: SettingsStore) {
scope.launch {
settingsStore.saveAll(
isDcAuto, dc2Text, dc4Text, portText, selectedPoolSize,
cfEnabled, secretKeyText
cfEnabled, customCfDomainEnabled, customCfDomain, secretKeyText
)
}
val startIntent = Intent(context, ProxyService::class.java).apply {
@@ -373,15 +385,14 @@ fun SettingsTab(settingsStore: SettingsStore) {
putExtra(ProxyService.EXTRA_IPS, parsedIps)
putExtra(ProxyService.EXTRA_POOL_SIZE, selectedPoolSize)
putExtra(ProxyService.EXTRA_CFPROXY_ENABLED, cfEnabled)
// ProxyService intent expects these even if CF priority is disabled in UI
putExtra(ProxyService.EXTRA_CFPROXY_PRIORITY, true)
putExtra(ProxyService.EXTRA_CFPROXY_DOMAIN, "")
putExtra(ProxyService.EXTRA_CFPROXY_DOMAIN, if (customCfDomainEnabled && cfEnabled) customCfDomain.trim() else "")
putExtra(ProxyService.EXTRA_SECRET_KEY, secretKeyText.trim())
}
ContextCompat.startForegroundService(context, startIntent)
}
},
modifier = Modifier.fillMaxWidth().height(50.dp),
modifier = Modifier.fillMaxWidth().height(48.dp),
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(containerColor = buttonColor)
) {
@@ -402,7 +413,7 @@ fun SettingsTab(settingsStore: SettingsStore) {
val raw = secretKeyText.trim()
if (raw.isNotEmpty()) raw else "00000000000000000000000000000000"
}
val proxyUrl = "tg://proxy?server=127.0.0.1&port=$port&secret=ee$secretForUrl"
val proxyUrl = "tg://proxy?server=127.0.0.1&port=$port&secret=dd$secretForUrl"
val telegramBtnColor by animateColorAsState(
targetValue = if (isRunning) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant,
animationSpec = tween(400),
@@ -411,20 +422,53 @@ fun SettingsTab(settingsStore: SettingsStore) {
Button(
onClick = { openTelegram(context, proxyUrl) },
enabled = isRunning,
modifier = Modifier.fillMaxWidth().height(50.dp),
modifier = Modifier.fillMaxWidth().height(48.dp),
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(containerColor = telegramBtnColor, contentColor = MaterialTheme.colorScheme.onSurface)
) {
Text("Применить в Telegram", fontWeight = FontWeight.SemiBold)
}
OutlinedButton(
onClick = { showHelpDialog = true },
modifier = Modifier.fillMaxWidth().height(46.dp),
shape = RoundedCornerShape(16.dp),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.4f))
Text(
text = "или",
modifier = Modifier.fillMaxWidth(),
textAlign = androidx.compose.ui.text.style.TextAlign.Center,
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Bold),
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Surface(
onClick = {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
val clip = android.content.ClipData.newPlainText("Proxy URL", proxyUrl)
clipboard.setPrimaryClip(clip)
Toast.makeText(context, "Ссылка скопирована", Toast.LENGTH_SHORT).show()
},
shape = RoundedCornerShape(14.dp),
color = androidx.compose.ui.graphics.Color.Transparent,
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)),
modifier = Modifier.fillMaxWidth().height(52.dp)
) {
Text("Пожалуйста ознакомьтесь!", fontWeight = FontWeight.Medium)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp)
) {
Text(
text = proxyUrl,
style = MaterialTheme.typography.bodySmall.copy(
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Medium
),
maxLines = 1,
modifier = Modifier.weight(1f)
)
Icon(
imageVector = Icons.Default.ContentCopy,
contentDescription = "Копировать",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(20.dp)
)
}
}
Spacer(Modifier.height(8.dp))
@@ -456,102 +500,10 @@ private fun PoolChip(
}
}
@Composable
private fun HelpDialog(onDismiss: () -> Unit) {
Dialog(
onDismissRequest = onDismiss,
properties = DialogProperties(usePlatformDefaultWidth = false)
) {
Surface(
shape = RoundedCornerShape(24.dp),
color = MaterialTheme.colorScheme.surface,
tonalElevation = 8.dp,
modifier = Modifier.fillMaxWidth(0.95f)
) {
Column(
modifier = Modifier
.padding(24.dp)
.fillMaxWidth()
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(14.dp)
) {
Text(
"Справка",
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold
)
HelpSection(
title = "Адреса датацентров",
text = "Внимание, рекомендую использовать при включенном CloudFlare - Автоматический режим получения DC от самого телеграма. " +
"В случае если вы не пользуетесь CloudFlare или он у вас не работает, переключитесь на ручное использование. " +
"По умолчанию указан DC4 149.154.167.220."
)
Divider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.3f))
HelpSection(
title = "Порт",
text = "Локальный порт прокси. Используйте свободный порт. По умолчанию — 1443."
)
Divider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.3f))
HelpSection(
title = "CloudFlare",
text = "Проксирует трафик через CloudFlare для обхода блокировок. Если Telegram не подключается — отключите."
)
Divider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.3f))
HelpSection(
title = "Пул WS",
text = "Количество фоновых соединений (по умолчанию 4). Увеличьте, если скорость низкая."
)
Divider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.3f))
HelpSection(
title = "Секретный ключ",
text = "Ключ шифрования MTProto. Обновляйте только при необходимости."
)
Spacer(Modifier.height(4.dp))
Button(
onClick = onDismiss,
modifier = Modifier.fillMaxWidth().height(46.dp),
shape = RoundedCornerShape(16.dp)
) {
Text("Понятно", fontWeight = FontWeight.SemiBold)
}
}
}
}
}
@Composable
private fun HelpSection(title: String, text: String) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(
title,
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold
)
Text(
text,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
lineHeight = 20.sp
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun IpSetupDialog(
isDcAuto: Boolean, onModeChange: (Boolean) -> Unit,
dc2Text: String, onDc2Change: (String) -> Unit,
dc4Text: String, onDc4Change: (String) -> Unit,
onDismiss: () -> Unit
@@ -585,37 +537,8 @@ private fun IpSetupDialog(
fontWeight = FontWeight.Bold
)
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(14.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.1f))
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 10.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = if (isDcAuto) "Авто DC от Telegram" else "Ручные DC",
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.primary
)
Switch(
checked = isDcAuto,
onCheckedChange = { onModeChange(it) }
)
}
}
if (!isDcAuto) {
@Composable
fun dcInput(label: String, value: String, update: (String) -> Unit) {
@Composable
fun dcInput(label: String, value: String, update: (String) -> Unit) {
Text(
label,
style = MaterialTheme.typography.bodySmall,
@@ -638,7 +561,6 @@ private fun IpSetupDialog(
dcInput("DC2", dc2Text, onDc2Change)
dcInput("DC4", dc4Text, onDc4Change)
}
Spacer(Modifier.height(4.dp))

View File

@@ -115,6 +115,92 @@ private val DarkColorScheme = darkColorScheme(
surfaceTint = Color(0xFFD7CCC8),
)
// ═══ Тёмная палитра — «Цвет 1» ═══
private val IndigoLightColorScheme = lightColorScheme(
primary = Color(0xFF5B588D),
onPrimary = Color(0xFFFFFFFF),
primaryContainer = Color(0xFFE2DFFF),
onPrimaryContainer = Color(0xFF1A1744),
secondary = Color(0xFF5B588D),
onSecondary = Color(0xFFFFFFFF),
secondaryContainer = Color(0xFFE2DFFF),
onSecondaryContainer = Color(0xFF1A1744),
background = Color(0xFFFBF8FF),
onBackground = Color(0xFF1B1B1F),
surface = Color(0xFFF6F3FA),
onSurface = Color(0xFF1B1B1F),
surfaceVariant = Color(0xFFE4E1EC),
onSurfaceVariant = Color(0xFF47464F),
outline = Color(0xFF787680),
outlineVariant = Color(0xFFC8C5D0),
)
private val IndigoDarkColorScheme = darkColorScheme(
primary = Color(0xFFC4C0FF),
onPrimary = Color(0xFF2D2A5B),
primaryContainer = Color(0xFF434073),
onPrimaryContainer = Color(0xFFE2DFFF),
secondary = Color(0xFFC4C0FF),
onSecondary = Color(0xFF2D2A5B),
secondaryContainer = Color(0xFF434073),
onSecondaryContainer = Color(0xFFE2DFFF),
background = Color(0xFF131316),
onBackground = Color(0xFFE4E1E6),
surface = Color(0xFF1B1B1F),
onSurface = Color(0xFFC8C5D0),
surfaceVariant = Color(0xFF47464F),
onSurfaceVariant = Color(0xFFC8C5D0),
outline = Color(0xFF918F9A),
outlineVariant = Color(0xFF47464F),
)
// ═══ Палитра «Цвет 2» ═══
private val ForestLightColorScheme = lightColorScheme(
primary = Color(0xFF5F5D68),
onPrimary = Color(0xFFFFFFFF),
primaryContainer = Color(0xFFE5E0F0),
onPrimaryContainer = Color(0xFF1C1A23),
secondary = Color(0xFF5F5D68),
onSecondary = Color(0xFFFFFFFF),
secondaryContainer = Color(0xFFE5E0F0),
onSecondaryContainer = Color(0xFF1C1A23),
background = Color(0xFFFCF8FF),
onBackground = Color(0xFF1D1B20),
surface = Color(0xFFF7F2FA),
onSurface = Color(0xFF1D1B20),
surfaceVariant = Color(0xFFE6E0E9),
onSurfaceVariant = Color(0xFF48454E),
outline = Color(0xFF79747E),
outlineVariant = Color(0xFFCAC4D0),
)
private val ForestDarkColorScheme = darkColorScheme(
primary = Color(0xFFC8C4D3),
onPrimary = Color(0xFF312F38),
primaryContainer = Color(0xFF474550),
onPrimaryContainer = Color(0xFFE5E0F0),
secondary = Color(0xFFC8C4D3),
onSecondary = Color(0xFF312F38),
secondaryContainer = Color(0xFF474550),
onSecondaryContainer = Color(0xFFE5E0F0),
background = Color(0xFF141318),
onBackground = Color(0xFFE6E1E5),
surface = Color(0xFF1D1B20),
onSurface = Color(0xFFCAC4D0),
surfaceVariant = Color(0xFF48454E),
onSurfaceVariant = Color(0xFFCAC4D0),
outline = Color(0xFF938F99),
outlineVariant = Color(0xFF48454E),
)
private fun getAppColorScheme(palette: String, isDark: Boolean): androidx.compose.material3.ColorScheme {
return when(palette) {
"espresso" -> if (isDark) DarkColorScheme else LightColorScheme
"forest" -> if (isDark) ForestDarkColorScheme else ForestLightColorScheme
else -> if (isDark) IndigoDarkColorScheme else IndigoLightColorScheme
}
}
// ═══ Расширенные цвета для кастомных элементов ═══
object AppColors {
val connected = Color(0xFF4CAF50)
@@ -148,6 +234,7 @@ object AppColors {
fun TgWsProxyTheme(
themeMode: String = "system",
dynamicColor: Boolean = true,
themePalette: String = "indigo",
content: @Composable () -> Unit
) {
val darkTheme = when (themeMode) {
@@ -161,8 +248,7 @@ fun TgWsProxyTheme(
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
else -> getAppColorScheme(themePalette, darkTheme)
}
MaterialTheme(