mirror of
https://github.com/Flowseal/tg-ws-proxy.git
synced 2026-06-17 20:18:28 +03:00
Update v1.0.7
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user