package no.naiv.tiltshift.ui import android.graphics.SurfaceTexture import android.location.Location import android.opengl.GLSurfaceView import android.view.Surface import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import no.naiv.tiltshift.camera.CameraManager import no.naiv.tiltshift.camera.ImageCaptureHandler import no.naiv.tiltshift.effect.BlurParameters import no.naiv.tiltshift.effect.TiltShiftRenderer import no.naiv.tiltshift.storage.PhotoSaver import no.naiv.tiltshift.storage.SaveResult import no.naiv.tiltshift.util.HapticFeedback import no.naiv.tiltshift.util.LocationProvider import no.naiv.tiltshift.util.OrientationDetector /** * Main camera screen with tilt-shift controls. */ @Composable fun CameraScreen( modifier: Modifier = Modifier ) { val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current val scope = rememberCoroutineScope() // Camera and effect state val cameraManager = remember { CameraManager(context) } val photoSaver = remember { PhotoSaver(context) } val captureHandler = remember { ImageCaptureHandler(context, photoSaver) } val haptics = remember { HapticFeedback(context) } val orientationDetector = remember { OrientationDetector(context) } val locationProvider = remember { LocationProvider(context) } // State var blurParams by remember { mutableStateOf(BlurParameters.DEFAULT) } var surfaceTexture by remember { mutableStateOf(null) } var renderer by remember { mutableStateOf(null) } var glSurfaceView by remember { mutableStateOf(null) } var isCapturing by remember { mutableStateOf(false) } var showSaveSuccess by remember { mutableStateOf(false) } var showSaveError by remember { mutableStateOf(null) } var currentRotation by remember { mutableStateOf(Surface.ROTATION_0) } var currentLocation by remember { mutableStateOf(null) } val zoomRatio by cameraManager.zoomRatio.collectAsState() val minZoom by cameraManager.minZoomRatio.collectAsState() val maxZoom by cameraManager.maxZoomRatio.collectAsState() // Collect orientation updates LaunchedEffect(Unit) { orientationDetector.orientationFlow().collectLatest { rotation -> currentRotation = rotation } } // Collect location updates LaunchedEffect(Unit) { locationProvider.locationFlow().collectLatest { location -> currentLocation = location } } // Update renderer with blur params LaunchedEffect(blurParams) { renderer?.updateParameters(blurParams) glSurfaceView?.requestRender() } // Start camera when surface texture is available LaunchedEffect(surfaceTexture) { surfaceTexture?.let { cameraManager.startCamera(lifecycleOwner) { surfaceTexture } } } // Cleanup DisposableEffect(Unit) { onDispose { cameraManager.release() renderer?.release() } } Box( modifier = modifier .fillMaxSize() .background(Color.Black) ) { // OpenGL Surface for camera preview with effect AndroidView( factory = { ctx -> GLSurfaceView(ctx).apply { setEGLContextClientVersion(2) val newRenderer = TiltShiftRenderer(ctx) { st -> surfaceTexture = st } renderer = newRenderer setRenderer(newRenderer) renderMode = GLSurfaceView.RENDERMODE_CONTINUOUSLY glSurfaceView = this } }, modifier = Modifier.fillMaxSize() ) // Tilt-shift overlay (gesture handling + visualization) TiltShiftOverlay( params = blurParams, onParamsChange = { newParams -> blurParams = newParams haptics.tick() }, onZoomChange = { zoomDelta -> val newZoom = (zoomRatio * zoomDelta).coerceIn(minZoom, maxZoom) cameraManager.setZoom(newZoom) }, modifier = Modifier.fillMaxSize() ) // Top bar Row( modifier = Modifier .fillMaxWidth() .statusBarsPadding() .padding(16.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { // Zoom indicator ZoomIndicator(currentZoom = zoomRatio) // Settings button (placeholder) IconButton( onClick = { /* TODO: Settings */ } ) { Icon( imageVector = Icons.Default.Settings, contentDescription = "Settings", tint = Color.White ) } } // Bottom controls Column( modifier = Modifier .align(Alignment.BottomCenter) .navigationBarsPadding() .padding(bottom = 24.dp), horizontalAlignment = Alignment.CenterHorizontally ) { // Zoom presets ZoomControl( currentZoom = zoomRatio, minZoom = minZoom, maxZoom = maxZoom, onZoomSelected = { zoom -> cameraManager.setZoom(zoom) haptics.click() } ) Spacer(modifier = Modifier.height(24.dp)) // Capture button CaptureButton( isCapturing = isCapturing, onClick = { if (!isCapturing) { isCapturing = true haptics.heavyClick() scope.launch { val imageCapture = cameraManager.imageCapture if (imageCapture != null) { val result = captureHandler.capturePhoto( imageCapture = imageCapture, executor = cameraManager.getExecutor(), blurParams = blurParams, deviceRotation = currentRotation, location = currentLocation, isFrontCamera = false ) when (result) { is SaveResult.Success -> { haptics.success() showSaveSuccess = true delay(1500) showSaveSuccess = false } is SaveResult.Error -> { haptics.error() showSaveError = result.message delay(2000) showSaveError = null } } } isCapturing = false } } } ) } // Success indicator AnimatedVisibility( visible = showSaveSuccess, enter = fadeIn(), exit = fadeOut(), modifier = Modifier.align(Alignment.Center) ) { Box( modifier = Modifier .size(80.dp) .clip(CircleShape) .background(Color(0xFF4CAF50)), contentAlignment = Alignment.Center ) { Icon( imageVector = Icons.Default.Check, contentDescription = "Saved", tint = Color.White, modifier = Modifier.size(48.dp) ) } } // Error indicator AnimatedVisibility( visible = showSaveError != null, enter = fadeIn(), exit = fadeOut(), modifier = Modifier .align(Alignment.Center) .padding(32.dp) ) { Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier .clip(androidx.compose.foundation.shape.RoundedCornerShape(16.dp)) .background(Color(0xFFF44336)) .padding(24.dp) ) { Icon( imageVector = Icons.Default.Close, contentDescription = "Error", tint = Color.White, modifier = Modifier.size(32.dp) ) Spacer(modifier = Modifier.height(8.dp)) Text( text = showSaveError ?: "Error", color = Color.White, fontSize = 14.sp ) } } } } /** * Capture button with animation for capturing state. */ @Composable private fun CaptureButton( isCapturing: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier ) { val outerSize = 72.dp val innerSize = if (isCapturing) 48.dp else 60.dp Box( modifier = modifier .size(outerSize) .clip(CircleShape) .border(4.dp, Color.White, CircleShape) .clickable(enabled = !isCapturing, onClick = onClick), contentAlignment = Alignment.Center ) { Box( modifier = Modifier .size(innerSize) .clip(CircleShape) .background(if (isCapturing) Color(0xFFFFB300) else Color.White) ) } }