feat: Add network permissions and security configuration for internet access
This commit is contained in:
@@ -2,6 +2,9 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
@@ -11,6 +14,7 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.RosettaAndroid"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
|
||||
@@ -96,6 +96,7 @@ fun ConfirmSeedPhraseScreen(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.imePadding()
|
||||
.padding(horizontal = 24.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
|
||||
@@ -70,6 +70,7 @@ fun ImportSeedPhraseScreen(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.imePadding()
|
||||
.padding(horizontal = 24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
|
||||
@@ -104,6 +104,7 @@ fun SetPasswordScreen(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.imePadding()
|
||||
.padding(horizontal = 24.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
|
||||
@@ -76,6 +76,7 @@ fun UnlockScreen(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.imePadding()
|
||||
.padding(horizontal = 24.dp)
|
||||
.statusBarsPadding(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
|
||||
@@ -16,6 +16,8 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
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.style.TextAlign
|
||||
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 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
|
||||
val lockComposition by rememberLottieComposition(LottieCompositionSpec.Asset("lottie/lock.json"))
|
||||
val lockProgress by animateLottieCompositionAsState(
|
||||
|
||||
@@ -19,8 +19,10 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -49,27 +51,50 @@ data class Chat(
|
||||
val isPinned: Boolean = false
|
||||
)
|
||||
|
||||
// Avatar colors like in React Native app
|
||||
private val avatarColors = listOf(
|
||||
Color(0xFF5E9FFF), // Blue
|
||||
Color(0xFFFF7EB3), // Pink
|
||||
Color(0xFF7B68EE), // Purple
|
||||
Color(0xFF50C878), // Green
|
||||
Color(0xFFFF6B6B), // Red
|
||||
Color(0xFF4ECDC4), // Teal
|
||||
Color(0xFFFFB347), // Orange
|
||||
Color(0xFFBA55D3) // Orchid
|
||||
// Avatar colors matching React Native app (Mantine inspired)
|
||||
// Light theme colors (background lighter, text darker)
|
||||
private val avatarColorsLight = listOf(
|
||||
Color(0xFF1971c2) to Color(0xFFd0ebff), // blue
|
||||
Color(0xFF0c8599) to Color(0xFFc5f6fa), // cyan
|
||||
Color(0xFF9c36b5) to Color(0xFFeebefa), // grape
|
||||
Color(0xFF2f9e44) to Color(0xFFd3f9d8), // green
|
||||
Color(0xFF4263eb) to Color(0xFFdbe4ff), // indigo
|
||||
Color(0xFF5c940d) to Color(0xFFe9fac8), // lime
|
||||
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 для цветов аватаров - избегаем вычисления каждый раз
|
||||
private val avatarColorCache = mutableMapOf<String, Color>()
|
||||
// Dark theme colors (background darker, text lighter)
|
||||
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 {
|
||||
return avatarColorCache.getOrPut(name) {
|
||||
val index = name.hashCode().mod(avatarColors.size).let {
|
||||
if (it < 0) it + avatarColors.size else it
|
||||
// Cache для цветов аватаров
|
||||
data class AvatarColors(val textColor: Color, val backgroundColor: Color)
|
||||
private val avatarColorCache = mutableMapOf<String, AvatarColors>()
|
||||
|
||||
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,
|
||||
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 textColor = if (isDarkTheme) Color.White else Color.Black
|
||||
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
||||
val dividerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8)
|
||||
val textColor by animateColorAsState(
|
||||
targetValue = if (isDarkTheme) Color.White else Color.Black,
|
||||
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 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(
|
||||
drawerState = drawerState,
|
||||
drawerContent = {
|
||||
@@ -283,13 +419,35 @@ fun ChatsListScreen(
|
||||
|
||||
// Theme toggle
|
||||
IconButton(
|
||||
onClick = onToggleTheme
|
||||
onClick = {},
|
||||
modifier = Modifier.onGloballyPositioned { coordinates ->
|
||||
// This will be handled by clickable below
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
if (isDarkTheme) Icons.Default.LightMode else Icons.Default.DarkMode,
|
||||
contentDescription = "Toggle theme",
|
||||
tint = textColor
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
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),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
val avatarColors = getAvatarColor(accountName, isDarkTheme)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(30.dp)
|
||||
.clip(CircleShape)
|
||||
.background(getAvatarColor(accountName)),
|
||||
.background(avatarColors.backgroundColor),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = getInitials(accountName),
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White
|
||||
color = avatarColors.textColor
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -514,15 +673,50 @@ fun ChatsListScreen(
|
||||
},
|
||||
containerColor = backgroundColor
|
||||
) { paddingValues ->
|
||||
// Empty state with Lottie animation
|
||||
EmptyChatsState(
|
||||
isDarkTheme = isDarkTheme,
|
||||
// Dev Console Button in bottom left corner
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.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
|
||||
@@ -638,7 +832,7 @@ fun ChatItem(
|
||||
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
||||
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)
|
||||
|
||||
Column {
|
||||
@@ -654,14 +848,14 @@ fun ChatItem(
|
||||
modifier = Modifier
|
||||
.size(56.dp)
|
||||
.clip(CircleShape)
|
||||
.background(avatarColor),
|
||||
.background(avatarColors.backgroundColor),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = initials,
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White
|
||||
color = avatarColors.textColor
|
||||
)
|
||||
|
||||
// Online indicator
|
||||
|
||||
6
app/src/main/res/xml/network_security_config.xml
Normal file
6
app/src/main/res/xml/network_security_config.xml
Normal 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>
|
||||
Reference in New Issue
Block a user