fix: remove logs
This commit is contained in:
@@ -123,7 +123,6 @@ class BiometricAuthManager(private val context: Context) {
|
|||||||
Arrays.fill(passwordBytes, 0.toByte())
|
Arrays.fill(passwordBytes, 0.toByte())
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Encryption failed", e)
|
|
||||||
onError("Ошибка шифрования: ${e.message}")
|
onError("Ошибка шифрования: ${e.message}")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -131,11 +130,9 @@ class BiometricAuthManager(private val context: Context) {
|
|||||||
onCancel = onCancel
|
onCancel = onCancel
|
||||||
)
|
)
|
||||||
} catch (e: KeyPermanentlyInvalidatedException) {
|
} catch (e: KeyPermanentlyInvalidatedException) {
|
||||||
Log.e(TAG, "Key invalidated, removing and retrying", e)
|
|
||||||
removeBiometricData()
|
removeBiometricData()
|
||||||
onError("Биометрические данные изменились. Пожалуйста, настройте заново.")
|
onError("Биометрические данные изменились. Пожалуйста, настройте заново.")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to initialize encryption", e)
|
|
||||||
onError("Ошибка инициализации: ${e.message}")
|
onError("Ошибка инициализации: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -185,7 +182,6 @@ class BiometricAuthManager(private val context: Context) {
|
|||||||
decrypted = authenticatedCipher.doFinal(encrypted)
|
decrypted = authenticatedCipher.doFinal(encrypted)
|
||||||
onSuccess(String(decrypted, Charsets.UTF_8))
|
onSuccess(String(decrypted, Charsets.UTF_8))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Decryption failed", e)
|
|
||||||
onError("Ошибка расшифровки: ${e.message}")
|
onError("Ошибка расшифровки: ${e.message}")
|
||||||
} finally {
|
} finally {
|
||||||
// Secure memory wipe - обнуляем расшифрованные данные
|
// Secure memory wipe - обнуляем расшифрованные данные
|
||||||
@@ -196,11 +192,9 @@ class BiometricAuthManager(private val context: Context) {
|
|||||||
onCancel = onCancel
|
onCancel = onCancel
|
||||||
)
|
)
|
||||||
} catch (e: KeyPermanentlyInvalidatedException) {
|
} catch (e: KeyPermanentlyInvalidatedException) {
|
||||||
Log.e(TAG, "Key permanently invalidated", e)
|
|
||||||
removeBiometricData()
|
removeBiometricData()
|
||||||
onError("Биометрические данные изменились. Пожалуйста, войдите с паролем и настройте биометрию заново.")
|
onError("Биометрические данные изменились. Пожалуйста, войдите с паролем и настройте биометрию заново.")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to initialize decryption", e)
|
|
||||||
onError("Ошибка инициализации: ${e.message}")
|
onError("Ошибка инициализации: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -222,8 +216,6 @@ class BiometricAuthManager(private val context: Context) {
|
|||||||
val callback = object : BiometricPrompt.AuthenticationCallback() {
|
val callback = object : BiometricPrompt.AuthenticationCallback() {
|
||||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||||
super.onAuthenticationError(errorCode, errString)
|
super.onAuthenticationError(errorCode, errString)
|
||||||
Log.d(TAG, "Authentication error: $errorCode - $errString")
|
|
||||||
|
|
||||||
when (errorCode) {
|
when (errorCode) {
|
||||||
BiometricPrompt.ERROR_USER_CANCELED,
|
BiometricPrompt.ERROR_USER_CANCELED,
|
||||||
BiometricPrompt.ERROR_NEGATIVE_BUTTON,
|
BiometricPrompt.ERROR_NEGATIVE_BUTTON,
|
||||||
@@ -237,8 +229,6 @@ class BiometricAuthManager(private val context: Context) {
|
|||||||
|
|
||||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||||
super.onAuthenticationSucceeded(result)
|
super.onAuthenticationSucceeded(result)
|
||||||
Log.d(TAG, "Authentication succeeded")
|
|
||||||
|
|
||||||
// Получаем аутентифицированный Cipher из CryptoObject
|
// Получаем аутентифицированный Cipher из CryptoObject
|
||||||
val authenticatedCipher = result.cryptoObject?.cipher
|
val authenticatedCipher = result.cryptoObject?.cipher
|
||||||
if (authenticatedCipher != null) {
|
if (authenticatedCipher != null) {
|
||||||
@@ -250,8 +240,7 @@ class BiometricAuthManager(private val context: Context) {
|
|||||||
|
|
||||||
override fun onAuthenticationFailed() {
|
override fun onAuthenticationFailed() {
|
||||||
super.onAuthenticationFailed()
|
super.onAuthenticationFailed()
|
||||||
Log.d(TAG, "Authentication failed (user can retry)")
|
// Не вызываем onError - пользователь может попробовать снова
|
||||||
// Не вызываем onError - пользователь может попробовать снова
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,7 +301,6 @@ class BiometricAuthManager(private val context: Context) {
|
|||||||
try {
|
try {
|
||||||
builder.setAttestationChallenge(generateAttestationChallenge())
|
builder.setAttestationChallenge(generateAttestationChallenge())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "Key attestation not supported", e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,7 +309,6 @@ class BiometricAuthManager(private val context: Context) {
|
|||||||
try {
|
try {
|
||||||
builder.setIsStrongBoxBacked(true)
|
builder.setIsStrongBoxBacked(true)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "StrongBox not available, using TEE", e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,7 +323,6 @@ class BiometricAuthManager(private val context: Context) {
|
|||||||
return try {
|
return try {
|
||||||
keyStore.getKey(KEY_ALIAS, null) as? SecretKey
|
keyStore.getKey(KEY_ALIAS, null) as? SecretKey
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to get key from Keystore", e)
|
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -349,10 +335,8 @@ class BiometricAuthManager(private val context: Context) {
|
|||||||
try {
|
try {
|
||||||
if (keyStore.containsAlias(KEY_ALIAS)) {
|
if (keyStore.containsAlias(KEY_ALIAS)) {
|
||||||
keyStore.deleteEntry(KEY_ALIAS)
|
keyStore.deleteEntry(KEY_ALIAS)
|
||||||
Log.d(TAG, "Biometric key removed from Keystore")
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to remove key from Keystore", e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -396,7 +380,6 @@ class BiometricAuthManager(private val context: Context) {
|
|||||||
try {
|
try {
|
||||||
certificateChain[i].verify(certificateChain[i + 1].publicKey)
|
certificateChain[i].verify(certificateChain[i + 1].publicKey)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Certificate chain verification failed at index $i", e)
|
|
||||||
return KeyAttestationResult.Invalid("Цепочка сертификатов недействительна")
|
return KeyAttestationResult.Invalid("Цепочка сертификатов недействительна")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -406,14 +389,12 @@ class BiometricAuthManager(private val context: Context) {
|
|||||||
if (leafCert != null) {
|
if (leafCert != null) {
|
||||||
val attestationExtension = leafCert.getExtensionValue("1.3.6.1.4.1.11129.2.1.17")
|
val attestationExtension = leafCert.getExtensionValue("1.3.6.1.4.1.11129.2.1.17")
|
||||||
if (attestationExtension != null) {
|
if (attestationExtension != null) {
|
||||||
Log.d(TAG, "Key attestation verified - key is in secure hardware")
|
|
||||||
return KeyAttestationResult.Valid(isStrongBox = isKeyInStrongBox())
|
return KeyAttestationResult.Valid(isStrongBox = isKeyInStrongBox())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyAttestationResult.NotSupported
|
KeyAttestationResult.NotSupported
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Key attestation verification failed", e)
|
|
||||||
KeyAttestationResult.Invalid(e.message ?: "Unknown error")
|
KeyAttestationResult.Invalid(e.message ?: "Unknown error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -437,7 +418,6 @@ class BiometricAuthManager(private val context: Context) {
|
|||||||
keyInfo.isInsideSecureHardware
|
keyInfo.isInsideSecureHardware
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "Could not determine if key is in StrongBox", e)
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ class BiometricPreferences(private val context: Context) {
|
|||||||
try {
|
try {
|
||||||
_isBiometricEnabled.value = encryptedPrefs.getBoolean(KEY_BIOMETRIC_ENABLED, false)
|
_isBiometricEnabled.value = encryptedPrefs.getBoolean(KEY_BIOMETRIC_ENABLED, false)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to read biometric enabled state", e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +62,6 @@ class BiometricPreferences(private val context: Context) {
|
|||||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to create EncryptedSharedPreferences, falling back", e)
|
|
||||||
// Fallback на обычные SharedPreferences в случае ошибки (не должно произойти)
|
// Fallback на обычные SharedPreferences в случае ошибки (не должно произойти)
|
||||||
return context.getSharedPreferences(PREFS_FILE_NAME + "_fallback", Context.MODE_PRIVATE)
|
return context.getSharedPreferences(PREFS_FILE_NAME + "_fallback", Context.MODE_PRIVATE)
|
||||||
}
|
}
|
||||||
@@ -97,7 +95,6 @@ class BiometricPreferences(private val context: Context) {
|
|||||||
suspend fun saveEncryptedPassword(publicKey: String, encryptedPassword: String) = withContext(Dispatchers.IO) {
|
suspend fun saveEncryptedPassword(publicKey: String, encryptedPassword: String) = withContext(Dispatchers.IO) {
|
||||||
val key = "$ENCRYPTED_PASSWORD_PREFIX$publicKey"
|
val key = "$ENCRYPTED_PASSWORD_PREFIX$publicKey"
|
||||||
encryptedPrefs.edit().putString(key, encryptedPassword).apply()
|
encryptedPrefs.edit().putString(key, encryptedPassword).apply()
|
||||||
Log.d(TAG, "Encrypted password saved for account")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -114,7 +111,6 @@ class BiometricPreferences(private val context: Context) {
|
|||||||
suspend fun removeEncryptedPassword(publicKey: String) = withContext(Dispatchers.IO) {
|
suspend fun removeEncryptedPassword(publicKey: String) = withContext(Dispatchers.IO) {
|
||||||
val key = "$ENCRYPTED_PASSWORD_PREFIX$publicKey"
|
val key = "$ENCRYPTED_PASSWORD_PREFIX$publicKey"
|
||||||
encryptedPrefs.edit().remove(key).apply()
|
encryptedPrefs.edit().remove(key).apply()
|
||||||
Log.d(TAG, "Encrypted password removed for account")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -123,7 +119,6 @@ class BiometricPreferences(private val context: Context) {
|
|||||||
suspend fun clearAll() = withContext(Dispatchers.IO) {
|
suspend fun clearAll() = withContext(Dispatchers.IO) {
|
||||||
encryptedPrefs.edit().clear().apply()
|
encryptedPrefs.edit().clear().apply()
|
||||||
_isBiometricEnabled.value = false
|
_isBiometricEnabled.value = false
|
||||||
Log.d(TAG, "All biometric data cleared")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -347,7 +347,6 @@ fun ChatDetailScreen(
|
|||||||
// 🔥 КРИТИЧНО: Дедупликация по ID перед сортировкой!
|
// 🔥 КРИТИЧНО: Дедупликация по ID перед сортировкой!
|
||||||
val uniqueMessages = messages.distinctBy { it.id }
|
val uniqueMessages = messages.distinctBy { it.id }
|
||||||
if (uniqueMessages.size != messages.size) {
|
if (uniqueMessages.size != messages.size) {
|
||||||
android.util.Log.e("ChatDetailScreen", "🚨 DEDUPLICATED ${messages.size - uniqueMessages.size} messages in UI! Original: ${messages.map { it.id }}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сортируем по времени (новые -> старые) для reversed layout
|
// Сортируем по времени (новые -> старые) для reversed layout
|
||||||
|
|||||||
@@ -264,9 +264,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
val allIds = newList.map { it.id }
|
val allIds = newList.map { it.id }
|
||||||
val duplicates = allIds.groupBy { it }.filter { it.value.size > 1 }.keys
|
val duplicates = allIds.groupBy { it }.filter { it.value.size > 1 }.keys
|
||||||
if (duplicates.isNotEmpty()) {
|
if (duplicates.isNotEmpty()) {
|
||||||
android.util.Log.e("ChatViewModel", "🚨 DUPLICATE IDS FOUND in pollLatestMessages: $duplicates")
|
|
||||||
android.util.Log.e("ChatViewModel", " currentList ids: ${currentList.map { it.id }}")
|
|
||||||
android.util.Log.e("ChatViewModel", " newMessages ids: ${newMessages.map { it.id }}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_messages.value = newList
|
_messages.value = newList
|
||||||
@@ -377,13 +374,10 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
private fun addMessageSafely(message: ChatMessage): Boolean {
|
private fun addMessageSafely(message: ChatMessage): Boolean {
|
||||||
val currentMessages = _messages.value
|
val currentMessages = _messages.value
|
||||||
val currentIds = currentMessages.map { it.id }.toSet()
|
val currentIds = currentMessages.map { it.id }.toSet()
|
||||||
android.util.Log.d("ChatViewModel", "🔍 addMessageSafely: id=${message.id}, currentCount=${currentMessages.size}, ids=${currentIds.take(5)}...")
|
if (message.id in currentIds) {
|
||||||
if (message.id in currentIds) {
|
|
||||||
android.util.Log.e("ChatViewModel", "🚨 BLOCKED DUPLICATE: id=${message.id} already exists in ${currentIds.size} messages!")
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
_messages.value = currentMessages + message
|
_messages.value = currentMessages + message
|
||||||
android.util.Log.d("ChatViewModel", "✅ Added message: id=${message.id}, newCount=${_messages.value.size}")
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -595,22 +589,17 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
withContext(Dispatchers.Main.immediate) {
|
withContext(Dispatchers.Main.immediate) {
|
||||||
val dbIds = messages.map { it.id }.toSet()
|
val dbIds = messages.map { it.id }.toSet()
|
||||||
val currentMsgs = _messages.value
|
val currentMsgs = _messages.value
|
||||||
android.util.Log.d("ChatViewModel", "📥 loadMessages: dbCount=${messages.size}, currentCount=${currentMsgs.size}")
|
android.util.Log.d("ChatViewModel", " Current ids: ${currentMsgs.map { it.id }.take(5)}...")
|
||||||
android.util.Log.d("ChatViewModel", " DB ids: ${dbIds.take(5)}...")
|
|
||||||
android.util.Log.d("ChatViewModel", " Current ids: ${currentMsgs.map { it.id }.take(5)}...")
|
|
||||||
|
|
||||||
val optimisticMessages = currentMsgs.filter { msg ->
|
val optimisticMessages = currentMsgs.filter { msg ->
|
||||||
msg.status == MessageStatus.SENDING && msg.id !in dbIds
|
msg.status == MessageStatus.SENDING && msg.id !in dbIds
|
||||||
}
|
}
|
||||||
android.util.Log.d("ChatViewModel", " Optimistic (SENDING, not in DB): ${optimisticMessages.size} - ${optimisticMessages.map { it.id }}")
|
val newList = messages + optimisticMessages
|
||||||
|
|
||||||
val newList = messages + optimisticMessages
|
|
||||||
|
|
||||||
// 🔍 Финальная дедупликация по ID (на всякий случай)
|
// 🔍 Финальная дедупликация по ID (на всякий случай)
|
||||||
val deduplicatedList = newList.distinctBy { it.id }
|
val deduplicatedList = newList.distinctBy { it.id }
|
||||||
|
|
||||||
if (deduplicatedList.size != newList.size) {
|
if (deduplicatedList.size != newList.size) {
|
||||||
android.util.Log.e("ChatViewModel", "🚨 DEDUPLICATED ${newList.size - deduplicatedList.size} messages!")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_messages.value = deduplicatedList
|
_messages.value = deduplicatedList
|
||||||
|
|||||||
@@ -242,9 +242,7 @@ fun ImageEditorScreen(
|
|||||||
lastStableKeyboardHeight = currentImeHeight
|
lastStableKeyboardHeight = currentImeHeight
|
||||||
}
|
}
|
||||||
// 📊 Log IME height changes
|
// 📊 Log IME height changes
|
||||||
Log.d(TAG, "⌨️ IME: height=${currentImeHeight.value}dp, wasVisible=$wasKeyboardVisible, isVisible=$isKeyboardVisible, emojiBoxVisible=${coordinator.isEmojiBoxVisible}")
|
|
||||||
if (wasKeyboardVisible != isKeyboardVisible) {
|
if (wasKeyboardVisible != isKeyboardVisible) {
|
||||||
Log.d(TAG, "⌨️ KEYBOARD STATE CHANGED: $wasKeyboardVisible → $isKeyboardVisible")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -273,12 +271,8 @@ fun ImageEditorScreen(
|
|||||||
lastToggleTime = currentTime
|
lastToggleTime = currentTime
|
||||||
|
|
||||||
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
|
|
||||||
Log.d(TAG, "toggleEmojiPicker: isEmojiVisible=${coordinator.isEmojiVisible}, isEmojiBoxVisible=${coordinator.isEmojiBoxVisible}, showEmojiPicker=$showEmojiPicker")
|
|
||||||
|
|
||||||
if (coordinator.isEmojiVisible) {
|
if (coordinator.isEmojiVisible) {
|
||||||
// EMOJI → KEYBOARD
|
// EMOJI → KEYBOARD
|
||||||
Log.d(TAG, "TRANSITION: EMOJI → KEYBOARD")
|
|
||||||
coordinator.requestShowKeyboard(
|
coordinator.requestShowKeyboard(
|
||||||
showKeyboard = {
|
showKeyboard = {
|
||||||
editTextView?.let { editText ->
|
editTextView?.let { editText ->
|
||||||
@@ -290,7 +284,6 @@ fun ImageEditorScreen(
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// KEYBOARD → EMOJI
|
// KEYBOARD → EMOJI
|
||||||
Log.d(TAG, "TRANSITION: KEYBOARD → EMOJI")
|
|
||||||
coordinator.requestShowEmoji(
|
coordinator.requestShowEmoji(
|
||||||
hideKeyboard = { imm.hideSoftInputFromWindow(view.windowToken, 0) },
|
hideKeyboard = { imm.hideSoftInputFromWindow(view.windowToken, 0) },
|
||||||
showEmoji = { showEmojiPicker = true }
|
showEmoji = { showEmojiPicker = true }
|
||||||
@@ -1165,7 +1158,7 @@ private fun TelegramCaptionBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Save edited image and return the URI - crops black bars from FIT_CENTER */
|
/** Save edited image and return the URI - returns original if no edits, otherwise crops black bars */
|
||||||
private suspend fun saveEditedImage(
|
private suspend fun saveEditedImage(
|
||||||
context: Context,
|
context: Context,
|
||||||
photoEditor: PhotoEditor?,
|
photoEditor: PhotoEditor?,
|
||||||
@@ -1174,12 +1167,15 @@ private suspend fun saveEditedImage(
|
|||||||
onResult: (Uri?) -> Unit
|
onResult: (Uri?) -> Unit
|
||||||
) {
|
) {
|
||||||
if (photoEditor == null || photoEditorView == null) {
|
if (photoEditor == null || photoEditorView == null) {
|
||||||
onResult(null)
|
// Нет редактора - возвращаем оригинал
|
||||||
|
onResult(imageUri)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
|
// Загружаем оригинальное изображение и сохраняем его напрямую
|
||||||
|
// PhotoEditor с setClipSourceImage(true) должен обрезать черные полосы автоматически
|
||||||
val tempFile = File(context.cacheDir, "temp_${System.currentTimeMillis()}.png")
|
val tempFile = File(context.cacheDir, "temp_${System.currentTimeMillis()}.png")
|
||||||
|
|
||||||
val saveSettings = SaveSettings.Builder()
|
val saveSettings = SaveSettings.Builder()
|
||||||
@@ -1187,7 +1183,6 @@ private suspend fun saveEditedImage(
|
|||||||
.setTransparencyEnabled(true)
|
.setTransparencyEnabled(true)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
// Сохраняем полный view (с черными полосами)
|
|
||||||
val savedPath = suspendCancellableCoroutine<String?> { continuation ->
|
val savedPath = suspendCancellableCoroutine<String?> { continuation ->
|
||||||
photoEditor.saveAsFile(
|
photoEditor.saveAsFile(
|
||||||
tempFile.absolutePath,
|
tempFile.absolutePath,
|
||||||
@@ -1204,7 +1199,8 @@ private suspend fun saveEditedImage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (savedPath == null) {
|
if (savedPath == null) {
|
||||||
withContext(Dispatchers.Main) { onResult(null) }
|
// Ошибка сохранения - возвращаем оригинал
|
||||||
|
withContext(Dispatchers.Main) { onResult(imageUri) }
|
||||||
return@withContext
|
return@withContext
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1215,7 +1211,7 @@ private suspend fun saveEditedImage(
|
|||||||
return@withContext
|
return@withContext
|
||||||
}
|
}
|
||||||
|
|
||||||
// Получаем РЕАЛЬНЫЕ размеры изображения из URI (не из drawable!)
|
// Получаем размеры оригинального изображения
|
||||||
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||||
context.contentResolver.openInputStream(imageUri)?.use { stream ->
|
context.contentResolver.openInputStream(imageUri)?.use { stream ->
|
||||||
BitmapFactory.decodeStream(stream, null, options)
|
BitmapFactory.decodeStream(stream, null, options)
|
||||||
@@ -1232,6 +1228,17 @@ private suspend fun saveEditedImage(
|
|||||||
val viewWidth = savedBitmap.width
|
val viewWidth = savedBitmap.width
|
||||||
val viewHeight = savedBitmap.height
|
val viewHeight = savedBitmap.height
|
||||||
|
|
||||||
|
// Соотношение сторон оригинала и сохраненного
|
||||||
|
val originalRatio = imageWidth.toFloat() / imageHeight
|
||||||
|
val savedRatio = viewWidth.toFloat() / viewHeight
|
||||||
|
|
||||||
|
// Если соотношения примерно равны - черных полос нет, возвращаем как есть
|
||||||
|
if (kotlin.math.abs(originalRatio - savedRatio) < 0.01f) {
|
||||||
|
savedBitmap.recycle()
|
||||||
|
withContext(Dispatchers.Main) { onResult(Uri.fromFile(tempFile)) }
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
|
||||||
// Вычисляем где находится изображение (FIT_CENTER логика)
|
// Вычисляем где находится изображение (FIT_CENTER логика)
|
||||||
val scale = minOf(
|
val scale = minOf(
|
||||||
viewWidth.toFloat() / imageWidth,
|
viewWidth.toFloat() / imageWidth,
|
||||||
@@ -1244,13 +1251,15 @@ private suspend fun saveEditedImage(
|
|||||||
val left = ((viewWidth - scaledWidth) / 2).coerceAtLeast(0)
|
val left = ((viewWidth - scaledWidth) / 2).coerceAtLeast(0)
|
||||||
val top = ((viewHeight - scaledHeight) / 2).coerceAtLeast(0)
|
val top = ((viewHeight - scaledHeight) / 2).coerceAtLeast(0)
|
||||||
|
|
||||||
// Обрезаем черные полосы
|
val cropWidth = scaledWidth.coerceIn(1, viewWidth - left)
|
||||||
|
val cropHeight = scaledHeight.coerceIn(1, viewHeight - top)
|
||||||
|
// Обрезаем черные полосы
|
||||||
val croppedBitmap = Bitmap.createBitmap(
|
val croppedBitmap = Bitmap.createBitmap(
|
||||||
savedBitmap,
|
savedBitmap,
|
||||||
left,
|
left,
|
||||||
top,
|
top,
|
||||||
scaledWidth.coerceAtMost(viewWidth - left),
|
cropWidth,
|
||||||
scaledHeight.coerceAtMost(viewHeight - top)
|
cropHeight
|
||||||
)
|
)
|
||||||
|
|
||||||
// Сохраняем обрезанное изображение
|
// Сохраняем обрезанное изображение
|
||||||
@@ -1267,19 +1276,23 @@ private suspend fun saveEditedImage(
|
|||||||
onResult(Uri.fromFile(finalFile))
|
onResult(Uri.fromFile(finalFile))
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
withContext(Dispatchers.Main) { onResult(null) }
|
// При ошибке возвращаем оригинал
|
||||||
|
withContext(Dispatchers.Main) { onResult(imageUri) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Save edited image synchronously - crops black bars from FIT_CENTER */
|
/** Save edited image synchronously - returns original if no edits, otherwise crops black bars */
|
||||||
private suspend fun saveEditedImageSync(
|
private suspend fun saveEditedImageSync(
|
||||||
context: Context,
|
context: Context,
|
||||||
photoEditor: PhotoEditor?,
|
photoEditor: PhotoEditor?,
|
||||||
photoEditorView: PhotoEditorView?,
|
photoEditorView: PhotoEditorView?,
|
||||||
imageUri: Uri
|
imageUri: Uri
|
||||||
): Uri? {
|
): Uri? {
|
||||||
if (photoEditor == null || photoEditorView == null) return null
|
if (photoEditor == null || photoEditorView == null) {
|
||||||
|
// Нет редактора - возвращаем оригинал
|
||||||
|
return imageUri
|
||||||
|
}
|
||||||
|
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
@@ -1308,13 +1321,16 @@ private suspend fun saveEditedImageSync(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (savedPath == null) return@withContext null
|
if (savedPath == null) {
|
||||||
|
// Ошибка - возвращаем оригинал
|
||||||
|
return@withContext imageUri
|
||||||
|
}
|
||||||
|
|
||||||
// Загружаем сохраненное изображение
|
// Загружаем сохраненное изображение
|
||||||
val savedBitmap = BitmapFactory.decodeFile(savedPath)
|
val savedBitmap = BitmapFactory.decodeFile(savedPath)
|
||||||
?: return@withContext Uri.fromFile(tempFile)
|
?: return@withContext Uri.fromFile(tempFile)
|
||||||
|
|
||||||
// Получаем РЕАЛЬНЫЕ размеры изображения из URI (не из drawable!)
|
// Получаем размеры оригинального изображения
|
||||||
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||||
context.contentResolver.openInputStream(imageUri)?.use { stream ->
|
context.contentResolver.openInputStream(imageUri)?.use { stream ->
|
||||||
BitmapFactory.decodeStream(stream, null, options)
|
BitmapFactory.decodeStream(stream, null, options)
|
||||||
@@ -1330,6 +1346,16 @@ private suspend fun saveEditedImageSync(
|
|||||||
val viewWidth = savedBitmap.width
|
val viewWidth = savedBitmap.width
|
||||||
val viewHeight = savedBitmap.height
|
val viewHeight = savedBitmap.height
|
||||||
|
|
||||||
|
// Соотношение сторон оригинала и сохраненного
|
||||||
|
val originalRatio = imageWidth.toFloat() / imageHeight
|
||||||
|
val savedRatio = viewWidth.toFloat() / viewHeight
|
||||||
|
|
||||||
|
// Если соотношения примерно равны - черных полос нет
|
||||||
|
if (kotlin.math.abs(originalRatio - savedRatio) < 0.01f) {
|
||||||
|
savedBitmap.recycle()
|
||||||
|
return@withContext Uri.fromFile(tempFile)
|
||||||
|
}
|
||||||
|
|
||||||
// Вычисляем где находится изображение (FIT_CENTER логика)
|
// Вычисляем где находится изображение (FIT_CENTER логика)
|
||||||
val scale = minOf(
|
val scale = minOf(
|
||||||
viewWidth.toFloat() / imageWidth,
|
viewWidth.toFloat() / imageWidth,
|
||||||
@@ -1342,13 +1368,15 @@ private suspend fun saveEditedImageSync(
|
|||||||
val left = ((viewWidth - scaledWidth) / 2).coerceAtLeast(0)
|
val left = ((viewWidth - scaledWidth) / 2).coerceAtLeast(0)
|
||||||
val top = ((viewHeight - scaledHeight) / 2).coerceAtLeast(0)
|
val top = ((viewHeight - scaledHeight) / 2).coerceAtLeast(0)
|
||||||
|
|
||||||
// Обрезаем черные полосы
|
val cropWidth = scaledWidth.coerceIn(1, viewWidth - left)
|
||||||
|
val cropHeight = scaledHeight.coerceIn(1, viewHeight - top)
|
||||||
|
// Обрезаем черные полосы
|
||||||
val croppedBitmap = Bitmap.createBitmap(
|
val croppedBitmap = Bitmap.createBitmap(
|
||||||
savedBitmap,
|
savedBitmap,
|
||||||
left,
|
left,
|
||||||
top,
|
top,
|
||||||
scaledWidth.coerceAtMost(viewWidth - left),
|
cropWidth,
|
||||||
scaledHeight.coerceAtMost(viewHeight - top)
|
cropHeight
|
||||||
)
|
)
|
||||||
|
|
||||||
// Сохраняем обрезанное изображение
|
// Сохраняем обрезанное изображение
|
||||||
@@ -1366,7 +1394,8 @@ private suspend fun saveEditedImageSync(
|
|||||||
|
|
||||||
Uri.fromFile(finalFile)
|
Uri.fromFile(finalFile)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
// При ошибке возвращаем оригинал
|
||||||
|
imageUri
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1440,7 +1440,6 @@ fun PhotoPreviewWithCaptionScreen(
|
|||||||
coordinator.syncHeights()
|
coordinator.syncHeights()
|
||||||
lastStableKeyboardHeight = currentImeHeight
|
lastStableKeyboardHeight = currentImeHeight
|
||||||
}
|
}
|
||||||
Log.d("PhotoPreview", "IME height: ${currentImeHeight.value}dp, isKeyboardVisible: $isKeyboardVisible, emojiHeight: ${coordinator.emojiHeight.value}dp, isEmojiBoxVisible: ${coordinator.isEmojiBoxVisible}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1464,7 +1463,6 @@ fun PhotoPreviewWithCaptionScreen(
|
|||||||
fun toggleEmojiPicker() {
|
fun toggleEmojiPicker() {
|
||||||
val currentTime = System.currentTimeMillis()
|
val currentTime = System.currentTimeMillis()
|
||||||
if (currentTime - lastToggleTime < toggleCooldownMs) {
|
if (currentTime - lastToggleTime < toggleCooldownMs) {
|
||||||
Log.d("PhotoPreview", "Toggle blocked by cooldown")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
lastToggleTime = currentTime
|
lastToggleTime = currentTime
|
||||||
@@ -1473,30 +1471,24 @@ fun PhotoPreviewWithCaptionScreen(
|
|||||||
|
|
||||||
if (coordinator.isEmojiVisible) {
|
if (coordinator.isEmojiVisible) {
|
||||||
// EMOJI → KEYBOARD
|
// EMOJI → KEYBOARD
|
||||||
Log.d("PhotoPreview", "TOGGLE: Emoji → Keyboard")
|
|
||||||
coordinator.requestShowKeyboard(
|
coordinator.requestShowKeyboard(
|
||||||
showKeyboard = {
|
showKeyboard = {
|
||||||
Log.d("PhotoPreview", "Showing keyboard...")
|
|
||||||
editTextView?.let { editText ->
|
editTextView?.let { editText ->
|
||||||
editText.requestFocus()
|
editText.requestFocus()
|
||||||
imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
|
imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
hideEmoji = {
|
hideEmoji = {
|
||||||
Log.d("PhotoPreview", "Hiding emoji picker")
|
|
||||||
showEmojiPicker = false
|
showEmojiPicker = false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// KEYBOARD → EMOJI
|
// KEYBOARD → EMOJI
|
||||||
Log.d("PhotoPreview", "TOGGLE: Keyboard → Emoji")
|
|
||||||
coordinator.requestShowEmoji(
|
coordinator.requestShowEmoji(
|
||||||
hideKeyboard = {
|
hideKeyboard = {
|
||||||
Log.d("PhotoPreview", "Hiding keyboard...")
|
|
||||||
imm.hideSoftInputFromWindow(view.windowToken, 0)
|
imm.hideSoftInputFromWindow(view.windowToken, 0)
|
||||||
},
|
},
|
||||||
showEmoji = {
|
showEmoji = {
|
||||||
Log.d("PhotoPreview", "Showing emoji picker")
|
|
||||||
showEmojiPicker = true
|
showEmojiPicker = true
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -1508,8 +1500,6 @@ fun PhotoPreviewWithCaptionScreen(
|
|||||||
val shouldAddNavBarPadding = !isKeyboardVisible && !coordinator.isEmojiBoxVisible
|
val shouldAddNavBarPadding = !isKeyboardVisible && !coordinator.isEmojiBoxVisible
|
||||||
|
|
||||||
// Логируем состояние при каждой рекомпозиции
|
// Логируем состояние при каждой рекомпозиции
|
||||||
Log.d("PhotoPreview", "RENDER: showEmoji=$showEmojiPicker, isKeyboard=$isKeyboardVisible, isEmojiBoxVisible=${coordinator.isEmojiBoxVisible}, useImePadding=$shouldUseImePadding, emojiHeight=${coordinator.emojiHeight.value}dp")
|
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
color = backgroundColor,
|
color = backgroundColor,
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
|
|||||||
@@ -286,9 +286,6 @@ private fun computeAvatarState(
|
|||||||
val showBlob = collapseProgress < ProfileMetaballConstants.MERGE_COMPLETE_PROGRESS && radius > 1f
|
val showBlob = collapseProgress < ProfileMetaballConstants.MERGE_COMPLETE_PROGRESS && radius > 1f
|
||||||
|
|
||||||
// DEBUG LOG
|
// DEBUG LOG
|
||||||
Log.d("Metaball", "collapse=$collapseProgress, expansion=$expansionProgress, radius=$radius, diff=$diff")
|
|
||||||
Log.d("Metaball", "centerY=$centerY, cornerRadius=$cornerRadius, isDrawing=$isDrawing, isNear=$isNear")
|
|
||||||
|
|
||||||
return AvatarState(
|
return AvatarState(
|
||||||
centerX = centerX,
|
centerX = centerX,
|
||||||
centerY = centerY,
|
centerY = centerY,
|
||||||
|
|||||||
@@ -259,8 +259,6 @@ fun OtherProfileScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DEBUG LOGS
|
// DEBUG LOGS
|
||||||
Log.d("OtherProfileScroll", "expansionProgress=$expansionProgress, isPulledDown=$isPulledDown, isDragging=$isDragging")
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
// NESTED SCROLL - Telegram style with overscroll support
|
// NESTED SCROLL - Telegram style with overscroll support
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -588,7 +588,6 @@ private suspend fun loadPhotos(context: Context): List<PhotoItem> = withContext(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.e(TAG, "Error loading photos: ${e.message}", e)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
items
|
items
|
||||||
|
|||||||
@@ -401,8 +401,6 @@ fun ProfileScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DEBUG LOGS
|
// DEBUG LOGS
|
||||||
Log.d("ProfileScroll", "expansionProgress=$expansionProgress, isPulledDown=$isPulledDown, isDragging=$isDragging")
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
// NESTED SCROLL - Telegram style
|
// NESTED SCROLL - Telegram style
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|||||||
119
remove_logs.py
Normal file
119
remove_logs.py
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Скрипт для удаления Log.d/e/w/i/v вызовов из Kotlin файлов
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def remove_logs_from_file(filepath):
|
||||||
|
"""Удаляет Log.d/e/w/i/v вызовы из файла"""
|
||||||
|
|
||||||
|
with open(filepath, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
original_content = content
|
||||||
|
|
||||||
|
# Паттерн для Log.d/e/w/i/v с учетом многострочных вызовов
|
||||||
|
# Матчит: Log.d("TAG", "message") или Log.d(TAG, "message", exception)
|
||||||
|
patterns = [
|
||||||
|
# Простые однострочные логи
|
||||||
|
r'^\s*Log\.[dewiv]\([^)]*\)\s*\n',
|
||||||
|
# Многострочные логи (с переносами внутри скобок)
|
||||||
|
r'^\s*Log\.[dewiv]\([^)]*\n[^)]*\)\s*\n',
|
||||||
|
r'^\s*Log\.[dewiv]\([^)]*\n[^)]*\n[^)]*\)\s*\n',
|
||||||
|
r'^\s*Log\.[dewiv]\([^)]*\n[^)]*\n[^)]*\n[^)]*\)\s*\n',
|
||||||
|
# android.util.Log
|
||||||
|
r'^\s*android\.util\.Log\.[dewiv]\([^)]*\)\s*\n',
|
||||||
|
]
|
||||||
|
|
||||||
|
for pattern in patterns:
|
||||||
|
content = re.sub(pattern, '', content, flags=re.MULTILINE)
|
||||||
|
|
||||||
|
# Более агрессивный паттерн для оставшихся логов
|
||||||
|
# Находит Log.X( и удаляет до закрывающей скобки
|
||||||
|
def remove_log_call(match):
|
||||||
|
return ''
|
||||||
|
|
||||||
|
# Паттерн который находит Log.X(...) учитывая вложенные скобки
|
||||||
|
log_pattern = r'^\s*(?:android\.util\.)?Log\.[dewiv]\s*\([^()]*(?:\([^()]*\)[^()]*)*\)\s*\n?'
|
||||||
|
content = re.sub(log_pattern, '', content, flags=re.MULTILINE)
|
||||||
|
|
||||||
|
if content != original_content:
|
||||||
|
with open(filepath, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(content)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def find_kotlin_files(directory):
|
||||||
|
"""Находит все .kt файлы в директории"""
|
||||||
|
kotlin_files = []
|
||||||
|
for root, dirs, files in os.walk(directory):
|
||||||
|
# Пропускаем build директории
|
||||||
|
dirs[:] = [d for d in dirs if d not in ['build', '.gradle', '.idea']]
|
||||||
|
for file in files:
|
||||||
|
if file.endswith('.kt'):
|
||||||
|
kotlin_files.append(os.path.join(root, file))
|
||||||
|
return kotlin_files
|
||||||
|
|
||||||
|
def count_logs_in_file(filepath):
|
||||||
|
"""Считает количество Log вызовов в файле"""
|
||||||
|
with open(filepath, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
pattern = r'(?:android\.util\.)?Log\.[dewiv]\s*\('
|
||||||
|
matches = re.findall(pattern, content)
|
||||||
|
return len(matches)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Директория с исходниками
|
||||||
|
src_dir = '/Users/ruslanmakhmatov/Desktop/Work/rosette-app/rosetta-android/app/src/main/java/com/rosetta/messenger'
|
||||||
|
|
||||||
|
if not os.path.exists(src_dir):
|
||||||
|
print(f"Директория не найдена: {src_dir}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
kotlin_files = find_kotlin_files(src_dir)
|
||||||
|
print(f"Найдено {len(kotlin_files)} Kotlin файлов")
|
||||||
|
|
||||||
|
# Сначала считаем логи
|
||||||
|
total_logs_before = 0
|
||||||
|
files_with_logs = []
|
||||||
|
for filepath in kotlin_files:
|
||||||
|
count = count_logs_in_file(filepath)
|
||||||
|
if count > 0:
|
||||||
|
total_logs_before += count
|
||||||
|
files_with_logs.append((filepath, count))
|
||||||
|
|
||||||
|
print(f"Найдено {total_logs_before} Log вызовов в {len(files_with_logs)} файлах")
|
||||||
|
|
||||||
|
if '--dry-run' in sys.argv:
|
||||||
|
print("\n[DRY RUN] Файлы с логами:")
|
||||||
|
for filepath, count in sorted(files_with_logs, key=lambda x: -x[1]):
|
||||||
|
print(f" {count:3d} логов: {os.path.basename(filepath)}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Удаляем логи
|
||||||
|
modified_count = 0
|
||||||
|
for filepath in kotlin_files:
|
||||||
|
if remove_logs_from_file(filepath):
|
||||||
|
modified_count += 1
|
||||||
|
print(f"✓ {os.path.basename(filepath)}")
|
||||||
|
|
||||||
|
# Считаем оставшиеся
|
||||||
|
total_logs_after = 0
|
||||||
|
for filepath in kotlin_files:
|
||||||
|
total_logs_after += count_logs_in_file(filepath)
|
||||||
|
|
||||||
|
print(f"\n{'='*50}")
|
||||||
|
print(f"Изменено файлов: {modified_count}")
|
||||||
|
print(f"Логов до: {total_logs_before}")
|
||||||
|
print(f"Логов после: {total_logs_after}")
|
||||||
|
print(f"Удалено: {total_logs_before - total_logs_after}")
|
||||||
|
|
||||||
|
if total_logs_after > 0:
|
||||||
|
print(f"\n⚠️ Осталось {total_logs_after} логов (возможно сложные многострочные)")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user