Restore viewer window state and handle photo decoding errors

This commit is contained in:
evgeny committed 2026-10-29 13:34:25 +01:00
1 parent bd4bd47e00
commit efe421b162
2 files changed
+69 -23

No files matched your search

@@ -2,9 +2,6 @@ package com.utun.chat.ui.screens
import com.utun.chat.R
import androidx.compose.ui.res.stringResource
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.graphics.Bitmap
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
@@ -19,43 +16,33 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import com.utun.chat.data.LogManager
import com.utun.chat.data.PhotoUtil
import com.utun.chat.ui.components.ZoomableImage
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.withContext
private const val MAX_DECODE_DIM = 4096
private tailrec fun Context.findActivity(): Activity? = when (this) {
is Activity -> this
is ContextWrapper -> baseContext.findActivity()
else -> null
}
/** Локальное изображение с EXIF и Fit/zoom; декодирование в IO ограничено стороной 4096 пикселей.
* Экран владеет Bitmap через Compose; смена пути отменяет доставку старого результата. Ошибка оставляет доступным выход. */
@Composable
fun PhotoViewerScreen(filePath: String, onClose: () -> Unit) {
val context = LocalContext.current
var bmp by remember(filePath) { mutableStateOf<Bitmap?>(null) }
var loaded by remember(filePath) { mutableStateOf(false) }
BackHandler(onBack = onClose)
/* ── immersive: hide system bars ── */
DisposableEffect(Unit) {
val act = context.findActivity()
val controller = act?.let { WindowCompat.getInsetsController(it.window, it.window.decorView) }
controller?.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
controller?.hide(WindowInsetsCompat.Type.systemBars())
onDispose { controller?.show(WindowInsetsCompat.Type.systemBars()) }
}
ViewerFullscreenWindow("photo")
LaunchedEffect(filePath) {
bmp = withContext(Dispatchers.IO) { PhotoUtil.decodeFitted(filePath, MAX_DECODE_DIM) }
bmp = try { withContext(Dispatchers.IO) { PhotoUtil.decodeFitted(filePath, MAX_DECODE_DIM) } }
catch (e: CancellationException) { throw e }
catch (e: Exception) {
LogManager.addLog("ERROR", "PhotoViewer", "decode exception path=$filePath: $e")
null
}
loaded = true
if (bmp == null) LogManager.addLog("ERROR", "PhotoViewer", "decode failed: $filePath")
else LogManager.addLog("DEBUG", "PhotoViewer", "Opened image ${bmp?.width}x${bmp?.height} path=$filePath")
@@ -0,0 +1,59 @@
package com.utun.chat.ui.screens
import android.app.Activity
import android.content.ContextWrapper
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import com.utun.chat.data.LogManager
/** Immersive-режим viewer без смены ориентации; возвращает прежнюю видимость обеих панелей и поведение swipe.
* Вызывающий экран должен быть единственным владельцем fullscreen окна. identity — диагностическое имя без секретов. */
@Composable
internal fun ViewerFullscreenWindow(identity: String) {
val context = LocalContext.current
val activity = remember(context) {
generateSequence(context) { (it as? ContextWrapper)?.baseContext }.filterIsInstance<Activity>().firstOrNull()
}
DisposableEffect(activity, identity) {
val window = activity?.window
val controller = window?.let { WindowCompat.getInsetsController(it, it.decorView) }
val insets = window?.let { ViewCompat.getRootWindowInsets(it.decorView) }
val statusVisible = insets?.isVisible(WindowInsetsCompat.Type.statusBars()) ?: true
val navigationVisible = insets?.isVisible(WindowInsetsCompat.Type.navigationBars()) ?: true
val behavior = controller?.systemBarsBehavior
if (controller != null) try {
controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
controller.hide(WindowInsetsCompat.Type.systemBars())
LogManager.addLog("INFO", "Viewer", "Fullscreen entered viewer=$identity")
} catch (e: Exception) {
LogManager.addLog("ERROR", "Viewer", "Cannot enter fullscreen viewer=$identity: $e")
}
onDispose {
if (controller != null && behavior != null) {
val restore: List<Pair<String, () -> Unit>> = listOf(
"behavior" to { controller.systemBarsBehavior = behavior },
"status" to {
if (statusVisible) controller.show(WindowInsetsCompat.Type.statusBars())
else controller.hide(WindowInsetsCompat.Type.statusBars())
},
"navigation" to {
if (navigationVisible) controller.show(WindowInsetsCompat.Type.navigationBars())
else controller.hide(WindowInsetsCompat.Type.navigationBars())
}
)
var failures = 0
for ((part, action) in restore) try { action() } catch (e: Exception) {
failures++
LogManager.addLog("ERROR", "Viewer", "Window restore failed viewer=$identity part=$part: $e")
}
if (failures == 0) LogManager.addLog("INFO", "Viewer", "Window restored viewer=$identity")
}
}
}
}