feat: implement system bars style utility for consistent UI behavior
This commit is contained in:
@@ -52,7 +52,6 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
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 androidx.core.view.WindowCompat
|
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
@@ -79,6 +78,7 @@ import com.rosetta.messenger.ui.chats.utils.*
|
|||||||
import com.rosetta.messenger.ui.components.AvatarImage
|
import com.rosetta.messenger.ui.components.AvatarImage
|
||||||
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.utils.SystemBarsStyleUtils
|
||||||
import com.rosetta.messenger.utils.MediaUtils
|
import com.rosetta.messenger.utils.MediaUtils
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@@ -191,22 +191,17 @@ fun ChatDetailScreen(
|
|||||||
var imageViewerSourceBounds by remember { mutableStateOf<ImageSourceBounds?>(null) }
|
var imageViewerSourceBounds by remember { mutableStateOf<ImageSourceBounds?>(null) }
|
||||||
|
|
||||||
// 🎨 Управление статус баром
|
// 🎨 Управление статус баром
|
||||||
DisposableEffect(isDarkTheme, showImageViewer) {
|
DisposableEffect(isDarkTheme, showImageViewer, window, view) {
|
||||||
val insetsController = window?.let { WindowCompat.getInsetsController(it, view) }
|
|
||||||
|
|
||||||
if (showImageViewer) {
|
if (showImageViewer) {
|
||||||
// 📸 При просмотре фото - чёрный статус бар
|
// 📸 При просмотре фото - чёрные system bars
|
||||||
window?.statusBarColor = android.graphics.Color.BLACK
|
SystemBarsStyleUtils.applyFullscreenDark(window, view)
|
||||||
insetsController?.isAppearanceLightStatusBars = false
|
|
||||||
} else {
|
} else {
|
||||||
// Обычный режим - прозрачный статус бар, иконки по теме
|
// Обычный режим чата
|
||||||
window?.statusBarColor = android.graphics.Color.TRANSPARENT
|
SystemBarsStyleUtils.applyChatStatusBar(window, view, isDarkTheme)
|
||||||
insetsController?.isAppearanceLightStatusBars = !isDarkTheme
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onDispose {
|
onDispose {
|
||||||
// Восстанавливаем прозрачный статус бар при выходе
|
SystemBarsStyleUtils.applyChatStatusBar(window, view, isDarkTheme)
|
||||||
window?.statusBarColor = android.graphics.Color.TRANSPARENT
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1990,11 +1985,7 @@ fun ChatDetailScreen(
|
|||||||
},
|
},
|
||||||
onClosingStart = {
|
onClosingStart = {
|
||||||
// Сразу сбрасываем status bar при начале закрытия (до анимации)
|
// Сразу сбрасываем status bar при начале закрытия (до анимации)
|
||||||
window?.statusBarColor = android.graphics.Color.TRANSPARENT
|
SystemBarsStyleUtils.applyChatStatusBar(window, view, isDarkTheme)
|
||||||
window?.let { w ->
|
|
||||||
WindowCompat.getInsetsController(w, view)
|
|
||||||
?.isAppearanceLightStatusBars = !isDarkTheme
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
isDarkTheme = isDarkTheme,
|
isDarkTheme = isDarkTheme,
|
||||||
sourceBounds = imageViewerSourceBounds
|
sourceBounds = imageViewerSourceBounds
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ import com.rosetta.messenger.ui.components.AppleEmojiTextField
|
|||||||
import com.rosetta.messenger.ui.components.KeyboardHeightProvider
|
import com.rosetta.messenger.ui.components.KeyboardHeightProvider
|
||||||
import com.rosetta.messenger.ui.components.OptimizedEmojiPicker
|
import com.rosetta.messenger.ui.components.OptimizedEmojiPicker
|
||||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||||
|
import com.rosetta.messenger.ui.utils.SystemBarsStyleUtils
|
||||||
import com.yalantis.ucrop.UCrop
|
import com.yalantis.ucrop.UCrop
|
||||||
import compose.icons.TablerIcons
|
import compose.icons.TablerIcons
|
||||||
import compose.icons.tablericons.*
|
import compose.icons.tablericons.*
|
||||||
@@ -68,7 +69,6 @@ import ja.burhanrashid52.photoeditor.PhotoEditorView
|
|||||||
import ja.burhanrashid52.photoeditor.SaveSettings
|
import ja.burhanrashid52.photoeditor.SaveSettings
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import androidx.core.view.WindowCompat
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -170,59 +170,26 @@ fun ImageEditorScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🎨 Плавная анимация status bar синхронно с fade
|
// 🎨 System bars style
|
||||||
val activity = context as? Activity
|
val activity = context as? Activity
|
||||||
val window = activity?.window
|
val window = activity?.window
|
||||||
|
val systemBarsState = remember(window, view) {
|
||||||
// Сохраняем оригинальные цвета один раз
|
SystemBarsStyleUtils.capture(window, view)
|
||||||
val originalStatusBarColor = remember { window?.statusBarColor ?: android.graphics.Color.WHITE }
|
|
||||||
val originalNavigationBarColor = remember { window?.navigationBarColor ?: android.graphics.Color.WHITE }
|
|
||||||
val insetsController = remember(window, view) { window?.let { WindowCompat.getInsetsController(it, view) } }
|
|
||||||
val originalLightStatusBars = remember { insetsController?.isAppearanceLightStatusBars ?: true }
|
|
||||||
val originalLightNavigationBars = remember { insetsController?.isAppearanceLightNavigationBars ?: true }
|
|
||||||
|
|
||||||
// ⚡ Анимация цвета status bar синхронно с fade
|
|
||||||
LaunchedEffect(animationProgress.value) {
|
|
||||||
if (window == null || insetsController == null) return@LaunchedEffect
|
|
||||||
|
|
||||||
val progress = animationProgress.value
|
|
||||||
|
|
||||||
// Интерполируем цвет: оригинальный (progress=0) -> черный (progress=1)
|
|
||||||
val currentStatusColor = androidx.core.graphics.ColorUtils.blendARGB(
|
|
||||||
originalStatusBarColor,
|
|
||||||
android.graphics.Color.BLACK,
|
|
||||||
progress
|
|
||||||
)
|
|
||||||
val currentNavColor = androidx.core.graphics.ColorUtils.blendARGB(
|
|
||||||
originalNavigationBarColor,
|
|
||||||
android.graphics.Color.BLACK,
|
|
||||||
progress
|
|
||||||
)
|
|
||||||
|
|
||||||
window.statusBarColor = currentStatusColor
|
|
||||||
window.navigationBarColor = currentNavColor
|
|
||||||
|
|
||||||
// Иконки: светлые (false) когда progress > 0.5, иначе оригинальные
|
|
||||||
insetsController.isAppearanceLightStatusBars = progress < 0.5f && originalLightStatusBars
|
|
||||||
insetsController.isAppearanceLightNavigationBars = progress < 0.5f && originalLightNavigationBars
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Восстановление при dispose
|
|
||||||
DisposableEffect(window) {
|
|
||||||
onDispose {
|
|
||||||
if (window == null || insetsController == null) return@onDispose
|
|
||||||
window.statusBarColor = originalStatusBarColor
|
|
||||||
insetsController.isAppearanceLightStatusBars = originalLightStatusBars
|
|
||||||
|
|
||||||
// Navigation bar: восстанавливаем только если есть нативные кнопки
|
LaunchedEffect(window, view) {
|
||||||
if (com.rosetta.messenger.ui.utils.NavigationModeUtils.hasNativeNavigationBar(context)) {
|
SystemBarsStyleUtils.applyFullscreenDark(window, view)
|
||||||
window.navigationBarColor = originalNavigationBarColor
|
}
|
||||||
insetsController.isAppearanceLightNavigationBars = originalLightNavigationBars
|
|
||||||
} else {
|
DisposableEffect(window, view, isDarkTheme, systemBarsState) {
|
||||||
insetsController.hide(androidx.core.view.WindowInsetsCompat.Type.navigationBars())
|
onDispose {
|
||||||
insetsController.systemBarsBehavior =
|
SystemBarsStyleUtils.restoreChatAfterFullscreen(
|
||||||
androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
window = window,
|
||||||
}
|
view = view,
|
||||||
|
context = context,
|
||||||
|
isDarkTheme = isDarkTheme,
|
||||||
|
state = systemBarsState
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,43 +375,47 @@ fun ImageEditorScreen(
|
|||||||
down.consume() // Поглощаем все touch события
|
down.consume() // Поглощаем все touch события
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.graphicsLayer { alpha = animationProgress.value } // ⚡ Всё плавно fade
|
|
||||||
.background(Color.Black)
|
.background(Color.Black)
|
||||||
) {
|
) {
|
||||||
// ═══════════════════════════════════════════════════════════
|
Box(
|
||||||
// 📸 FULLSCREEN PHOTO - занимает ВЕСЬ экран, не реагирует на клавиатуру
|
modifier =
|
||||||
// ═══════════════════════════════════════════════════════════
|
Modifier.fillMaxSize()
|
||||||
AndroidView(
|
.graphicsLayer { alpha = animationProgress.value }
|
||||||
factory = { ctx ->
|
) {
|
||||||
PhotoEditorView(ctx).apply {
|
// ═══════════════════════════════════════════════════════════
|
||||||
photoEditorView = this
|
// 📸 FULLSCREEN PHOTO - занимает ВЕСЬ экран, не реагирует на клавиатуру
|
||||||
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
AndroidView(
|
||||||
|
factory = { ctx ->
|
||||||
|
PhotoEditorView(ctx).apply {
|
||||||
|
photoEditorView = this
|
||||||
|
|
||||||
// Убираем отступы
|
// Убираем отступы
|
||||||
setPadding(0, 0, 0, 0)
|
|
||||||
setBackgroundColor(android.graphics.Color.BLACK)
|
|
||||||
|
|
||||||
// Простой FIT_CENTER - показывает ВСЁ фото, центрирует
|
|
||||||
source.apply {
|
|
||||||
setImageURI(currentImageUri)
|
|
||||||
scaleType = ImageView.ScaleType.FIT_CENTER
|
|
||||||
adjustViewBounds = true
|
|
||||||
setPadding(0, 0, 0, 0)
|
setPadding(0, 0, 0, 0)
|
||||||
}
|
setBackgroundColor(android.graphics.Color.BLACK)
|
||||||
|
|
||||||
photoEditor = PhotoEditor.Builder(ctx, this)
|
// Простой FIT_CENTER - показывает ВСЁ фото, центрирует
|
||||||
.setPinchTextScalable(true)
|
source.apply {
|
||||||
.setClipSourceImage(true)
|
setImageURI(currentImageUri)
|
||||||
.build()
|
scaleType = ImageView.ScaleType.FIT_CENTER
|
||||||
}
|
adjustViewBounds = true
|
||||||
},
|
setPadding(0, 0, 0, 0)
|
||||||
update = { view ->
|
}
|
||||||
view.source.rotation = rotationAngle
|
|
||||||
view.source.scaleX = if (isFlippedHorizontally) -1f else 1f
|
photoEditor = PhotoEditor.Builder(ctx, this)
|
||||||
view.source.scaleY = if (isFlippedVertically) -1f else 1f
|
.setPinchTextScalable(true)
|
||||||
},
|
.setClipSourceImage(true)
|
||||||
// КРИТИЧНО: fillMaxSize без imePadding - фото НЕ сжимается при клавиатуре
|
.build()
|
||||||
modifier = Modifier.fillMaxSize()
|
}
|
||||||
)
|
},
|
||||||
|
update = { view ->
|
||||||
|
view.source.rotation = rotationAngle
|
||||||
|
view.source.scaleX = if (isFlippedHorizontally) -1f else 1f
|
||||||
|
view.source.scaleY = if (isFlippedVertically) -1f else 1f
|
||||||
|
},
|
||||||
|
// КРИТИЧНО: fillMaxSize без imePadding - фото НЕ сжимается при клавиатуре
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
// 🎛️ TOP BAR - Solid black (Telegram style)
|
// 🎛️ TOP BAR - Solid black (Telegram style)
|
||||||
@@ -682,14 +653,14 @@ fun ImageEditorScreen(
|
|||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
val isKeyboardOpen = WindowInsets.ime.getBottom(LocalDensity.current) > 0
|
val isKeyboardOpen = WindowInsets.ime.getBottom(LocalDensity.current) > 0
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = !isKeyboardOpen && !showEmojiPicker && !coordinator.isEmojiBoxVisible,
|
visible = !isKeyboardOpen && !showEmojiPicker && !coordinator.isEmojiBoxVisible,
|
||||||
enter = fadeIn() + slideInVertically { it },
|
enter = fadeIn() + slideInVertically { it },
|
||||||
exit = fadeOut() + slideOutVertically { it },
|
exit = fadeOut() + slideOutVertically { it },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.align(Alignment.BottomCenter)
|
.align(Alignment.BottomCenter)
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -779,6 +750,7 @@ fun ImageEditorScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1539,59 +1511,26 @@ fun MultiImageEditorScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🎨 Плавная анимация status bar синхронно с fade
|
// 🎨 System bars style
|
||||||
val activity = context as? Activity
|
val activity = context as? Activity
|
||||||
val window = activity?.window
|
val window = activity?.window
|
||||||
|
val systemBarsState = remember(window, view) {
|
||||||
// Сохраняем оригинальные цвета один раз
|
SystemBarsStyleUtils.capture(window, view)
|
||||||
val originalStatusBarColor = remember { window?.statusBarColor ?: android.graphics.Color.WHITE }
|
|
||||||
val originalNavigationBarColor = remember { window?.navigationBarColor ?: android.graphics.Color.WHITE }
|
|
||||||
val insetsController = remember(window, view) { window?.let { WindowCompat.getInsetsController(it, view) } }
|
|
||||||
val originalLightStatusBars = remember { insetsController?.isAppearanceLightStatusBars ?: true }
|
|
||||||
val originalLightNavigationBars = remember { insetsController?.isAppearanceLightNavigationBars ?: true }
|
|
||||||
|
|
||||||
// ⚡ Анимация цвета status bar синхронно с fade
|
|
||||||
LaunchedEffect(animationProgress.value) {
|
|
||||||
if (window == null || insetsController == null) return@LaunchedEffect
|
|
||||||
|
|
||||||
val progress = animationProgress.value
|
|
||||||
|
|
||||||
// Интерполируем цвет: оригинальный (progress=0) -> черный (progress=1)
|
|
||||||
val currentStatusColor = androidx.core.graphics.ColorUtils.blendARGB(
|
|
||||||
originalStatusBarColor,
|
|
||||||
android.graphics.Color.BLACK,
|
|
||||||
progress
|
|
||||||
)
|
|
||||||
val currentNavColor = androidx.core.graphics.ColorUtils.blendARGB(
|
|
||||||
originalNavigationBarColor,
|
|
||||||
android.graphics.Color.BLACK,
|
|
||||||
progress
|
|
||||||
)
|
|
||||||
|
|
||||||
window.statusBarColor = currentStatusColor
|
|
||||||
window.navigationBarColor = currentNavColor
|
|
||||||
|
|
||||||
// Иконки: светлые (false) когда progress > 0.5, иначе оригинальные
|
|
||||||
insetsController.isAppearanceLightStatusBars = progress < 0.5f && originalLightStatusBars
|
|
||||||
insetsController.isAppearanceLightNavigationBars = progress < 0.5f && originalLightNavigationBars
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Восстановление при dispose
|
|
||||||
DisposableEffect(window) {
|
|
||||||
onDispose {
|
|
||||||
if (window == null || insetsController == null) return@onDispose
|
|
||||||
window.statusBarColor = originalStatusBarColor
|
|
||||||
insetsController.isAppearanceLightStatusBars = originalLightStatusBars
|
|
||||||
|
|
||||||
// Navigation bar: восстанавливаем только если есть нативные кнопки
|
LaunchedEffect(window, view) {
|
||||||
if (com.rosetta.messenger.ui.utils.NavigationModeUtils.hasNativeNavigationBar(context)) {
|
SystemBarsStyleUtils.applyFullscreenDark(window, view)
|
||||||
window.navigationBarColor = originalNavigationBarColor
|
}
|
||||||
insetsController.isAppearanceLightNavigationBars = originalLightNavigationBars
|
|
||||||
} else {
|
DisposableEffect(window, view, isDarkTheme, systemBarsState) {
|
||||||
insetsController.hide(androidx.core.view.WindowInsetsCompat.Type.navigationBars())
|
onDispose {
|
||||||
insetsController.systemBarsBehavior =
|
SystemBarsStyleUtils.restoreChatAfterFullscreen(
|
||||||
androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
window = window,
|
||||||
}
|
view = view,
|
||||||
|
context = context,
|
||||||
|
isDarkTheme = isDarkTheme,
|
||||||
|
state = systemBarsState
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1648,76 +1587,82 @@ fun MultiImageEditorScreen(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.graphicsLayer { alpha = animationProgress.value } // ⚡ Всё плавно fade
|
|
||||||
.background(Color.Black)
|
.background(Color.Black)
|
||||||
) {
|
) {
|
||||||
// Pager
|
Box(
|
||||||
HorizontalPager(
|
modifier =
|
||||||
state = pagerState,
|
Modifier.fillMaxSize()
|
||||||
modifier = Modifier.fillMaxSize(),
|
.graphicsLayer { alpha = animationProgress.value }
|
||||||
userScrollEnabled = currentTool == EditorTool.NONE
|
) {
|
||||||
) { page ->
|
// Pager
|
||||||
Box(
|
HorizontalPager(
|
||||||
|
state = pagerState,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentAlignment = Alignment.Center
|
userScrollEnabled = currentTool == EditorTool.NONE
|
||||||
) {
|
) { page ->
|
||||||
AndroidView(
|
Box(
|
||||||
factory = { ctx ->
|
|
||||||
PhotoEditorView(ctx).apply {
|
|
||||||
photoEditorViews[page] = this
|
|
||||||
|
|
||||||
setPadding(0, 0, 0, 0)
|
|
||||||
setBackgroundColor(android.graphics.Color.BLACK)
|
|
||||||
|
|
||||||
// Загружаем изображение
|
|
||||||
scope.launch(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
val inputStream = ctx.contentResolver.openInputStream(imagesWithCaptions[page].uri)
|
|
||||||
val bitmap = BitmapFactory.decodeStream(inputStream)
|
|
||||||
inputStream?.close()
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
source.apply {
|
|
||||||
setImageBitmap(bitmap)
|
|
||||||
scaleType = ImageView.ScaleType.FIT_CENTER
|
|
||||||
adjustViewBounds = true
|
|
||||||
setPadding(0, 0, 0, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
val editor = PhotoEditor.Builder(ctx, this@apply)
|
|
||||||
.setPinchTextScalable(true)
|
|
||||||
.build()
|
|
||||||
photoEditors[page] = editor
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// Handle error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
update = { view ->
|
contentAlignment = Alignment.Center
|
||||||
val currentUri = imagesWithCaptions.getOrNull(page)?.uri
|
) {
|
||||||
if (currentUri != null) {
|
AndroidView(
|
||||||
scope.launch(Dispatchers.IO) {
|
factory = { ctx ->
|
||||||
try {
|
PhotoEditorView(ctx).apply {
|
||||||
val inputStream = context.contentResolver.openInputStream(currentUri)
|
photoEditorViews[page] = this
|
||||||
val bitmap = BitmapFactory.decodeStream(inputStream)
|
|
||||||
inputStream?.close()
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
setPadding(0, 0, 0, 0)
|
||||||
view.source.setImageBitmap(bitmap)
|
setBackgroundColor(android.graphics.Color.BLACK)
|
||||||
view.source.scaleType = ImageView.ScaleType.FIT_CENTER
|
|
||||||
|
// Загружаем изображение
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val inputStream =
|
||||||
|
ctx.contentResolver.openInputStream(imagesWithCaptions[page].uri)
|
||||||
|
val bitmap = BitmapFactory.decodeStream(inputStream)
|
||||||
|
inputStream?.close()
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
source.apply {
|
||||||
|
setImageBitmap(bitmap)
|
||||||
|
scaleType = ImageView.ScaleType.FIT_CENTER
|
||||||
|
adjustViewBounds = true
|
||||||
|
setPadding(0, 0, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
val editor = PhotoEditor.Builder(ctx, this@apply)
|
||||||
|
.setPinchTextScalable(true)
|
||||||
|
.build()
|
||||||
|
photoEditors[page] = editor
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Handle error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
update = { view ->
|
||||||
|
val currentUri = imagesWithCaptions.getOrNull(page)?.uri
|
||||||
|
if (currentUri != null) {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val inputStream =
|
||||||
|
context.contentResolver.openInputStream(currentUri)
|
||||||
|
val bitmap = BitmapFactory.decodeStream(inputStream)
|
||||||
|
inputStream?.close()
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
view.source.setImageBitmap(bitmap)
|
||||||
|
view.source.scaleType = ImageView.ScaleType.FIT_CENTER
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Handle error
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
// Handle error
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Top bar
|
// Top bar
|
||||||
Box(
|
Box(
|
||||||
@@ -1789,19 +1734,19 @@ fun MultiImageEditorScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Bottom section - без imePadding, фото не сжимается
|
// Bottom section - без imePadding, фото не сжимается
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.align(Alignment.BottomCenter)
|
.align(Alignment.BottomCenter)
|
||||||
.background(
|
.background(
|
||||||
Brush.verticalGradient(
|
Brush.verticalGradient(
|
||||||
colors = listOf(
|
colors = listOf(
|
||||||
Color.Transparent,
|
Color.Transparent,
|
||||||
Color.Black.copy(alpha = 0.7f)
|
Color.Black.copy(alpha = 0.7f)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
) {
|
||||||
) {
|
|
||||||
// Color picker
|
// Color picker
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = showColorPicker && currentTool == EditorTool.DRAW,
|
visible = showColorPicker && currentTool == EditorTool.DRAW,
|
||||||
@@ -1956,7 +1901,8 @@ fun MultiImageEditorScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -618,14 +618,22 @@ fun MediaPickerBottomSheet(
|
|||||||
animatedClose()
|
animatedClose()
|
||||||
onOpenCamera()
|
onOpenCamera()
|
||||||
},
|
},
|
||||||
onItemClick = { item, position ->
|
onItemClick = { item, _ ->
|
||||||
// Telegram-style: клик на фото сразу открывает редактор
|
// Telegram-style selection:
|
||||||
|
// Tap toggles selection for both photos and videos.
|
||||||
|
if (item.id in selectedItems) {
|
||||||
|
selectedItems = selectedItems - item.id
|
||||||
|
} else if (selectedItems.size < maxSelection) {
|
||||||
|
selectedItems = selectedItems + item.id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onItemLongClick = { item ->
|
||||||
|
// Long press keeps quick edit for photos.
|
||||||
if (!item.isVideo) {
|
if (!item.isVideo) {
|
||||||
thumbnailPosition = position
|
thumbnailPosition = null
|
||||||
// Сразу открываем редактор - галерея закроется автоматически
|
|
||||||
editingItem = item
|
editingItem = item
|
||||||
} else {
|
} else {
|
||||||
// Для видео - добавляем/убираем из selection
|
// Videos: keep long-press toggle behavior.
|
||||||
if (item.id in selectedItems) {
|
if (item.id in selectedItems) {
|
||||||
selectedItems = selectedItems - item.id
|
selectedItems = selectedItems - item.id
|
||||||
} else if (selectedItems.size < maxSelection) {
|
} else if (selectedItems.size < maxSelection) {
|
||||||
@@ -633,14 +641,6 @@ fun MediaPickerBottomSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onItemLongClick = { item ->
|
|
||||||
// Long press - снять выделение если выбрана
|
|
||||||
if (item.id in selectedItems) {
|
|
||||||
selectedItems = selectedItems - item.id
|
|
||||||
} else if (selectedItems.size < maxSelection) {
|
|
||||||
selectedItems = selectedItems + item.id
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isDarkTheme = isDarkTheme,
|
isDarkTheme = isDarkTheme,
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1372,17 +1372,20 @@ private fun TelegramTextField(
|
|||||||
targetValue = if (hasError) errorColor else Color.Transparent,
|
targetValue = if (hasError) errorColor else Color.Transparent,
|
||||||
label = "profile_field_border_color"
|
label = "profile_field_border_color"
|
||||||
)
|
)
|
||||||
|
val fieldModifier =
|
||||||
|
if (hasError) {
|
||||||
|
Modifier.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(containerColor)
|
||||||
|
.border(1.dp, borderColor, RoundedCornerShape(12.dp))
|
||||||
|
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||||
|
} else {
|
||||||
|
Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp)
|
||||||
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
Column(
|
Column(modifier = fieldModifier) {
|
||||||
modifier =
|
|
||||||
Modifier.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
|
||||||
.clip(RoundedCornerShape(12.dp))
|
|
||||||
.background(containerColor)
|
|
||||||
.border(1.dp, borderColor, RoundedCornerShape(12.dp))
|
|
||||||
.padding(horizontal = 12.dp, vertical = 8.dp)
|
|
||||||
) {
|
|
||||||
if (isEditable && onValueChange != null) {
|
if (isEditable && onValueChange != null) {
|
||||||
BasicTextField(
|
BasicTextField(
|
||||||
value = value,
|
value = value,
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package com.rosetta.messenger.ui.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.view.View
|
||||||
|
import android.view.Window
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
|
|
||||||
|
data class SystemBarsState(
|
||||||
|
val statusBarColor: Int,
|
||||||
|
val navigationBarColor: Int,
|
||||||
|
val isLightStatusBars: Boolean,
|
||||||
|
val isLightNavigationBars: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
object SystemBarsStyleUtils {
|
||||||
|
|
||||||
|
fun capture(window: Window?, view: View?): SystemBarsState? {
|
||||||
|
if (window == null || view == null) return null
|
||||||
|
val insetsController = WindowCompat.getInsetsController(window, view)
|
||||||
|
return SystemBarsState(
|
||||||
|
statusBarColor = window.statusBarColor,
|
||||||
|
navigationBarColor = window.navigationBarColor,
|
||||||
|
isLightStatusBars = insetsController.isAppearanceLightStatusBars,
|
||||||
|
isLightNavigationBars = insetsController.isAppearanceLightNavigationBars
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun applyFullscreenDark(window: Window?, view: View?) {
|
||||||
|
if (window == null || view == null) return
|
||||||
|
val insetsController = WindowCompat.getInsetsController(window, view)
|
||||||
|
window.statusBarColor = Color.BLACK
|
||||||
|
window.navigationBarColor = Color.BLACK
|
||||||
|
insetsController.isAppearanceLightStatusBars = false
|
||||||
|
insetsController.isAppearanceLightNavigationBars = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun applyChatStatusBar(window: Window?, view: View?, isDarkTheme: Boolean) {
|
||||||
|
if (window == null || view == null) return
|
||||||
|
val insetsController = WindowCompat.getInsetsController(window, view)
|
||||||
|
window.statusBarColor = Color.TRANSPARENT
|
||||||
|
insetsController.isAppearanceLightStatusBars = !isDarkTheme
|
||||||
|
}
|
||||||
|
|
||||||
|
fun restoreNavigationBar(window: Window?, view: View?, context: Context, state: SystemBarsState?) {
|
||||||
|
if (window == null || view == null) return
|
||||||
|
val insetsController = WindowCompat.getInsetsController(window, view)
|
||||||
|
|
||||||
|
if (NavigationModeUtils.hasNativeNavigationBar(context)) {
|
||||||
|
insetsController.show(WindowInsetsCompat.Type.navigationBars())
|
||||||
|
if (state != null) {
|
||||||
|
window.navigationBarColor = state.navigationBarColor
|
||||||
|
insetsController.isAppearanceLightNavigationBars = state.isLightNavigationBars
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
insetsController.hide(WindowInsetsCompat.Type.navigationBars())
|
||||||
|
insetsController.systemBarsBehavior =
|
||||||
|
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun restoreChatAfterFullscreen(
|
||||||
|
window: Window?,
|
||||||
|
view: View?,
|
||||||
|
context: Context,
|
||||||
|
isDarkTheme: Boolean,
|
||||||
|
state: SystemBarsState?
|
||||||
|
) {
|
||||||
|
applyChatStatusBar(window, view, isDarkTheme)
|
||||||
|
restoreNavigationBar(window, view, context, state)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user