Смена иконки приложения — калькулятор, погода, заметки + экран выбора в настройка

This commit is contained in:
2026-04-12 23:59:04 +05:00
parent b1fc623f5e
commit cb920b490d
14 changed files with 554 additions and 4 deletions

View File

@@ -47,10 +47,7 @@
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode|smallestScreenSize|screenLayout"
android:windowSoftInputMode="adjustResize"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- LAUNCHER intent-filter moved to activity-alias entries for icon switching -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
@@ -65,6 +62,63 @@
</intent-filter>
</activity>
<!-- App Icon Aliases: only one enabled at a time -->
<activity-alias
android:name=".MainActivityDefault"
android:targetActivity=".MainActivity"
android:enabled="true"
android:exported="true"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<activity-alias
android:name=".MainActivityCalculator"
android:targetActivity=".MainActivity"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_launcher_calc"
android:roundIcon="@mipmap/ic_launcher_calc"
android:label="Calculator">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<activity-alias
android:name=".MainActivityWeather"
android:targetActivity=".MainActivity"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_launcher_weather"
android:roundIcon="@mipmap/ic_launcher_weather"
android:label="Weather">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<activity-alias
android:name=".MainActivityNotes"
android:targetActivity=".MainActivity"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_launcher_notes"
android:roundIcon="@mipmap/ic_launcher_notes"
android:label="Notes">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<activity
android:name=".IncomingCallActivity"
android:exported="false"

View File

