feat: simplify color handling in ChatsListScreen and improve gesture callbacks in SwipeableDialogItem
This commit is contained in:
@@ -14,6 +14,7 @@
|
|||||||
<application
|
<application
|
||||||
android:name=".RosettaApplication"
|
android:name=".RosettaApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
android:enableOnBackInvokedCallback="false"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
|||||||
@@ -331,6 +331,56 @@ class MainActivity : FragmentActivity() {
|
|||||||
accountManager.logout()
|
accountManager.logout()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onDeleteAccount = {
|
||||||
|
val publicKey = currentAccount?.publicKey ?: return@MainScreen
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
val database = RosettaDatabase.getDatabase(this@MainActivity)
|
||||||
|
// 1. Delete all messages
|
||||||
|
database.messageDao().deleteAllByAccount(publicKey)
|
||||||
|
// 2. Delete all dialogs
|
||||||
|
database.dialogDao().deleteAllByAccount(publicKey)
|
||||||
|
// 3. Delete blacklist
|
||||||
|
database.blacklistDao().deleteAllByAccount(publicKey)
|
||||||
|
// 4. Delete avatars from DB
|
||||||
|
database.avatarDao().deleteAvatars(publicKey)
|
||||||
|
// 5. Delete account from Room DB
|
||||||
|
database.accountDao().deleteAccount(publicKey)
|
||||||
|
// 6. Disconnect protocol
|
||||||
|
com.rosetta.messenger.network.ProtocolManager.disconnect()
|
||||||
|
// 7. Delete account from AccountManager DataStore (removes from accounts list + clears login)
|
||||||
|
accountManager.deleteAccount(publicKey)
|
||||||
|
// 8. Refresh accounts list
|
||||||
|
val accounts = accountManager.getAllAccounts()
|
||||||
|
accountInfoList = accounts.map { acc ->
|
||||||
|
val shortKey = acc.publicKey.take(7)
|
||||||
|
val displayName = acc.name ?: shortKey
|
||||||
|
val initials = displayName.trim()
|
||||||
|
.split(Regex("\\s+"))
|
||||||
|
.filter { it.isNotEmpty() }
|
||||||
|
.let { words ->
|
||||||
|
when {
|
||||||
|
words.isEmpty() -> "??"
|
||||||
|
words.size == 1 -> words[0].take(2).uppercase()
|
||||||
|
else -> "${words[0].first()}${words[1].first()}".uppercase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AccountInfo(
|
||||||
|
id = acc.publicKey,
|
||||||
|
name = displayName,
|
||||||
|
username = acc.username ?: "",
|
||||||
|
initials = initials,
|
||||||
|
publicKey = acc.publicKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
hasExistingAccount = accounts.isNotEmpty()
|
||||||
|
// 8. Navigate away last
|
||||||
|
currentAccount = null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("DeleteAccount", "Failed to delete account", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
onAccountInfoUpdated = {
|
onAccountInfoUpdated = {
|
||||||
// Reload account list when profile is updated
|
// Reload account list when profile is updated
|
||||||
val accounts = accountManager.getAllAccounts()
|
val accounts = accountManager.getAllAccounts()
|
||||||
@@ -509,6 +559,7 @@ fun MainScreen(
|
|||||||
onToggleTheme: () -> Unit = {},
|
onToggleTheme: () -> Unit = {},
|
||||||
onThemeModeChange: (String) -> Unit = {},
|
onThemeModeChange: (String) -> Unit = {},
|
||||||
onLogout: () -> Unit = {},
|
onLogout: () -> Unit = {},
|
||||||
|
onDeleteAccount: () -> Unit = {},
|
||||||
onAccountInfoUpdated: suspend () -> Unit = {}
|
onAccountInfoUpdated: suspend () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
// Reactive state for account name and username
|
// Reactive state for account name and username
|
||||||
@@ -758,7 +809,6 @@ fun MainScreen(
|
|||||||
onNavigateToAppearance = { pushScreen(Screen.Appearance) },
|
onNavigateToAppearance = { pushScreen(Screen.Appearance) },
|
||||||
onNavigateToSafety = { pushScreen(Screen.Safety) },
|
onNavigateToSafety = { pushScreen(Screen.Safety) },
|
||||||
onNavigateToLogs = { pushScreen(Screen.Logs) },
|
onNavigateToLogs = { pushScreen(Screen.Logs) },
|
||||||
onNavigateToCrashLogs = { pushScreen(Screen.CrashLogs) },
|
|
||||||
onNavigateToBiometric = { pushScreen(Screen.Biometric) },
|
onNavigateToBiometric = { pushScreen(Screen.Biometric) },
|
||||||
viewModel = profileViewModel,
|
viewModel = profileViewModel,
|
||||||
avatarRepository = avatarRepository,
|
avatarRepository = avatarRepository,
|
||||||
@@ -770,13 +820,13 @@ fun MainScreen(
|
|||||||
// Other screens with swipe back
|
// Other screens with swipe back
|
||||||
SwipeBackContainer(
|
SwipeBackContainer(
|
||||||
isVisible = isBackupVisible,
|
isVisible = isBackupVisible,
|
||||||
onBack = { navStack = navStack.filterNot { it is Screen.Backup } + Screen.Safety },
|
onBack = { navStack = navStack.filterNot { it is Screen.Backup } },
|
||||||
isDarkTheme = isDarkTheme
|
isDarkTheme = isDarkTheme
|
||||||
) {
|
) {
|
||||||
BackupScreen(
|
BackupScreen(
|
||||||
isDarkTheme = isDarkTheme,
|
isDarkTheme = isDarkTheme,
|
||||||
onBack = {
|
onBack = {
|
||||||
navStack = navStack.filterNot { it is Screen.Backup } + Screen.Safety
|
navStack = navStack.filterNot { it is Screen.Backup }
|
||||||
},
|
},
|
||||||
onVerifyPassword = { password ->
|
onVerifyPassword = { password ->
|
||||||
// Verify password by trying to decrypt the private key
|
// Verify password by trying to decrypt the private key
|
||||||
@@ -824,11 +874,9 @@ fun MainScreen(
|
|||||||
accountPrivateKey = accountPrivateKey,
|
accountPrivateKey = accountPrivateKey,
|
||||||
onBack = { navStack = navStack.filterNot { it is Screen.Safety } },
|
onBack = { navStack = navStack.filterNot { it is Screen.Safety } },
|
||||||
onBackupClick = {
|
onBackupClick = {
|
||||||
navStack = navStack.filterNot { it is Screen.Safety } + Screen.Backup
|
navStack = navStack + Screen.Backup
|
||||||
},
|
},
|
||||||
onDeleteAccount = {
|
onDeleteAccount = onDeleteAccount
|
||||||
// TODO: Implement account deletion
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -893,8 +941,13 @@ fun MainScreen(
|
|||||||
currentUserName = accountName,
|
currentUserName = accountName,
|
||||||
onBack = { popChatAndChildren() },
|
onBack = { popChatAndChildren() },
|
||||||
onUserProfileClick = { user ->
|
onUserProfileClick = { user ->
|
||||||
|
if (user.publicKey == accountPublicKey) {
|
||||||
|
// Свой профиль — открываем My Profile
|
||||||
|
pushScreen(Screen.Profile)
|
||||||
|
} else {
|
||||||
// Открываем профиль другого пользователя
|
// Открываем профиль другого пользователя
|
||||||
pushScreen(Screen.OtherProfile(user))
|
pushScreen(Screen.OtherProfile(user))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onNavigateToChat = { forwardUser ->
|
onNavigateToChat = { forwardUser ->
|
||||||
// 📨 Forward: переход в выбранный чат с полными данными
|
// 📨 Forward: переход в выбранный чат с полными данными
|
||||||
@@ -926,6 +979,9 @@ fun MainScreen(
|
|||||||
navStack =
|
navStack =
|
||||||
navStack.filterNot { it is Screen.Search } +
|
navStack.filterNot { it is Screen.Search } +
|
||||||
Screen.ChatDetail(selectedSearchUser)
|
Screen.ChatDetail(selectedSearchUser)
|
||||||
|
},
|
||||||
|
onNavigateToCrashLogs = {
|
||||||
|
navStack = navStack.filterNot { it is Screen.Search } + Screen.CrashLogs
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,6 +118,32 @@ class AccountManager(private val context: Context) {
|
|||||||
saveAccount(updatedAccount)
|
saveAccount(updatedAccount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete account completely - remove from accounts list and clear login state
|
||||||
|
*/
|
||||||
|
suspend fun deleteAccount(publicKey: String) {
|
||||||
|
context.accountDataStore.edit { preferences ->
|
||||||
|
// Remove from accounts list
|
||||||
|
val existingJson = preferences[ACCOUNTS_JSON]
|
||||||
|
if (existingJson != null) {
|
||||||
|
val accounts = parseAccounts(existingJson).toMutableList()
|
||||||
|
accounts.removeAll { it.publicKey == publicKey }
|
||||||
|
preferences[ACCOUNTS_JSON] = serializeAccounts(accounts)
|
||||||
|
}
|
||||||
|
// Clear current login if this was the active account
|
||||||
|
val currentKey = preferences[CURRENT_PUBLIC_KEY]
|
||||||
|
if (currentKey == publicKey) {
|
||||||
|
preferences[IS_LOGGED_IN] = false
|
||||||
|
preferences.remove(CURRENT_PUBLIC_KEY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Clear SharedPreferences if this was the last logged account
|
||||||
|
val lastLogged = sharedPrefs.getString(KEY_LAST_LOGGED, null)
|
||||||
|
if (lastLogged == publicKey) {
|
||||||
|
sharedPrefs.edit().remove(KEY_LAST_LOGGED).commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun clearAll() {
|
suspend fun clearAll() {
|
||||||
context.accountDataStore.edit { it.clear() }
|
context.accountDataStore.edit { it.clear() }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,4 +64,8 @@ interface BlacklistDao {
|
|||||||
*/
|
*/
|
||||||
@Query("SELECT public_key FROM blacklist WHERE account = :account")
|
@Query("SELECT public_key FROM blacklist WHERE account = :account")
|
||||||
suspend fun getBlockedPublicKeys(account: String): List<String>
|
suspend fun getBlockedPublicKeys(account: String): List<String>
|
||||||
|
|
||||||
|
/** Удалить все записи blacklist для аккаунта */
|
||||||
|
@Query("DELETE FROM blacklist WHERE account = :account")
|
||||||
|
suspend fun deleteAllByAccount(account: String)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,6 +277,10 @@ interface MessageDao {
|
|||||||
)
|
)
|
||||||
suspend fun deleteMessagesBetweenUsers(account: String, user1: String, user2: String): Int
|
suspend fun deleteMessagesBetweenUsers(account: String, user1: String, user2: String): Int
|
||||||
|
|
||||||
|
/** Удалить все сообщения аккаунта */
|
||||||
|
@Query("DELETE FROM messages WHERE account = :account")
|
||||||
|
suspend fun deleteAllByAccount(account: String): Int
|
||||||
|
|
||||||
/** Количество непрочитанных сообщений в диалоге */
|
/** Количество непрочитанных сообщений в диалоге */
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
@@ -492,6 +496,10 @@ interface DialogDao {
|
|||||||
@Query("DELETE FROM dialogs WHERE account = :account AND opponent_key = :opponentKey")
|
@Query("DELETE FROM dialogs WHERE account = :account AND opponent_key = :opponentKey")
|
||||||
suspend fun deleteDialog(account: String, opponentKey: String)
|
suspend fun deleteDialog(account: String, opponentKey: String)
|
||||||
|
|
||||||
|
/** Удалить все диалоги аккаунта */
|
||||||
|
@Query("DELETE FROM dialogs WHERE account = :account")
|
||||||
|
suspend fun deleteAllByAccount(account: String)
|
||||||
|
|
||||||
/** Обновить информацию о собеседнике */
|
/** Обновить информацию о собеседнике */
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -2267,17 +2267,7 @@ fun ChatDetailScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🐛 Debug Logs BottomSheet
|
// Forward Chat Picker BottomSheet
|
||||||
if (showDebugLogs) {
|
|
||||||
DebugLogsBottomSheet(
|
|
||||||
logs = debugLogs,
|
|
||||||
isDarkTheme = isDarkTheme,
|
|
||||||
onDismiss = { showDebugLogs = false },
|
|
||||||
onClearLogs = { ProtocolManager.clearLogs() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 📨 Forward Chat Picker BottomSheet
|
|
||||||
if (showForwardPicker) {
|
if (showForwardPicker) {
|
||||||
ForwardChatPickerBottomSheet(
|
ForwardChatPickerBottomSheet(
|
||||||
dialogs = dialogsList,
|
dialogs = dialogsList,
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import com.rosetta.messenger.ui.components.AvatarImage
|
|||||||
import com.rosetta.messenger.ui.components.BlurredAvatarBackground
|
import com.rosetta.messenger.ui.components.BlurredAvatarBackground
|
||||||
import com.rosetta.messenger.ui.components.VerifiedBadge
|
import com.rosetta.messenger.ui.components.VerifiedBadge
|
||||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||||
|
import com.rosetta.messenger.ui.onboarding.PrimaryBlueDark
|
||||||
import com.rosetta.messenger.ui.settings.BackgroundBlurPresets
|
import com.rosetta.messenger.ui.settings.BackgroundBlurPresets
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
@@ -237,9 +238,6 @@ fun ChatsListScreen(
|
|||||||
val textColor = remember(isDarkTheme) { if (isDarkTheme) Color.White else Color.Black }
|
val textColor = remember(isDarkTheme) { if (isDarkTheme) Color.White else Color.Black }
|
||||||
val secondaryTextColor =
|
val secondaryTextColor =
|
||||||
remember(isDarkTheme) { if (isDarkTheme) Color(0xFF8E8E8E) else Color(0xFF8E8E93) }
|
remember(isDarkTheme) { if (isDarkTheme) Color(0xFF8E8E8E) else Color(0xFF8E8E93) }
|
||||||
val drawerGrabZonePx = with(androidx.compose.ui.platform.LocalDensity.current) { 88.dp.toPx() }
|
|
||||||
val drawerOpenDistancePx = with(androidx.compose.ui.platform.LocalDensity.current) { 20.dp.toPx() }
|
|
||||||
val drawerOpenVelocityThresholdPx = with(androidx.compose.ui.platform.LocalDensity.current) { 110.dp.toPx() }
|
|
||||||
|
|
||||||
// Protocol connection state
|
// Protocol connection state
|
||||||
val protocolState by ProtocolManager.state.collectAsState()
|
val protocolState by ProtocolManager.state.collectAsState()
|
||||||
@@ -277,7 +275,7 @@ fun ChatsListScreen(
|
|||||||
var visible by rememberSaveable { mutableStateOf(true) }
|
var visible by rememberSaveable { mutableStateOf(true) }
|
||||||
|
|
||||||
// Confirmation dialogs state
|
// Confirmation dialogs state
|
||||||
var dialogToDelete by remember { mutableStateOf<DialogUiModel?>(null) }
|
var dialogsToDelete by remember { mutableStateOf<List<DialogUiModel>>(emptyList()) }
|
||||||
var dialogToBlock by remember { mutableStateOf<DialogUiModel?>(null) }
|
var dialogToBlock by remember { mutableStateOf<DialogUiModel?>(null) }
|
||||||
var dialogToUnblock by remember { mutableStateOf<DialogUiModel?>(null) }
|
var dialogToUnblock by remember { mutableStateOf<DialogUiModel?>(null) }
|
||||||
|
|
||||||
@@ -290,12 +288,16 @@ fun ChatsListScreen(
|
|||||||
val mutedChats by preferencesManager.mutedChatsForAccount(accountPublicKey)
|
val mutedChats by preferencesManager.mutedChatsForAccount(accountPublicKey)
|
||||||
.collectAsState(initial = emptySet())
|
.collectAsState(initial = emptySet())
|
||||||
|
|
||||||
// Перехватываем системный back gesture - не закрываем приложение
|
val activity = context as? android.app.Activity
|
||||||
|
|
||||||
|
// Всегда перехватываем back чтобы predictive back анимация не ломала UI
|
||||||
BackHandler(enabled = true) {
|
BackHandler(enabled = true) {
|
||||||
if (isSelectionMode) {
|
if (isSelectionMode) {
|
||||||
selectedChatKeys = emptySet()
|
selectedChatKeys = emptySet()
|
||||||
} else if (drawerState.isOpen) {
|
} else if (drawerState.isOpen) {
|
||||||
scope.launch { drawerState.close() }
|
scope.launch { drawerState.close() }
|
||||||
|
} else {
|
||||||
|
activity?.moveTaskToBack(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,96 +449,10 @@ fun ChatsListScreen(
|
|||||||
Modifier.fillMaxSize()
|
Modifier.fillMaxSize()
|
||||||
.background(backgroundColor)
|
.background(backgroundColor)
|
||||||
.navigationBarsPadding()
|
.navigationBarsPadding()
|
||||||
.pointerInput(drawerState.isOpen, showRequestsScreen) {
|
|
||||||
if (showRequestsScreen) return@pointerInput
|
|
||||||
|
|
||||||
val velocityTracker = VelocityTracker()
|
|
||||||
val relaxedTouchSlop = viewConfiguration.touchSlop * 0.8f
|
|
||||||
|
|
||||||
awaitEachGesture {
|
|
||||||
val down =
|
|
||||||
awaitFirstDown(requireUnconsumed = false)
|
|
||||||
|
|
||||||
if (drawerState.isOpen || down.position.x > drawerGrabZonePx) {
|
|
||||||
return@awaitEachGesture
|
|
||||||
}
|
|
||||||
|
|
||||||
velocityTracker.resetTracking()
|
|
||||||
velocityTracker.addPosition(
|
|
||||||
down.uptimeMillis,
|
|
||||||
down.position
|
|
||||||
)
|
|
||||||
var totalDragX = 0f
|
|
||||||
var totalDragY = 0f
|
|
||||||
var claimed = false
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
val event = awaitPointerEvent()
|
|
||||||
val change =
|
|
||||||
event.changes.firstOrNull {
|
|
||||||
it.id == down.id
|
|
||||||
}
|
|
||||||
?: break
|
|
||||||
|
|
||||||
if (change.changedToUpIgnoreConsumed()) break
|
|
||||||
|
|
||||||
val delta = change.positionChange()
|
|
||||||
totalDragX += delta.x
|
|
||||||
totalDragY += delta.y
|
|
||||||
velocityTracker.addPosition(
|
|
||||||
change.uptimeMillis,
|
|
||||||
change.position
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!claimed) {
|
|
||||||
val distance =
|
|
||||||
kotlin.math.sqrt(
|
|
||||||
totalDragX *
|
|
||||||
totalDragX +
|
|
||||||
totalDragY *
|
|
||||||
totalDragY
|
|
||||||
)
|
|
||||||
if (distance < relaxedTouchSlop)
|
|
||||||
continue
|
|
||||||
|
|
||||||
val horizontalDominance =
|
|
||||||
kotlin.math.abs(
|
|
||||||
totalDragX
|
|
||||||
) >
|
|
||||||
kotlin.math.abs(
|
|
||||||
totalDragY
|
|
||||||
) * 1.15f
|
|
||||||
if (
|
|
||||||
totalDragX > 0 &&
|
|
||||||
horizontalDominance
|
|
||||||
) {
|
|
||||||
claimed = true
|
|
||||||
change.consume()
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
change.consume()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val velocityX = velocityTracker.calculateVelocity().x
|
|
||||||
val shouldOpenDrawer =
|
|
||||||
claimed &&
|
|
||||||
(totalDragX >=
|
|
||||||
drawerOpenDistancePx ||
|
|
||||||
velocityX >
|
|
||||||
drawerOpenVelocityThresholdPx)
|
|
||||||
|
|
||||||
if (shouldOpenDrawer && drawerState.isClosed) {
|
|
||||||
scope.launch { drawerState.open() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
ModalNavigationDrawer(
|
ModalNavigationDrawer(
|
||||||
drawerState = drawerState,
|
drawerState = drawerState,
|
||||||
gesturesEnabled = !showRequestsScreen, // Disable drawer swipe when requests are open
|
gesturesEnabled = !showRequestsScreen,
|
||||||
drawerContent = {
|
drawerContent = {
|
||||||
ModalDrawerSheet(
|
ModalDrawerSheet(
|
||||||
drawerContainerColor = Color.Transparent,
|
drawerContainerColor = Color.Transparent,
|
||||||
@@ -571,7 +487,8 @@ fun ChatsListScreen(
|
|||||||
BackgroundBlurPresets
|
BackgroundBlurPresets
|
||||||
.getOverlayColors(
|
.getOverlayColors(
|
||||||
backgroundBlurColorId
|
backgroundBlurColorId
|
||||||
)
|
),
|
||||||
|
isDarkTheme = isDarkTheme
|
||||||
)
|
)
|
||||||
|
|
||||||
// Content поверх фона
|
// Content поверх фона
|
||||||
@@ -634,11 +551,7 @@ fun ChatsListScreen(
|
|||||||
contentDescription =
|
contentDescription =
|
||||||
if (isDarkTheme) "Light Mode"
|
if (isDarkTheme) "Light Mode"
|
||||||
else "Dark Mode",
|
else "Dark Mode",
|
||||||
tint =
|
tint = Color.White,
|
||||||
if (isDarkTheme)
|
|
||||||
Color.White.copy(alpha = 0.8f)
|
|
||||||
else
|
|
||||||
Color.Black.copy(alpha = 0.7f),
|
|
||||||
modifier = Modifier.size(22.dp)
|
modifier = Modifier.size(22.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -646,7 +559,7 @@ fun ChatsListScreen(
|
|||||||
|
|
||||||
Spacer(
|
Spacer(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.height(14.dp)
|
Modifier.height(8.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Display name
|
// Display name
|
||||||
@@ -656,11 +569,7 @@ fun ChatsListScreen(
|
|||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight =
|
fontWeight =
|
||||||
FontWeight.SemiBold,
|
FontWeight.SemiBold,
|
||||||
color =
|
color = Color.White
|
||||||
if (isDarkTheme)
|
|
||||||
Color.White
|
|
||||||
else
|
|
||||||
Color.Black
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -674,13 +583,7 @@ fun ChatsListScreen(
|
|||||||
text =
|
text =
|
||||||
"@$accountUsername",
|
"@$accountUsername",
|
||||||
fontSize = 13.sp,
|
fontSize = 13.sp,
|
||||||
color =
|
color = Color.White
|
||||||
if (isDarkTheme)
|
|
||||||
Color.White
|
|
||||||
.copy(alpha = 0.7f)
|
|
||||||
else
|
|
||||||
Color.Black
|
|
||||||
.copy(alpha = 0.7f)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -699,7 +602,10 @@ fun ChatsListScreen(
|
|||||||
.padding(vertical = 8.dp)
|
.padding(vertical = 8.dp)
|
||||||
) {
|
) {
|
||||||
val menuIconColor =
|
val menuIconColor =
|
||||||
textColor.copy(alpha = 0.6f)
|
if (isDarkTheme) Color(0xFF7A7F85)
|
||||||
|
else textColor.copy(alpha = 0.6f)
|
||||||
|
|
||||||
|
val accentColor = if (isDarkTheme) PrimaryBlueDark else PrimaryBlue
|
||||||
|
|
||||||
// 👤 Profile
|
// 👤 Profile
|
||||||
DrawerMenuItemEnhanced(
|
DrawerMenuItemEnhanced(
|
||||||
@@ -826,6 +732,7 @@ fun ChatsListScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
key(isDarkTheme, showRequestsScreen, isSelectionMode) {
|
key(isDarkTheme, showRequestsScreen, isSelectionMode) {
|
||||||
@@ -876,9 +783,8 @@ fun ChatsListScreen(
|
|||||||
// Delete
|
// Delete
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
val allDialogs = topLevelChatsState.dialogs
|
val allDialogs = topLevelChatsState.dialogs
|
||||||
val first = selectedChatKeys.firstOrNull()
|
val selected = allDialogs.filter { selectedChatKeys.contains(it.opponentKey) }
|
||||||
val dlg = allDialogs.find { it.opponentKey == first }
|
if (selected.isNotEmpty()) dialogsToDelete = selected
|
||||||
if (dlg != null) dialogToDelete = dlg
|
|
||||||
selectedChatKeys = emptySet()
|
selectedChatKeys = emptySet()
|
||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -956,8 +862,8 @@ fun ChatsListScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
containerColor = if (isDarkTheme) Color(0xFF043359) else Color(0xFF0D8CF4),
|
containerColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFF0D8CF4),
|
||||||
scrolledContainerColor = if (isDarkTheme) Color(0xFF043359) else Color(0xFF0D8CF4),
|
scrolledContainerColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFF0D8CF4),
|
||||||
navigationIconContentColor = Color.White,
|
navigationIconContentColor = Color.White,
|
||||||
titleContentColor = Color.White,
|
titleContentColor = Color.White,
|
||||||
actionIconContentColor = Color.White
|
actionIconContentColor = Color.White
|
||||||
@@ -1089,9 +995,9 @@ fun ChatsListScreen(
|
|||||||
colors =
|
colors =
|
||||||
TopAppBarDefaults.topAppBarColors(
|
TopAppBarDefaults.topAppBarColors(
|
||||||
containerColor =
|
containerColor =
|
||||||
if (isDarkTheme) Color(0xFF043359) else Color(0xFF0D8CF4),
|
if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFF0D8CF4),
|
||||||
scrolledContainerColor =
|
scrolledContainerColor =
|
||||||
if (isDarkTheme) Color(0xFF043359) else Color(0xFF0D8CF4),
|
if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFF0D8CF4),
|
||||||
navigationIconContentColor =
|
navigationIconContentColor =
|
||||||
Color.White,
|
Color.White,
|
||||||
titleContentColor =
|
titleContentColor =
|
||||||
@@ -1493,8 +1399,8 @@ fun ChatsListScreen(
|
|||||||
selectedChatKeys + dialog.opponentKey
|
selectedChatKeys + dialog.opponentKey
|
||||||
},
|
},
|
||||||
onDelete = {
|
onDelete = {
|
||||||
dialogToDelete =
|
dialogsToDelete =
|
||||||
dialog
|
listOf(dialog)
|
||||||
},
|
},
|
||||||
onBlock = {
|
onBlock = {
|
||||||
dialogToBlock =
|
dialogToBlock =
|
||||||
@@ -1539,44 +1445,49 @@ fun ChatsListScreen(
|
|||||||
// Console button removed
|
// Console button removed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} // Close content Box
|
||||||
} // Close ModalNavigationDrawer
|
} // Close ModalNavigationDrawer
|
||||||
|
|
||||||
// 🔥 Confirmation Dialogs
|
// 🔥 Confirmation Dialogs
|
||||||
|
|
||||||
// Delete Dialog Confirmation
|
// Delete Dialog Confirmation
|
||||||
dialogToDelete?.let { dialog ->
|
if (dialogsToDelete.isNotEmpty()) {
|
||||||
|
val count = dialogsToDelete.size
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { dialogToDelete = null },
|
onDismissRequest = { dialogsToDelete = emptyList() },
|
||||||
containerColor =
|
containerColor =
|
||||||
if (isDarkTheme) Color(0xFF2C2C2E) else Color.White,
|
if (isDarkTheme) Color(0xFF2C2C2E) else Color.White,
|
||||||
title = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
"Delete Chat",
|
if (count == 1) "Delete Chat" else "Delete $count Chats",
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = textColor
|
color = textColor
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
Text(
|
Text(
|
||||||
"Are you sure you want to delete this chat? This action cannot be undone.",
|
if (count == 1) "Are you sure you want to delete this chat? This action cannot be undone."
|
||||||
|
else "Are you sure you want to delete $count chats? This action cannot be undone.",
|
||||||
color = secondaryTextColor
|
color = secondaryTextColor
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
val opponentKey = dialog.opponentKey
|
val toDelete = dialogsToDelete.toList()
|
||||||
dialogToDelete = null
|
dialogsToDelete = emptyList()
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
toDelete.forEach { dialog ->
|
||||||
chatsViewModel.deleteDialog(
|
chatsViewModel.deleteDialog(
|
||||||
opponentKey
|
dialog.opponentKey
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
) { Text("Delete", color = Color(0xFFFF3B30)) }
|
) { Text("Delete", color = Color(0xFFFF3B30)) }
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = { dialogToDelete = null }) {
|
TextButton(onClick = { dialogsToDelete = emptyList() }) {
|
||||||
Text("Cancel", color = PrimaryBlue)
|
Text("Cancel", color = PrimaryBlue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1662,6 +1573,7 @@ fun ChatsListScreen(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
} // Close Box
|
} // Close Box
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2272,6 +2184,9 @@ fun SwipeableDialogItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. КОНТЕНТ - поверх кнопок, сдвигается при свайпе
|
// 2. КОНТЕНТ - поверх кнопок, сдвигается при свайпе
|
||||||
|
// 🔥 rememberUpdatedState чтобы pointerInput всегда вызывал актуальные callbacks
|
||||||
|
val currentOnClick by rememberUpdatedState(onClick)
|
||||||
|
val currentOnLongClick by rememberUpdatedState(onLongClick)
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxSize()
|
Modifier.fillMaxSize()
|
||||||
@@ -2338,12 +2253,12 @@ fun SwipeableDialogItem(
|
|||||||
|
|
||||||
when (gestureType) {
|
when (gestureType) {
|
||||||
"tap" -> {
|
"tap" -> {
|
||||||
onClick()
|
currentOnClick()
|
||||||
return@awaitEachGesture
|
return@awaitEachGesture
|
||||||
}
|
}
|
||||||
"cancelled" -> return@awaitEachGesture
|
"cancelled" -> return@awaitEachGesture
|
||||||
"longpress" -> {
|
"longpress" -> {
|
||||||
onLongClick()
|
currentOnLongClick()
|
||||||
// Consume remaining events until finger lifts
|
// Consume remaining events until finger lifts
|
||||||
while (true) {
|
while (true) {
|
||||||
val event = awaitPointerEvent()
|
val event = awaitPointerEvent()
|
||||||
@@ -3317,7 +3232,7 @@ fun DrawerMenuItemEnhanced(
|
|||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Medium,
|
||||||
color = textColor,
|
color = textColor,
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
@@ -3348,7 +3263,7 @@ fun DrawerDivider(isDarkTheme: Boolean) {
|
|||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Divider(
|
Divider(
|
||||||
modifier = Modifier.padding(horizontal = 20.dp),
|
modifier = Modifier.padding(horizontal = 20.dp),
|
||||||
color = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFEEEEEE),
|
color = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFC8C8CC),
|
||||||
thickness = 0.5.dp
|
thickness = 0.5.dp
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|||||||
@@ -225,9 +225,9 @@ fun ForwardChatPickerBottomSheet(
|
|||||||
onClick = {
|
onClick = {
|
||||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||||
selectedChats = if (isSelected) {
|
selectedChats = if (isSelected) {
|
||||||
selectedChats - dialog.opponentKey
|
emptySet()
|
||||||
} else {
|
} else {
|
||||||
selectedChats + dialog.opponentKey
|
setOf(dialog.opponentKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -263,6 +263,7 @@ fun ForwardChatPickerBottomSheet(
|
|||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = PrimaryBlue,
|
containerColor = PrimaryBlue,
|
||||||
|
contentColor = Color.White,
|
||||||
disabledContainerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFD1D1D6),
|
disabledContainerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFD1D1D6),
|
||||||
disabledContentColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF999999)
|
disabledContentColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF999999)
|
||||||
)
|
)
|
||||||
@@ -274,10 +275,7 @@ fun ForwardChatPickerBottomSheet(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = if (hasSelection)
|
text = if (hasSelection) "Forward" else "Select a chat",
|
||||||
"Forward to ${selectedChats.size} chat${if (selectedChats.size > 1) "s" else ""}"
|
|
||||||
else
|
|
||||||
"Select a chat",
|
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.SemiBold
|
fontWeight = FontWeight.SemiBold
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -54,7 +54,8 @@ fun SearchScreen(
|
|||||||
isDarkTheme: Boolean,
|
isDarkTheme: Boolean,
|
||||||
protocolState: ProtocolState,
|
protocolState: ProtocolState,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
onUserSelect: (SearchUser) -> Unit
|
onUserSelect: (SearchUser) -> Unit,
|
||||||
|
onNavigateToCrashLogs: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
// Context и View для мгновенного закрытия клавиатуры
|
// Context и View для мгновенного закрытия клавиатуры
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -84,6 +85,14 @@ fun SearchScreen(
|
|||||||
val searchResults by searchViewModel.searchResults.collectAsState()
|
val searchResults by searchViewModel.searchResults.collectAsState()
|
||||||
val isSearching by searchViewModel.isSearching.collectAsState()
|
val isSearching by searchViewModel.isSearching.collectAsState()
|
||||||
|
|
||||||
|
// Easter egg: navigate to CrashLogs when typing "rosettadev1"
|
||||||
|
LaunchedEffect(searchQuery) {
|
||||||
|
if (searchQuery.trim().equals("rosettadev1", ignoreCase = true)) {
|
||||||
|
searchViewModel.clearSearchQuery()
|
||||||
|
onNavigateToCrashLogs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Always reset query/results when leaving Search screen (back/swipe/navigation).
|
// Always reset query/results when leaving Search screen (back/swipe/navigation).
|
||||||
DisposableEffect(Unit) { onDispose { searchViewModel.clearSearchQuery() } }
|
DisposableEffect(Unit) { onDispose { searchViewModel.clearSearchQuery() } }
|
||||||
|
|
||||||
|
|||||||
@@ -1753,23 +1753,7 @@ fun KebabMenu(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug Logs
|
// Delete chat
|
||||||
KebabMenuItem(
|
|
||||||
icon = TablerIcons.Bug,
|
|
||||||
text = "Debug Logs",
|
|
||||||
onClick = onLogsClick,
|
|
||||||
tintColor = PrimaryBlue,
|
|
||||||
textColor = if (isDarkTheme) Color.White else Color.Black
|
|
||||||
)
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier =
|
|
||||||
Modifier.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp, vertical = 4.dp)
|
|
||||||
.height(0.5.dp)
|
|
||||||
.background(dividerColor)
|
|
||||||
)
|
|
||||||
|
|
||||||
KebabMenuItem(
|
KebabMenuItem(
|
||||||
icon = TablerIcons.Trash,
|
icon = TablerIcons.Trash,
|
||||||
text = "Delete Chat",
|
text = "Delete Chat",
|
||||||
|
|||||||
@@ -38,37 +38,63 @@ fun BoxScope.BlurredAvatarBackground(
|
|||||||
avatarRepository: AvatarRepository?,
|
avatarRepository: AvatarRepository?,
|
||||||
fallbackColor: Color,
|
fallbackColor: Color,
|
||||||
blurRadius: Float = 25f,
|
blurRadius: Float = 25f,
|
||||||
alpha: Float = 0.3f,
|
alpha: Float = 0.9f,
|
||||||
overlayColors: List<Color>? = null
|
overlayColors: List<Color>? = null,
|
||||||
|
isDarkTheme: Boolean = true
|
||||||
) {
|
) {
|
||||||
// Получаем аватары из репозитория
|
// В светлой теме: если дефолтный фон (avatar) — синий как шапка chat list,
|
||||||
|
// если выбран кастомный цвет в Appearance — используем его
|
||||||
|
if (!isDarkTheme) {
|
||||||
|
val lightBgModifier = if (overlayColors != null && overlayColors.isNotEmpty()) {
|
||||||
|
if (overlayColors.size == 1) {
|
||||||
|
Modifier.matchParentSize().background(overlayColors[0])
|
||||||
|
} else {
|
||||||
|
Modifier.matchParentSize().background(
|
||||||
|
Brush.linearGradient(colors = overlayColors)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Modifier.matchParentSize().background(Color(0xFF0D8CF4))
|
||||||
|
}
|
||||||
|
Box(modifier = lightBgModifier)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если выбран цвет в Appearance — просто сплошной цвет/градиент, без blur
|
||||||
|
if (overlayColors != null && overlayColors.isNotEmpty()) {
|
||||||
|
val bgModifier = if (overlayColors.size == 1) {
|
||||||
|
Modifier.matchParentSize().background(overlayColors[0])
|
||||||
|
} else {
|
||||||
|
Modifier.matchParentSize().background(
|
||||||
|
Brush.linearGradient(colors = overlayColors)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Box(modifier = bgModifier)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Нет фона (avatar default) — blur аватарки
|
||||||
val avatars by avatarRepository?.getAvatars(publicKey, allDecode = false)?.collectAsState()
|
val avatars by avatarRepository?.getAvatars(publicKey, allDecode = false)?.collectAsState()
|
||||||
?: remember { mutableStateOf(emptyList()) }
|
?: remember { mutableStateOf(emptyList()) }
|
||||||
|
|
||||||
// Состояние для bitmap и размытого bitmap
|
|
||||||
var originalBitmap by remember(avatars) { mutableStateOf<Bitmap?>(null) }
|
var originalBitmap by remember(avatars) { mutableStateOf<Bitmap?>(null) }
|
||||||
var blurredBitmap by remember(avatars) { mutableStateOf<Bitmap?>(null) }
|
var blurredBitmap by remember(avatars) { mutableStateOf<Bitmap?>(null) }
|
||||||
|
|
||||||
// Декодируем и размываем аватар
|
|
||||||
LaunchedEffect(avatars) {
|
LaunchedEffect(avatars) {
|
||||||
if (avatars.isNotEmpty()) {
|
if (avatars.isNotEmpty()) {
|
||||||
originalBitmap = withContext(Dispatchers.IO) {
|
originalBitmap = withContext(Dispatchers.IO) {
|
||||||
AvatarFileManager.base64ToBitmap(avatars.first().base64Data)
|
AvatarFileManager.base64ToBitmap(avatars.first().base64Data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Размываем bitmap (уменьшаем для производительности, затем применяем blur)
|
|
||||||
originalBitmap?.let { bitmap ->
|
originalBitmap?.let { bitmap ->
|
||||||
blurredBitmap = withContext(Dispatchers.Default) {
|
blurredBitmap = withContext(Dispatchers.Default) {
|
||||||
// Уменьшаем размер для быстрого blur
|
|
||||||
val scaledBitmap = Bitmap.createScaledBitmap(
|
val scaledBitmap = Bitmap.createScaledBitmap(
|
||||||
bitmap,
|
bitmap,
|
||||||
bitmap.width / 4,
|
bitmap.width / 4,
|
||||||
bitmap.height / 4,
|
bitmap.height / 4,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
// Применяем blur несколько раз для более гладкого эффекта
|
|
||||||
var result = scaledBitmap
|
var result = scaledBitmap
|
||||||
repeat(3) {
|
repeat(2) {
|
||||||
result = fastBlur(result, (blurRadius / 4).toInt().coerceAtLeast(1))
|
result = fastBlur(result, (blurRadius / 4).toInt().coerceAtLeast(1))
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
@@ -80,64 +106,16 @@ fun BoxScope.BlurredAvatarBackground(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Используем matchParentSize() чтобы занимать только размер родителя
|
|
||||||
Box(modifier = Modifier.matchParentSize()) {
|
Box(modifier = Modifier.matchParentSize()) {
|
||||||
if (blurredBitmap != null) {
|
if (blurredBitmap != null) {
|
||||||
// Показываем размытое изображение
|
|
||||||
Image(
|
Image(
|
||||||
bitmap = blurredBitmap!!.asImageBitmap(),
|
bitmap = blurredBitmap!!.asImageBitmap(),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize(),
|
||||||
.fillMaxSize()
|
|
||||||
.graphicsLayer {
|
|
||||||
this.alpha = alpha
|
|
||||||
},
|
|
||||||
contentScale = ContentScale.Crop
|
contentScale = ContentScale.Crop
|
||||||
)
|
)
|
||||||
|
|
||||||
// Дополнительный overlay — кастомный или стандартный
|
|
||||||
if (overlayColors != null && overlayColors.isNotEmpty()) {
|
|
||||||
// Кастомный цветной overlay
|
|
||||||
val overlayModifier = if (overlayColors.size == 1) {
|
|
||||||
Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(overlayColors[0].copy(alpha = 0.55f))
|
|
||||||
} else {
|
} else {
|
||||||
Modifier
|
// Нет фото — цвет аватарки
|
||||||
.fillMaxSize()
|
|
||||||
.background(
|
|
||||||
Brush.linearGradient(
|
|
||||||
colors = overlayColors.map { it.copy(alpha = 0.55f) }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Box(modifier = overlayModifier)
|
|
||||||
} else {
|
|
||||||
// Стандартный overlay для затемнения
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(fallbackColor.copy(alpha = 0.3f))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Fallback: когда нет аватарки
|
|
||||||
if (overlayColors != null && overlayColors.isNotEmpty()) {
|
|
||||||
// Кастомный фон без blur
|
|
||||||
val bgModifier = if (overlayColors.size == 1) {
|
|
||||||
Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(overlayColors[0])
|
|
||||||
} else {
|
|
||||||
Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(
|
|
||||||
Brush.linearGradient(colors = overlayColors)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Box(modifier = bgModifier)
|
|
||||||
} else {
|
|
||||||
// Стандартный fallback: цветной фон
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -146,7 +124,6 @@ fun BoxScope.BlurredAvatarBackground(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Быстрое размытие по Гауссу (Box Blur - упрощенная версия)
|
* Быстрое размытие по Гауссу (Box Blur - упрощенная версия)
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ object BackgroundBlurPresets {
|
|||||||
|
|
||||||
/** Сплошные цвета */
|
/** Сплошные цвета */
|
||||||
private val solidColors = listOf(
|
private val solidColors = listOf(
|
||||||
BackgroundBlurOption("solid_blue", listOf(Color(0xFF2979FF)), "Blue"),
|
BackgroundBlurOption("solid_blue", listOf(Color(0xFF0D8CF4)), "Blue"),
|
||||||
BackgroundBlurOption("solid_green", listOf(Color(0xFF4CAF50)), "Green"),
|
BackgroundBlurOption("solid_green", listOf(Color(0xFF4CAF50)), "Green"),
|
||||||
BackgroundBlurOption("solid_orange", listOf(Color(0xFFFF9800)), "Orange"),
|
BackgroundBlurOption("solid_orange", listOf(Color(0xFFFF9800)), "Orange"),
|
||||||
BackgroundBlurOption("solid_red", listOf(Color(0xFFE53935)), "Red"),
|
BackgroundBlurOption("solid_red", listOf(Color(0xFFE53935)), "Red"),
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ fun AppearanceScreen(
|
|||||||
|
|
||||||
BackHandler { onBack() }
|
BackHandler { onBack() }
|
||||||
|
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -105,7 +106,7 @@ fun AppearanceScreen(
|
|||||||
tint = Color.White
|
tint = Color.White
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
IconButton(onClick = onToggleTheme) {
|
IconButton(onClick = { onToggleTheme() }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (isDarkTheme) TablerIcons.Sun else TablerIcons.Moon,
|
imageVector = if (isDarkTheme) TablerIcons.Sun else TablerIcons.Moon,
|
||||||
contentDescription = "Toggle theme",
|
contentDescription = "Toggle theme",
|
||||||
@@ -155,6 +156,7 @@ fun AppearanceScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
@@ -439,7 +441,7 @@ private fun ColorCircleItem(
|
|||||||
|
|
||||||
val borderColor by animateColorAsState(
|
val borderColor by animateColorAsState(
|
||||||
targetValue = if (isSelected) {
|
targetValue = if (isSelected) {
|
||||||
if (isDarkTheme) Color.White else Color(0xFF222222)
|
Color.White
|
||||||
} else {
|
} else {
|
||||||
Color.Transparent
|
Color.Transparent
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -226,6 +226,14 @@ fun OtherProfileScreen(
|
|||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
|
|
||||||
|
// Закрываем клавиатуру при открытии экрана
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
val imm = context.getSystemService(android.content.Context.INPUT_METHOD_SERVICE)
|
||||||
|
as android.view.inputmethod.InputMethodManager
|
||||||
|
imm.hideSoftInputFromWindow(view.windowToken, 0)
|
||||||
|
focusManager.clearFocus()
|
||||||
|
}
|
||||||
|
|
||||||
// 🗑️ Для удаления диалога
|
// 🗑️ Для удаления диалога
|
||||||
val database = remember { RosettaDatabase.getDatabase(context) }
|
val database = remember { RosettaDatabase.getDatabase(context) }
|
||||||
val messageDao = remember { database.messageDao() }
|
val messageDao = remember { database.messageDao() }
|
||||||
@@ -1664,7 +1672,7 @@ private fun CollapsingOtherProfileHeader(
|
|||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
// 🎨 TEXT COLOR - просто по теме: белый в тёмной, чёрный в светлой
|
// 🎨 TEXT COLOR - просто по теме: белый в тёмной, чёрный в светлой
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = Color.White
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxWidth().height(headerHeight)) {
|
Box(modifier = Modifier.fillMaxWidth().height(headerHeight)) {
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
@@ -1674,9 +1682,10 @@ private fun CollapsingOtherProfileHeader(
|
|||||||
publicKey = publicKey,
|
publicKey = publicKey,
|
||||||
avatarRepository = avatarRepository,
|
avatarRepository = avatarRepository,
|
||||||
fallbackColor = avatarColors.backgroundColor,
|
fallbackColor = avatarColors.backgroundColor,
|
||||||
blurRadius = 25f,
|
blurRadius = 20f,
|
||||||
alpha = 0.3f,
|
alpha = 0.9f,
|
||||||
overlayColors = com.rosetta.messenger.ui.settings.BackgroundBlurPresets.getOverlayColors(backgroundBlurColorId)
|
overlayColors = com.rosetta.messenger.ui.settings.BackgroundBlurPresets.getOverlayColors(backgroundBlurColorId),
|
||||||
|
isDarkTheme = isDarkTheme
|
||||||
)
|
)
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
@@ -1730,7 +1739,7 @@ private fun CollapsingOtherProfileHeader(
|
|||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.ArrowBack,
|
imageVector = Icons.Filled.ArrowBack,
|
||||||
contentDescription = "Back",
|
contentDescription = "Back",
|
||||||
tint = if (isDarkTheme) Color.White else Color.Black,
|
tint = Color.White,
|
||||||
modifier = Modifier.size(24.dp)
|
modifier = Modifier.size(24.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1751,7 +1760,7 @@ private fun CollapsingOtherProfileHeader(
|
|||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.MoreVert,
|
imageVector = Icons.Default.MoreVert,
|
||||||
contentDescription = "Profile menu",
|
contentDescription = "Profile menu",
|
||||||
tint = if (isDarkTheme) Color.White else Color.Black,
|
tint = Color.White,
|
||||||
modifier = Modifier.size(24.dp)
|
modifier = Modifier.size(24.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1820,12 +1829,7 @@ private fun CollapsingOtherProfileHeader(
|
|||||||
Text(
|
Text(
|
||||||
text = statusText,
|
text = statusText,
|
||||||
fontSize = onlineFontSize,
|
fontSize = onlineFontSize,
|
||||||
color =
|
color = Color.White
|
||||||
if (isOnline) {
|
|
||||||
Color(0xFF4CAF50)
|
|
||||||
} else {
|
|
||||||
textColor.copy(alpha = 0.7f)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ import com.rosetta.messenger.biometric.BiometricAvailability
|
|||||||
import com.rosetta.messenger.biometric.BiometricPreferences
|
import com.rosetta.messenger.biometric.BiometricPreferences
|
||||||
import com.rosetta.messenger.repository.AvatarRepository
|
import com.rosetta.messenger.repository.AvatarRepository
|
||||||
import com.rosetta.messenger.ui.components.BlurredAvatarBackground
|
import com.rosetta.messenger.ui.components.BlurredAvatarBackground
|
||||||
|
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||||
|
import com.rosetta.messenger.ui.onboarding.PrimaryBlueDark
|
||||||
|
|
||||||
import com.rosetta.messenger.ui.components.metaball.ProfileMetaballEffect
|
import com.rosetta.messenger.ui.components.metaball.ProfileMetaballEffect
|
||||||
import com.rosetta.messenger.utils.AvatarFileManager
|
import com.rosetta.messenger.utils.AvatarFileManager
|
||||||
@@ -259,7 +261,6 @@ fun ProfileScreen(
|
|||||||
onNavigateToAppearance: () -> Unit = {},
|
onNavigateToAppearance: () -> Unit = {},
|
||||||
onNavigateToSafety: () -> Unit = {},
|
onNavigateToSafety: () -> Unit = {},
|
||||||
onNavigateToLogs: () -> Unit = {},
|
onNavigateToLogs: () -> Unit = {},
|
||||||
onNavigateToCrashLogs: () -> Unit = {},
|
|
||||||
onNavigateToBiometric: () -> Unit = {},
|
onNavigateToBiometric: () -> Unit = {},
|
||||||
viewModel: ProfileViewModel = androidx.lifecycle.viewmodel.compose.viewModel(),
|
viewModel: ProfileViewModel = androidx.lifecycle.viewmodel.compose.viewModel(),
|
||||||
avatarRepository: AvatarRepository? = null,
|
avatarRepository: AvatarRepository? = null,
|
||||||
@@ -759,6 +760,31 @@ fun ProfileScreen(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════
|
||||||
|
// 🔔 NOTIFICATIONS SECTION
|
||||||
|
// ═════════════════════════════════════════════════════════════
|
||||||
|
TelegramSectionTitle(title = "Notifications", isDarkTheme = isDarkTheme)
|
||||||
|
|
||||||
|
run {
|
||||||
|
val preferencesManager = remember { com.rosetta.messenger.data.PreferencesManager(context) }
|
||||||
|
val notificationsEnabled by preferencesManager.notificationsEnabled.collectAsState(initial = true)
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
TelegramToggleItem(
|
||||||
|
icon = TablerIcons.Bell,
|
||||||
|
title = "Push Notifications",
|
||||||
|
isEnabled = notificationsEnabled,
|
||||||
|
onToggle = {
|
||||||
|
scope.launch {
|
||||||
|
preferencesManager.setNotificationsEnabled(!notificationsEnabled)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isDarkTheme = isDarkTheme
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
// ═════════════════════════════════════════════════════════════
|
// ═════════════════════════════════════════════════════════════
|
||||||
// ⚙️ SETTINGS SECTION - Telegram style
|
// ⚙️ SETTINGS SECTION - Telegram style
|
||||||
// ═════════════════════════════════════════════════════════════
|
// ═════════════════════════════════════════════════════════════
|
||||||
@@ -785,14 +811,6 @@ fun ProfileScreen(
|
|||||||
title = "Safety",
|
title = "Safety",
|
||||||
onClick = onNavigateToSafety,
|
onClick = onNavigateToSafety,
|
||||||
isDarkTheme = isDarkTheme,
|
isDarkTheme = isDarkTheme,
|
||||||
showDivider = true
|
|
||||||
)
|
|
||||||
|
|
||||||
TelegramSettingsItem(
|
|
||||||
icon = TablerIcons.Bug,
|
|
||||||
title = "Crash Logs",
|
|
||||||
onClick = onNavigateToCrashLogs,
|
|
||||||
isDarkTheme = isDarkTheme,
|
|
||||||
showDivider = biometricAvailable is BiometricAvailability.Available
|
showDivider = biometricAvailable is BiometricAvailability.Available
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -938,7 +956,7 @@ private fun CollapsingProfileHeader(
|
|||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
// 🎨 TEXT COLOR - просто по теме: белый в тёмной, чёрный в светлой
|
// 🎨 TEXT COLOR - просто по теме: белый в тёмной, чёрный в светлой
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = Color.White
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
// 📐 HEADER HEIGHT - ФИКСИРОВАННАЯ! Не меняется при overscroll
|
// 📐 HEADER HEIGHT - ФИКСИРОВАННАЯ! Не меняется при overscroll
|
||||||
@@ -959,7 +977,7 @@ private fun CollapsingProfileHeader(
|
|||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
// 📝 TEXT - внизу header зоны, внутри блока
|
// 📝 TEXT - внизу header зоны, внутри блока
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
val textDefaultY = expandedHeight - 48.dp // Внизу header блока (ближе к низу)
|
val textDefaultY = expandedHeight - 70.dp // Ближе к аватарке
|
||||||
val textCollapsedY = statusBarHeight + COLLAPSED_HEADER_HEIGHT / 2
|
val textCollapsedY = statusBarHeight + COLLAPSED_HEADER_HEIGHT / 2
|
||||||
|
|
||||||
// Текст меняет позицию только при collapse, НЕ при overscroll
|
// Текст меняет позицию только при collapse, НЕ при overscroll
|
||||||
@@ -977,9 +995,10 @@ private fun CollapsingProfileHeader(
|
|||||||
publicKey = publicKey,
|
publicKey = publicKey,
|
||||||
avatarRepository = avatarRepository,
|
avatarRepository = avatarRepository,
|
||||||
fallbackColor = avatarColors.backgroundColor,
|
fallbackColor = avatarColors.backgroundColor,
|
||||||
blurRadius = 25f,
|
blurRadius = 20f,
|
||||||
alpha = 0.3f,
|
alpha = 0.9f,
|
||||||
overlayColors = BackgroundBlurPresets.getOverlayColors(backgroundBlurColorId)
|
overlayColors = BackgroundBlurPresets.getOverlayColors(backgroundBlurColorId),
|
||||||
|
isDarkTheme = isDarkTheme
|
||||||
)
|
)
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
@@ -1033,7 +1052,7 @@ private fun CollapsingProfileHeader(
|
|||||||
Icon(
|
Icon(
|
||||||
imageVector = TablerIcons.ArrowLeft,
|
imageVector = TablerIcons.ArrowLeft,
|
||||||
contentDescription = "Back",
|
contentDescription = "Back",
|
||||||
tint = if (isDarkTheme) Color.White else Color.Black,
|
tint = Color.White,
|
||||||
modifier = Modifier.size(24.dp)
|
modifier = Modifier.size(24.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1055,11 +1074,9 @@ private fun CollapsingProfileHeader(
|
|||||||
text = "Save",
|
text = "Save",
|
||||||
color =
|
color =
|
||||||
if (isSaveEnabled) {
|
if (isSaveEnabled) {
|
||||||
if (isDarkTheme) Color.White else Color.Black
|
Color.White
|
||||||
} else {
|
} else {
|
||||||
(if (isDarkTheme) Color.White else Color.Black).copy(
|
Color.White.copy(alpha = 0.45f)
|
||||||
alpha = 0.45f
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
fontWeight = FontWeight.SemiBold
|
fontWeight = FontWeight.SemiBold
|
||||||
)
|
)
|
||||||
@@ -1074,7 +1091,7 @@ private fun CollapsingProfileHeader(
|
|||||||
Icon(
|
Icon(
|
||||||
imageVector = TablerIcons.DotsVertical,
|
imageVector = TablerIcons.DotsVertical,
|
||||||
contentDescription = "Profile menu",
|
contentDescription = "Profile menu",
|
||||||
tint = if (isDarkTheme) Color.White else Color.Black,
|
tint = Color.White,
|
||||||
modifier = Modifier.size(24.dp)
|
modifier = Modifier.size(24.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1174,7 +1191,8 @@ private fun FullSizeAvatar(
|
|||||||
}
|
}
|
||||||
initialLoadComplete = true
|
initialLoadComplete = true
|
||||||
} else {
|
} else {
|
||||||
// Нет аватарки - помечаем загрузку завершенной
|
// Нет аватарки - сбрасываем bitmap и помечаем загрузку завершенной
|
||||||
|
bitmap = null
|
||||||
initialLoadComplete = true
|
initialLoadComplete = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1323,8 +1341,8 @@ fun ProfileCard(
|
|||||||
// ═════════════════════════════════════════════════════════════
|
// ═════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TelegramSectionTitle(title: String, isDarkTheme: Boolean) {
|
fun TelegramSectionTitle(title: String, isDarkTheme: Boolean, color: Color? = null) {
|
||||||
val textColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF8E8E93)
|
val textColor = color ?: if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF8E8E93)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
@@ -1483,11 +1501,12 @@ private fun TelegramSettingsItem(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
isDarkTheme: Boolean,
|
isDarkTheme: Boolean,
|
||||||
showDivider: Boolean = false,
|
showDivider: Boolean = false,
|
||||||
subtitle: String? = null
|
subtitle: String? = null,
|
||||||
|
iconTint: Color? = null
|
||||||
) {
|
) {
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
||||||
val iconColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
val iconColor = iconTint ?: if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
||||||
val dividerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE0E0E0)
|
val dividerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE0E0E0)
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
@@ -1527,11 +1546,22 @@ private fun TelegramSettingsItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun TelegramBiometricItem(isEnabled: Boolean, onToggle: () -> Unit, isDarkTheme: Boolean) {
|
private fun TelegramToggleItem(
|
||||||
|
icon: ImageVector,
|
||||||
|
title: String,
|
||||||
|
subtitle: String? = null,
|
||||||
|
isEnabled: Boolean,
|
||||||
|
onToggle: () -> Unit,
|
||||||
|
isDarkTheme: Boolean,
|
||||||
|
showDivider: Boolean = false
|
||||||
|
) {
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
|
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
||||||
val iconColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
val iconColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
|
||||||
val primaryBlue = Color(0xFF007AFF)
|
val accentColor = if (isDarkTheme) PrimaryBlueDark else PrimaryBlue
|
||||||
|
val dividerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE0E0E0)
|
||||||
|
|
||||||
|
Column {
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.fillMaxWidth()
|
Modifier.fillMaxWidth()
|
||||||
@@ -1540,7 +1570,7 @@ private fun TelegramBiometricItem(isEnabled: Boolean, onToggle: () -> Unit, isDa
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = TablerIcons.Fingerprint,
|
imageVector = icon,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = iconColor,
|
tint = iconColor,
|
||||||
modifier = Modifier.size(24.dp)
|
modifier = Modifier.size(24.dp)
|
||||||
@@ -1548,62 +1578,75 @@ private fun TelegramBiometricItem(isEnabled: Boolean, onToggle: () -> Unit, isDa
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.width(20.dp))
|
Spacer(modifier = Modifier.width(20.dp))
|
||||||
|
|
||||||
Text(
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
text = "Biometric Authentication",
|
Text(text = title, fontSize = 16.sp, color = textColor)
|
||||||
fontSize = 16.sp,
|
if (subtitle != null) {
|
||||||
color = textColor,
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
modifier = Modifier.weight(1f)
|
Text(text = subtitle, fontSize = 13.sp, color = secondaryTextColor)
|
||||||
)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// iOS-style animated switch
|
// Material 2 / old Telegram style switch
|
||||||
val animatedThumbOffset by
|
val thumbOffset by animateFloatAsState(
|
||||||
animateFloatAsState(
|
|
||||||
targetValue = if (isEnabled) 1f else 0f,
|
targetValue = if (isEnabled) 1f else 0f,
|
||||||
animationSpec =
|
animationSpec = tween(durationMillis = 150),
|
||||||
spring(
|
label = "thumb"
|
||||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
|
||||||
stiffness = Spring.StiffnessLow
|
|
||||||
),
|
|
||||||
label = "switchThumb"
|
|
||||||
)
|
)
|
||||||
|
val trackColor by animateColorAsState(
|
||||||
val trackColor by
|
targetValue = if (isEnabled) accentColor.copy(alpha = 0.5f)
|
||||||
animateColorAsState(
|
else if (isDarkTheme) Color(0xFF39393D) else Color(0xFFBDBDBD),
|
||||||
targetValue =
|
animationSpec = tween(durationMillis = 150),
|
||||||
if (isEnabled) primaryBlue
|
label = "track"
|
||||||
else if (isDarkTheme) Color(0xFF39393D) else Color(0xFFE9E9EA),
|
)
|
||||||
animationSpec = tween(300),
|
val thumbColor by animateColorAsState(
|
||||||
label = "trackColor"
|
targetValue = if (isEnabled) accentColor
|
||||||
|
else if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFFF1F1F1),
|
||||||
|
animationSpec = tween(durationMillis = 150),
|
||||||
|
label = "thumbColor"
|
||||||
)
|
)
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier = Modifier
|
||||||
Modifier.width(51.dp)
|
.width(37.dp)
|
||||||
.height(31.dp)
|
.height(20.dp)
|
||||||
.clip(RoundedCornerShape(15.5.dp))
|
.clip(RoundedCornerShape(10.dp))
|
||||||
.background(trackColor)
|
.background(trackColor)
|
||||||
.clickable(
|
.clickable(
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
indication = null,
|
indication = null,
|
||||||
onClick = { onToggle() }
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
)
|
onClick = onToggle
|
||||||
.padding(2.dp)
|
),
|
||||||
|
contentAlignment = Alignment.CenterStart
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier = Modifier
|
||||||
Modifier.size(27.dp)
|
.offset(x = (17.dp * thumbOffset))
|
||||||
.align(Alignment.CenterStart)
|
.size(20.dp)
|
||||||
.offset(x = (20.dp * animatedThumbOffset))
|
.shadow(2.dp, CircleShape)
|
||||||
.shadow(
|
|
||||||
elevation = if (isEnabled) 3.dp else 2.dp,
|
|
||||||
shape = CircleShape,
|
|
||||||
spotColor = Color.Black.copy(alpha = 0.15f)
|
|
||||||
)
|
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(Color.White)
|
.background(thumbColor)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showDivider) {
|
||||||
|
Divider(
|
||||||
|
color = dividerColor,
|
||||||
|
thickness = 0.5.dp,
|
||||||
|
modifier = Modifier.padding(start = 60.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TelegramBiometricItem(isEnabled: Boolean, onToggle: () -> Unit, isDarkTheme: Boolean) {
|
||||||
|
TelegramToggleItem(
|
||||||
|
icon = TablerIcons.Fingerprint,
|
||||||
|
title = "Biometric Authentication",
|
||||||
|
isEnabled = isEnabled,
|
||||||
|
onToggle = onToggle,
|
||||||
|
isDarkTheme = isDarkTheme
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ import androidx.compose.ui.text.AnnotatedString
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||||
|
import com.rosetta.messenger.ui.onboarding.PrimaryBlueDark
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -50,6 +52,7 @@ fun SafetyScreen(
|
|||||||
// Copy states
|
// Copy states
|
||||||
var copiedPublicKey by remember { mutableStateOf(false) }
|
var copiedPublicKey by remember { mutableStateOf(false) }
|
||||||
var copiedPrivateKey by remember { mutableStateOf(false) }
|
var copiedPrivateKey by remember { mutableStateOf(false) }
|
||||||
|
var showDeleteConfirmation by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
// Handle back gesture
|
// Handle back gesture
|
||||||
BackHandler { onBack() }
|
BackHandler { onBack() }
|
||||||
@@ -105,7 +108,7 @@ fun SafetyScreen(
|
|||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
// Keys Section - Telegram style
|
// Keys Section - Telegram style
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
TelegramSectionHeader("Keys", secondaryTextColor)
|
TelegramSectionHeader("Keys", Color(0xFF8E8E93))
|
||||||
|
|
||||||
TelegramCopyRow(
|
TelegramCopyRow(
|
||||||
label = "Public Key",
|
label = "Public Key",
|
||||||
@@ -173,7 +176,7 @@ fun SafetyScreen(
|
|||||||
|
|
||||||
TelegramActionRow(
|
TelegramActionRow(
|
||||||
label = "Delete Account",
|
label = "Delete Account",
|
||||||
onClick = onDeleteAccount,
|
onClick = { showDeleteConfirmation = true },
|
||||||
textColor = redColor,
|
textColor = redColor,
|
||||||
secondaryTextColor = secondaryTextColor,
|
secondaryTextColor = secondaryTextColor,
|
||||||
showDivider = false
|
showDivider = false
|
||||||
@@ -187,6 +190,40 @@ fun SafetyScreen(
|
|||||||
Spacer(modifier = Modifier.height(32.dp))
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete Account Confirmation Dialog
|
||||||
|
if (showDeleteConfirmation) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showDeleteConfirmation = false },
|
||||||
|
containerColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color.White,
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
"Delete Account",
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = textColor
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
"You are attempting to delete your account. Are you sure? This action cannot be undone.",
|
||||||
|
color = secondaryTextColor
|
||||||
|
)
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
showDeleteConfirmation = false
|
||||||
|
onDeleteAccount()
|
||||||
|
}
|
||||||
|
) { Text("Delete", color = Color(0xFFFF3B30)) }
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showDeleteConfirmation = false }) {
|
||||||
|
Text("Cancel", color = if (isDarkTheme) PrimaryBlueDark else PrimaryBlue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
Reference in New Issue
Block a user