Смена иконки приложения — калькулятор, погода, заметки + экран выбора в настройка
This commit is contained in:
@@ -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 } },
|
||||
|
||||
@@ -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
|
||||
// ═════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user