@@ -672,6 +672,7 @@ sealed class Screen {
data object CrashLogs : Screen()
data object Biometric : Screen()
data object Appearance : Screen()
data object AppIcon : Screen()
data object QrScanner : Screen()
data object MyQr : Screen()
}
@@ -1031,6 +1032,9 @@ fun MainScreen(
val isAppearanceVisible by remember {
derivedStateOf { navStack.any { it is Screen.Appearance } }
}
val isAppIconVisible by remember {
derivedStateOf { navStack.any { it is Screen.AppIcon } }
}
val isQrScannerVisible by remember { derivedStateOf { navStack.any { it is Screen.QrScanner } } }
val isMyQrVisible by remember { derivedStateOf { navStack.any { it is Screen.MyQr } } }
var profileHasUnsavedChanges by remember(accountPublicKey) { mutableStateOf(false) }
@@ -1437,12 +1441,25 @@ fun MainScreen(
}
},
onToggleTheme = onToggleTheme,
onAppIconClick = { navStack = navStack + Screen.AppIcon },
accountPublicKey = accountPublicKey,
accountName = accountName,
avatarRepository = avatarRepository
)
}
SwipeBackContainer(
isVisible = isAppIconVisible,
onBack = { navStack = navStack.filterNot { it is Screen.AppIcon } },
isDarkTheme = isDarkTheme,
layer = 3
) {
com.rosetta.messenger.ui.settings.AppIconScreen(
isDarkTheme = isDarkTheme,
onBack = { navStack = navStack.filterNot { it is Screen.AppIcon } }
)
}
SwipeBackContainer(
isVisible = isUpdatesVisible,
onBack = { navStack = navStack.filterNot { it is Screen.Updates } },

View File

@@ -58,6 +58,9 @@ class PreferencesManager(private val context: Context) {
val BACKGROUND_BLUR_COLOR_ID =
stringPreferencesKey("background_blur_color_id") // id from BackgroundBlurPresets
// App Icon disguise: "default", "calculator", "weather", "notes"
val APP_ICON = stringPreferencesKey("app_icon")
// Pinned Chats (max 3)
val PINNED_CHATS = stringSetPreferencesKey("pinned_chats") // Set of opponent public keys
@@ -333,6 +336,19 @@ class PreferencesManager(private val context: Context) {
return wasPinned
}
// ═════════════════════════════════════════════════════════════
// 🎨 APP ICON
// ═════════════════════════════════════════════════════════════
val appIcon: Flow<String> =
context.dataStore.data.map { preferences ->
preferences[APP_ICON] ?: "default"
}
suspend fun setAppIcon(value: String) {
context.dataStore.edit { preferences -> preferences[APP_ICON] = value }
}
// ═════════════════════════════════════════════════════════════
// 🔕 MUTED CHATS
// ═════════════════════════════════════════════════════════════

View File

@@ -0,0 +1,270 @@
package com.rosetta.messenger.ui.settings
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import compose.icons.TablerIcons
import compose.icons.tablericons.ChevronLeft
import com.rosetta.messenger.R
import com.rosetta.messenger.data.PreferencesManager
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
data class AppIconOption(
val id: String,
val label: String,
val subtitle: String,
val aliasName: String,
val iconRes: Int,
val previewBg: Color
)
private val iconOptions = listOf(
AppIconOption("default", "Rosetta", "Original icon", ".MainActivityDefault", R.drawable.ic_launcher_foreground, Color(0xFF1B1B1B)),
AppIconOption("calculator", "Calculator", "Disguise as calculator", ".MainActivityCalculator", R.drawable.ic_calc_foreground, Color(0xFF795548)),
AppIconOption("weather", "Weather", "Disguise as weather app", ".MainActivityWeather", R.drawable.ic_weather_foreground, Color(0xFF42A5F5)),
AppIconOption("notes", "Notes", "Disguise as notes app", ".MainActivityNotes", R.drawable.ic_notes_foreground, Color(0xFFFFC107))
)
@Composable
fun AppIconScreen(
isDarkTheme: Boolean,
onBack: () -> Unit
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val prefs = remember { PreferencesManager(context) }
var currentIcon by remember { mutableStateOf("default") }
LaunchedEffect(Unit) {
currentIcon = prefs.appIcon.first()
}
val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF)
val surfaceColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color(0xFFF2F2F7)
val textColor = if (isDarkTheme) Color.White else Color.Black
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
val dividerColor = if (isDarkTheme) Color(0xFF38383A) else Color(0xFFE5E5EA)
// Status bar
val view = androidx.compose.ui.platform.LocalView.current
if (!view.isInEditMode) {
DisposableEffect(isDarkTheme) {
val window = (view.context as android.app.Activity).window
val insetsController = androidx.core.view.WindowCompat.getInsetsController(window, view)
val prev = insetsController.isAppearanceLightStatusBars
insetsController.isAppearanceLightStatusBars = !isDarkTheme
onDispose { insetsController.isAppearanceLightStatusBars = prev }
}
}
BackHandler { onBack() }
Column(
modifier = Modifier
.fillMaxSize()
.background(backgroundColor)
) {
// ═══════════════════════════════════════════════════════
// TOP BAR — same style as SafetyScreen
// ═══════════════════════════════════════════════════════
Surface(
modifier = Modifier.fillMaxWidth(),
color = backgroundColor
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding())
.padding(horizontal = 4.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = onBack) {
Icon(
imageVector = TablerIcons.ChevronLeft,
contentDescription = "Back",
tint = textColor
)
}
Text(
text = "App Icon",
fontSize = 20.sp,
fontWeight = FontWeight.SemiBold,
color = textColor,
modifier = Modifier.padding(start = 8.dp)
)
}
}
// ═══════════════════════════════════════════════════════
// CONTENT
// ═══════════════════════════════════════════════════════
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
Spacer(modifier = Modifier.height(8.dp))
// Section header
Text(
text = "CHOOSE ICON",
fontSize = 13.sp,
fontWeight = FontWeight.Medium,
color = secondaryTextColor,
letterSpacing = 0.5.sp,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
)
// Icon cards in grouped surface (Telegram style)
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
shape = RoundedCornerShape(12.dp),
color = surfaceColor
) {
Column {
iconOptions.forEachIndexed { index, option ->
val isSelected = currentIcon == option.id
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
if (!isSelected) {
scope.launch {
changeAppIcon(context, prefs, option.id)
currentIcon = option.id
}
}
}
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Icon preview
Box(
modifier = Modifier
.size(52.dp)
.clip(RoundedCornerShape(12.dp))
.background(option.previewBg),
contentAlignment = Alignment.Center
) {
// Default icon has 15% inset built-in — show full size
val iconSize = if (option.id == "default") 52.dp else 36.dp
val scaleType = if (option.id == "default")
android.widget.ImageView.ScaleType.CENTER_CROP
else
android.widget.ImageView.ScaleType.FIT_CENTER
androidx.compose.ui.viewinterop.AndroidView(
factory = { ctx ->
android.widget.ImageView(ctx).apply {
setImageResource(option.iconRes)
this.scaleType = scaleType
}
},
modifier = Modifier.size(iconSize)
)
}
Spacer(modifier = Modifier.width(14.dp))
// Label + subtitle
Column(modifier = Modifier.weight(1f)) {
Text(
text = option.label,
color = textColor,
fontSize = 16.sp,
fontWeight = FontWeight.Normal
)
Text(
text = option.subtitle,
color = secondaryTextColor,
fontSize = 13.sp
)
}
// Checkmark
if (isSelected) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = "Selected",
tint = PrimaryBlue,
modifier = Modifier.size(22.dp)
)
}
}
// Divider between items (not after last)
if (index < iconOptions.lastIndex) {
Divider(
modifier = Modifier.padding(start = 82.dp),
thickness = 0.5.dp,
color = dividerColor
)
}
}
}
}
// Info text below
Text(
text = "The app icon and name on your home screen will change. Rosetta will continue to work normally. The launcher may take a moment to update.",
fontSize = 13.sp,
color = secondaryTextColor,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
lineHeight = 18.sp
)
Spacer(modifier = Modifier.height(32.dp))
}
}
}
private suspend fun changeAppIcon(context: Context, prefs: PreferencesManager, newIconId: String) {
val pm = context.packageManager
val packageName = context.packageName
iconOptions.forEach { option ->
val component = ComponentName(packageName, "$packageName${option.aliasName}")
pm.setComponentEnabledSetting(
component,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP
)
}
val selected = iconOptions.first { it.id == newIconId }
val component = ComponentName(packageName, "$packageName${selected.aliasName}")
pm.setComponentEnabledSetting(
component,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP
)
prefs.setAppIcon(newIconId)
Toast.makeText(context, "Icon changed to ${selected.label}", Toast.LENGTH_SHORT).show()
}

