feat: Add network permissions and security configuration for internet access

This commit is contained in:
k1ngsterr1
2026-01-09 01:00:49 +05:00
parent ed973ed485
commit 4adbbd095d
8 changed files with 253 additions and 36 deletions

View File

@@ -2,6 +2,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
@@ -11,6 +14,7 @@
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.RosettaAndroid" android:theme="@style/Theme.RosettaAndroid"
android:networkSecurityConfig="@xml/network_security_config"
tools:targetApi="31"> tools:targetApi="31">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"

View File

@@ -96,6 +96,7 @@ fun ConfirmSeedPhraseScreen(
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.imePadding()
.padding(horizontal = 24.dp) .padding(horizontal = 24.dp)
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally

View File

@@ -70,6 +70,7 @@ fun ImportSeedPhraseScreen(
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.imePadding()
.padding(horizontal = 24.dp), .padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {

View File

@@ -104,6 +104,7 @@ fun SetPasswordScreen(
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.imePadding()
.padding(horizontal = 24.dp) .padding(horizontal = 24.dp)
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally

View File

@@ -76,6 +76,7 @@ fun UnlockScreen(
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.imePadding()
.padding(horizontal = 24.dp) .padding(horizontal = 24.dp)
.statusBarsPadding(), .statusBarsPadding(),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally

View File

@@ -16,6 +16,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -41,6 +43,13 @@ fun WelcomeScreen(
val textColor by animateColorAsState(if (isDarkTheme) Color.White else Color.Black, animationSpec = themeAnimSpec) val textColor by animateColorAsState(if (isDarkTheme) Color.White else Color.Black, animationSpec = themeAnimSpec)
val secondaryTextColor by animateColorAsState(if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666), animationSpec = themeAnimSpec) val secondaryTextColor by animateColorAsState(if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666), animationSpec = themeAnimSpec)
// Sync navigation bar color with background
val view = LocalView.current
SideEffect {
val window = (view.context as? android.app.Activity)?.window
window?.navigationBarColor = backgroundColor.toArgb()
}
// Animation for Lottie // Animation for Lottie
val lockComposition by rememberLottieComposition(LottieCompositionSpec.Asset("lottie/lock.json")) val lockComposition by rememberLottieComposition(LottieCompositionSpec.Asset("lottie/lock.json"))
val lockProgress by animateLottieCompositionAsState( val lockProgress by animateLottieCompositionAsState(

View File

@@ -19,8 +19,10 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
@@ -49,27 +51,50 @@ data class Chat(
val isPinned: Boolean = false val isPinned: Boolean = false
) )
// Avatar colors like in React Native app // Avatar colors matching React Native app (Mantine inspired)
private val avatarColors = listOf( // Light theme colors (background lighter, text darker)
Color(0xFF5E9FFF), // Blue private val avatarColorsLight = listOf(
Color(0xFFFF7EB3), // Pink Color(0xFF1971c2) to Color(0xFFd0ebff), // blue
Color(0xFF7B68EE), // Purple Color(0xFF0c8599) to Color(0xFFc5f6fa), // cyan
Color(0xFF50C878), // Green Color(0xFF9c36b5) to Color(0xFFeebefa), // grape
Color(0xFFFF6B6B), // Red Color(0xFF2f9e44) to Color(0xFFd3f9d8), // green
Color(0xFF4ECDC4), // Teal Color(0xFF4263eb) to Color(0xFFdbe4ff), // indigo
Color(0xFFFFB347), // Orange Color(0xFF5c940d) to Color(0xFFe9fac8), // lime
Color(0xFFBA55D3) // Orchid Color(0xFFd9480f) to Color(0xFFffe8cc), // orange
Color(0xFFc2255c) to Color(0xFFffdeeb), // pink
Color(0xFFe03131) to Color(0xFFffe0e0), // red
Color(0xFF099268) to Color(0xFFc3fae8), // teal
Color(0xFF6741d9) to Color(0xFFe5dbff) // violet
) )
// Cache для цветов аватаров - избегаем вычисления каждый раз // Dark theme colors (background darker, text lighter)
private val avatarColorCache = mutableMapOf<String, Color>() private val avatarColorsDark = listOf(
Color(0xFF7dd3fc) to Color(0xFF2d3548), // blue
Color(0xFF67e8f9) to Color(0xFF2d4248), // cyan
Color(0xFFd8b4fe) to Color(0xFF39334c), // grape
Color(0xFF86efac) to Color(0xFF2d3f32), // green
Color(0xFFa5b4fc) to Color(0xFF333448), // indigo
Color(0xFFbef264) to Color(0xFF383f2d), // lime
Color(0xFFfdba74) to Color(0xFF483529), // orange
Color(0xFFf9a8d4) to Color(0xFF482d3d), // pink
Color(0xFFfca5a5) to Color(0xFF482d2d), // red
Color(0xFF5eead4) to Color(0xFF2d4340), // teal
Color(0xFFc4b5fd) to Color(0xFF3a334c) // violet
)
fun getAvatarColor(name: String): Color { // Cache для цветов аватаров
return avatarColorCache.getOrPut(name) { data class AvatarColors(val textColor: Color, val backgroundColor: Color)
val index = name.hashCode().mod(avatarColors.size).let { private val avatarColorCache = mutableMapOf<String, AvatarColors>()
if (it < 0) it + avatarColors.size else it
fun getAvatarColor(name: String, isDarkTheme: Boolean): AvatarColors {
val cacheKey = "${name}_${if (isDarkTheme) "dark" else "light"}"
return avatarColorCache.getOrPut(cacheKey) {
val colors = if (isDarkTheme) avatarColorsDark else avatarColorsLight
val index = name.hashCode().mod(colors.size).let {
if (it < 0) it + colors.size else it
} }
avatarColors[index] val (textColor, bgColor) = colors[index]
AvatarColors(textColor, bgColor)
} }
} }
@@ -113,11 +138,97 @@ fun ChatsListScreen(
onNewChat: () -> Unit, onNewChat: () -> Unit,
onLogout: () -> Unit onLogout: () -> Unit
) { ) {
val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF) // Theme transition animation state
var isTransitioning by remember { mutableStateOf(false) }
var transitionProgress by remember { mutableStateOf(0f) }
var clickPosition by remember { mutableStateOf(Offset.Zero) }
var shouldUpdateStatusBar by remember { mutableStateOf(false) }
var hasInitialized by remember { mutableStateOf(false) }
var previousTheme by remember { mutableStateOf(isDarkTheme) }
var targetTheme by remember { mutableStateOf(isDarkTheme) }
LaunchedEffect(Unit) {
hasInitialized = true
}
// Theme transition animation
LaunchedEffect(isTransitioning) {
if (isTransitioning) {
shouldUpdateStatusBar = false
val duration = 800f
val startTime = System.currentTimeMillis()
while (transitionProgress < 1f) {
val elapsed = System.currentTimeMillis() - startTime
transitionProgress = (elapsed / duration).coerceAtMost(1f)
kotlinx.coroutines.delay(16)
}
shouldUpdateStatusBar = true
kotlinx.coroutines.delay(50)
isTransitioning = false
transitionProgress = 0f
shouldUpdateStatusBar = false
previousTheme = targetTheme
}
}
val view = androidx.compose.ui.platform.LocalView.current
// Animate navigation bar color starting at 80% of wave animation
LaunchedEffect(isTransitioning, transitionProgress) {
if (isTransitioning && transitionProgress >= 0.8f && !view.isInEditMode) {
val window = (view.context as android.app.Activity).window
val navProgress = ((transitionProgress - 0.8f) / 0.2f).coerceIn(0f, 1f)
val oldColor = if (previousTheme) 0xFF1A1A1A else 0xFFFFFFFF
val newColor = if (targetTheme) 0xFF1A1A1A else 0xFFFFFFFF
val r1 = (oldColor shr 16 and 0xFF)
val g1 = (oldColor shr 8 and 0xFF)
val b1 = (oldColor and 0xFF)
val r2 = (newColor shr 16 and 0xFF)
val g2 = (newColor shr 8 and 0xFF)
val b2 = (newColor and 0xFF)
val r = (r1 + (r2 - r1) * navProgress).toInt()
val g = (g1 + (g2 - g1) * navProgress).toInt()
val b = (b1 + (b2 - b1) * navProgress).toInt()
window.navigationBarColor = (0xFF000000 or (r.toLong() shl 16) or (g.toLong() shl 8) or b.toLong()).toInt()
}
}
// Update status bar icons when animation finishes
LaunchedEffect(shouldUpdateStatusBar) {
if (shouldUpdateStatusBar && !view.isInEditMode) {
val window = (view.context as android.app.Activity).window
val insetsController = androidx.core.view.WindowCompat.getInsetsController(window, view)
insetsController.isAppearanceLightStatusBars = !isDarkTheme
insetsController.isAppearanceLightNavigationBars = !isDarkTheme
window.statusBarColor = android.graphics.Color.TRANSPARENT
}
}
val backgroundColor by animateColorAsState(
targetValue = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF),
animationSpec = if (!hasInitialized) snap() else tween(800, easing = FastOutSlowInEasing),
label = "backgroundColor"
)
val drawerBackgroundColor = if (isDarkTheme) Color(0xFF212121) else Color(0xFFFFFFFF) val drawerBackgroundColor = if (isDarkTheme) Color(0xFF212121) else Color(0xFFFFFFFF)
val textColor = if (isDarkTheme) Color.White else Color.Black val textColor by animateColorAsState(
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666) targetValue = if (isDarkTheme) Color.White else Color.Black,
val dividerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8) animationSpec = if (!hasInitialized) snap() else tween(800, easing = FastOutSlowInEasing),
label = "textColor"
)
val secondaryTextColor by animateColorAsState(
targetValue = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666),
animationSpec = if (!hasInitialized) snap() else tween(800, easing = FastOutSlowInEasing),
label = "secondaryTextColor"
)
val dividerColor by animateColorAsState(
targetValue = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8),
animationSpec = if (!hasInitialized) snap() else tween(800, easing = FastOutSlowInEasing),
label = "dividerColor"
)
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@@ -245,6 +356,31 @@ fun ChatsListScreen(
) )
) )
Box(modifier = Modifier.fillMaxSize()) {
// Base background - shows the OLD theme color during transition
Box(
modifier = Modifier
.fillMaxSize()
.background(if (isTransitioning) {
if (previousTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF)
} else backgroundColor)
)
// Circular reveal overlay - draws the NEW theme color expanding
if (isTransitioning) {
Canvas(modifier = Modifier.fillMaxSize()) {
val maxRadius = kotlin.math.hypot(size.width, size.height)
val radius = maxRadius * transitionProgress
// Draw the NEW theme color expanding from click point
drawCircle(
color = if (targetTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF),
radius = radius,
center = clickPosition
)
}
}
ModalNavigationDrawer( ModalNavigationDrawer(
drawerState = drawerState, drawerState = drawerState,
drawerContent = { drawerContent = {
@@ -283,13 +419,35 @@ fun ChatsListScreen(
// Theme toggle // Theme toggle
IconButton( IconButton(
onClick = onToggleTheme onClick = {},
modifier = Modifier.onGloballyPositioned { coordinates ->
// This will be handled by clickable below
}
) { ) {
Icon( Box(
if (isDarkTheme) Icons.Default.LightMode else Icons.Default.DarkMode, modifier = Modifier
contentDescription = "Toggle theme", .clickable {
tint = textColor if (!isTransitioning) {
) previousTheme = isDarkTheme
targetTheme = !isDarkTheme
// Use center of icon as click position
val screenWidth = view.width.toFloat()
val screenHeight = view.height.toFloat()
clickPosition = Offset(
screenWidth - 48.dp.value * view.resources.displayMetrics.density,
96.dp.value * view.resources.displayMetrics.density
)
isTransitioning = true
onToggleTheme()
}
}
) {
Icon(
if (isDarkTheme) Icons.Default.LightMode else Icons.Default.DarkMode,
contentDescription = "Toggle theme",
tint = textColor
)
}
} }
} }
@@ -434,18 +592,19 @@ fun ChatsListScreen(
.background(backgroundColor), .background(backgroundColor),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
val avatarColors = getAvatarColor(accountName, isDarkTheme)
Box( Box(
modifier = Modifier modifier = Modifier
.size(30.dp) .size(30.dp)
.clip(CircleShape) .clip(CircleShape)
.background(getAvatarColor(accountName)), .background(avatarColors.backgroundColor),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Text(
text = getInitials(accountName), text = getInitials(accountName),
fontSize = 12.sp, fontSize = 12.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = Color.White color = avatarColors.textColor
) )
} }
} }
@@ -514,15 +673,50 @@ fun ChatsListScreen(
}, },
containerColor = backgroundColor containerColor = backgroundColor
) { paddingValues -> ) { paddingValues ->
// Empty state with Lottie animation // Dev Console Button in bottom left corner
EmptyChatsState( Box(
isDarkTheme = isDarkTheme,
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues) .padding(paddingValues)
) ) {
// Console button
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(500, delayMillis = 400)) + slideInHorizontally(
initialOffsetX = { -it },
animationSpec = tween(500, delayMillis = 400)
),
modifier = Modifier
.align(Alignment.BottomStart)
.padding(16.dp)
) {
FloatingActionButton(
onClick = { showDevConsole = true },
containerColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5),
contentColor = if (protocolState == ProtocolState.AUTHENTICATED)
Color(0xFF4CAF50)
else
Color(0xFFFF9800),
shape = CircleShape,
modifier = Modifier.size(48.dp)
) {
Icon(
Icons.Default.Terminal,
contentDescription = "Dev Console",
modifier = Modifier.size(24.dp)
)
}
}
// Empty state with Lottie animation
EmptyChatsState(
isDarkTheme = isDarkTheme,
modifier = Modifier.fillMaxSize()
)
}
} }
} }
} // Close Box for circular reveal
} }
@Composable @Composable
@@ -638,7 +832,7 @@ fun ChatItem(
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666) val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
val dividerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8) val dividerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8)
val avatarColor = getAvatarColor(chat.name) val avatarColors = getAvatarColor(chat.name, isDarkTheme)
val initials = getInitials(chat.name) val initials = getInitials(chat.name)
Column { Column {
@@ -654,14 +848,14 @@ fun ChatItem(
modifier = Modifier modifier = Modifier
.size(56.dp) .size(56.dp)
.clip(CircleShape) .clip(CircleShape)
.background(avatarColor), .background(avatarColors.backgroundColor),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Text(
text = initials, text = initials,
fontSize = 20.sp, fontSize = 20.sp,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
color = Color.White color = avatarColors.textColor
) )
// Online indicator // Online indicator

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">46.28.71.12</domain>
</domain-config>
</network-security-config>