View File

@@ -78,6 +78,7 @@ fun AppearanceScreen(
onBack: () -> Unit,
onBlurColorChange: (String) -> Unit,
onToggleTheme: () -> Unit = {},
onAppIconClick: () -> Unit = {},
accountPublicKey: String = "",
accountName: String = "",
avatarRepository: AvatarRepository? = null
@@ -282,6 +283,49 @@ fun AppearanceScreen(
lineHeight = 18.sp
)
Spacer(modifier = Modifier.height(24.dp))
// ═══════════════════════════════════════════════════════
// APP ICON SECTION
// ═══════════════════════════════════════════════════════
Text(
text = "APP ICON",
fontSize = 13.sp,
fontWeight = FontWeight.Medium,
color = secondaryTextColor,
letterSpacing = 0.5.sp,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
)
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onAppIconClick() }
.padding(horizontal = 16.dp, vertical = 14.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Change App Icon",
fontSize = 16.sp,
color = textColor
)
Icon(
imageVector = TablerIcons.ChevronRight,
contentDescription = null,
tint = secondaryTextColor,
modifier = Modifier.size(20.dp)
)
}
Text(
text = "Disguise Rosetta as a calculator, weather app, or notes.",
fontSize = 13.sp,
color = secondaryTextColor,
modifier = Modifier.padding(horizontal = 16.dp),
lineHeight = 18.sp
)
Spacer(modifier = Modifier.height(32.dp))
}
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#795548"/>
</shape>

View File

@@ -0,0 +1,38 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<!-- Calculator — Google Calculator style: simple, bold, white on color -->
<group android:translateX="30" android:translateY="24">
<!-- = sign (equals, large, centered, bold) -->
<path
android:fillColor="#FFFFFF"
android:pathData="M6,22h36v5H6z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M6,33h36v5H6z" />
<!-- + sign (plus, smaller, top right) -->
<path
android:fillColor="#FFFFFF"
android:alpha="0.7"
android:pathData="M34,2h4v16h-4z" />
<path
android:fillColor="#FFFFFF"
android:alpha="0.7"
android:pathData="M28,8h16v4H28z" />
<!-- ÷ sign (divide, bottom) -->
<path
android:fillColor="#FFFFFF"
android:alpha="0.7"
android:pathData="M6,50h36v4H6z" />
<path
android:fillColor="#FFFFFF"
android:alpha="0.7"
android:pathData="M22,43a3,3,0,1,1,-3,3a3,3,0,0,1,3,-3z" />
<path
android:fillColor="#FFFFFF"
android:alpha="0.7"
android:pathData="M22,57a3,3,0,1,1,-3,3a3,3,0,0,1,3,-3z" />
</group>
</vector>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#FFC107"/>
</shape>

View File

@@ -0,0 +1,52 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<!-- Notes — Google Keep style: white card with colored pin/accent -->
<group android:translateX="26" android:translateY="20">
<!-- Card shadow -->
<path
android:fillColor="#00000020"
android:pathData="M5,5h46c2.8,0 5,2.2 5,5v54c0,2.8 -2.2,5 -5,5H5c-2.8,0 -5,-2.2 -5,-5V10C0,7.2 2.2,5 5,5z" />
<!-- White card -->
<path
android:fillColor="#FFFFFF"
android:pathData="M5,2h46c2.8,0 5,2.2 5,5v54c0,2.8 -2.2,5 -5,5H5c-2.8,0 -5,-2.2 -5,-5V7C0,4.2 2.2,2 5,2z" />
<!-- Checkbox checked -->
<path
android:fillColor="#FFA000"
android:pathData="M6,14h6v6H6z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M7.5,17.5l1.5,1.5l3.5,-3.5"
android:strokeColor="#FFFFFF"
android:strokeWidth="1.5"/>
<!-- Text line 1 (next to checkbox) -->
<path android:fillColor="#757575" android:pathData="M16,15h32v3H16z" />
<!-- Checkbox unchecked -->
<path
android:fillColor="#00000000"
android:strokeColor="#BDBDBD"
android:strokeWidth="1.5"
android:pathData="M6,28h6v6H6z" />
<!-- Text line 2 -->
<path android:fillColor="#BDBDBD" android:pathData="M16,29h28v3H16z" />
<!-- Checkbox unchecked -->
<path
android:fillColor="#00000000"
android:strokeColor="#BDBDBD"
android:strokeWidth="1.5"
android:pathData="M6,42h6v6H6z" />
<!-- Text line 3 -->
<path android:fillColor="#BDBDBD" android:pathData="M16,43h22v3H16z" />
<!-- Checkbox unchecked -->
<path
android:fillColor="#00000000"
android:strokeColor="#BDBDBD"
android:strokeWidth="1.5"
android:pathData="M6,56h6v6H6z" />
<!-- Text line 4 -->
<path android:fillColor="#BDBDBD" android:pathData="M16,57h18v3H16z" />
</group>
</vector>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#42A5F5"/>
</shape>

View File

@@ -0,0 +1,32 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<!-- Weather — sun with cloud, Material You style, centered in safe zone -->
<group android:translateX="20" android:translateY="20">
<!-- Sun glow (soft circle) -->
<path
android:fillColor="#FFF9C4"
android:pathData="M34,24a14,14,0,1,1,-14,14a14,14,0,0,1,14,-14z" />
<!-- Sun core -->
<path
android:fillColor="#FFB300"
android:pathData="M34,28a10,10,0,1,1,-10,10a10,10,0,0,1,10,-10z" />
<!-- Sun rays -->
<path
android:fillColor="#FFB300"
android:pathData="M33,12h2v8h-2zM33,48h2v8h-2zM12,37v-2h8v2zM48,37v-2h8v2z" />
<path
android:fillColor="#FFB300"
android:pathData="M18.3,18.3l1.4,1.4l5.7,5.7l-1.4,1.4l-5.7,-5.7zM42.6,42.6l1.4,1.4l5.7,5.7l-1.4,1.4l-5.7,-5.7zM18.3,49.7l5.7,-5.7l1.4,1.4l-5.7,5.7zM42.6,25.4l5.7,-5.7l1.4,1.4l-5.7,5.7z" />
<!-- Cloud (in front of sun) -->
<path
android:fillColor="#FFFFFF"
android:pathData="M52,44c4.4,0 8,3.6 8,8s-3.6,8 -8,8H20c-5.5,0 -10,-4.5 -10,-10s4.5,-10 10,-10c0.5,0 1,0 1.5,0.1C23.2,35.2 28,32 34,32c6.4,0 11.7,4.4 13.2,10.3C49,42.1 50.5,42 52,44z" />
<!-- Cloud shadow hint -->
<path
android:fillColor="#E0E0E0"
android:pathData="M20,58h32c2.5,0 4.8,-0.8 6.6,-2H14C16,57 17.9,58 20,58z" />
</group>
</vector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_calc_background"/>
<foreground android:drawable="@drawable/ic_calc_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_notes_background"/>
<foreground android:drawable="@drawable/ic_notes_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_weather_background"/>
<foreground android:drawable="@drawable/ic_weather_foreground"/>
</adaptive-icon>