Project

General

Profile

• Código e Implementación

Componentes bases.

@OptIn(ExperimentalMaterial3Api::class)
    @Composable
    fun SimpleTopBar(title: String, onBack: () -> Unit) {
        CenterAlignedTopAppBar(
            title = { Text(text = title, fontWeight = FontWeight.Bold) },
            navigationIcon = {
                IconButton(onClick = onBack) {
                    Text(text = "←", fontSize = 20.sp)
                }
            }
        )
    }

    @Composable
    fun GradientBackground(content: @Composable ColumnScope.() -> Unit) {
        val bg = Brush.verticalGradient(
            colors = listOf(
                Color(0xFFEFF6FF),
                Color(0xFFFFFFFF)
            )
        )
        Column(
            modifier = Modifier
                .fillMaxSize()
                .background(bg)
                .padding(24.dp),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center,
            content = content
        )
    }

    @Composable
    fun PrimaryPillButton(text: String, onClick: () -> Unit) {
        Button(
            onClick = onClick,
            modifier = Modifier
                .fillMaxWidth()
                .height(46.dp),
            shape = RoundedCornerShape(999.dp)
        ) {
            Text(text = text, fontWeight = FontWeight.SemiBold)
        }
    }

    @Composable
    fun OutlinePillButton(text: String, onClick: () -> Unit) {
        OutlinedButton(
            onClick = onClick,
            modifier = Modifier
                .fillMaxWidth()
                .height(46.dp),
            shape = RoundedCornerShape(999.dp)
        ) {
            Text(text = text, fontWeight = FontWeight.SemiBold)
        }
    }

Ventana principal
@Composable
    fun HomeScreen(
        onGoContainers: () -> Unit,
        onGoNotifications: () -> Unit,
        onGoLogin: () -> Unit
    ) {
        GradientBackground {
            // “Logo” simple estilo BR (placeholder)
            Image(
                painter = painterResource(id = R.drawable.logo_binraiders),
                contentDescription = "Logo Bin Raiders",
                modifier = Modifier
                    .size(300.dp)
                    .clip(CircleShape),
                contentScale = ContentScale.Crop
            )

            Spacer(Modifier.height(18.dp))

            Text("¡Bienvenidos a\nBin Raiders!", fontSize = 28.sp, fontWeight = FontWeight.Bold)
            Spacer(Modifier.height(28.dp))

            PrimaryPillButton("Ver contenedores", onGoContainers)
            Spacer(Modifier.height(12.dp))
            OutlinePillButton("Notificaciones", onGoNotifications)
            Spacer(Modifier.height(12.dp))
            PrimaryPillButton("Acceder", onGoLogin)

            Spacer(Modifier.height(18.dp))
            Text("Powered by Raspberry Pi 4", color = Color(0xFF6B7280), fontSize = 13.sp)
        }
    }

Contenedores

data class ContainerItem(
        val id: Int,
        val name: String,
        val address: String,
        val imageHint: String = "" 
    )

Ventana Contenedores

@Composable
    fun ContainersScreen(
        onBack: () -> Unit,
        onOpenCamera: (ContainerItem) -> Unit
    ) {
        val containers = remember {
            listOf(
                ContainerItem(1, "Contenedor 1", "Dirección: Av. Diego Portales & Las Torres"),
                ContainerItem(2, "Contenedor 2", "Dirección: Av. Edmundo Flores 112"),
                ContainerItem(3, "Contenedor 3", "Dirección: Av. Santa María 421")
            )
        }

        var estado by remember { mutableStateOf<EstadoResponse?>(null) }
        var loading by remember { mutableStateOf(false) }
        val scope = rememberCoroutineScope()

        suspend fun refreshEstado() {
            loading = true
            val res = withContext(Dispatchers.IO) { ApiClient.getEstado() }
            estado = res
            loading = false
        }

        // 1) Primera carga
        LaunchedEffect(Unit) {
            refreshEstado()
        }

        // 2) Auto-refresh para demo (cada 2.5s)
        LaunchedEffect(Unit) {
            while (true) {
                delay(2500)
                refreshEstado()
            }
        }

        Scaffold(
            topBar = { SimpleTopBar(title = "Contenedores", onBack = onBack) }
        ) { pad ->
            Column(
                modifier = Modifier
                    .padding(pad)
                    .fillMaxSize()
                    .padding(16.dp),
                verticalArrangement = Arrangement.spacedBy(14.dp)
            ) {
                EstadoSensorCard(
                    estado = estado,
                    loading = loading,
                    onRefresh = { scope.launch { refreshEstado() } }
                )

                LazyColumn(
                    modifier = Modifier.fillMaxSize(),
                    verticalArrangement = Arrangement.spacedBy(12.dp)
                ) {
                    items(containers) { c ->
                        ContainerCard(item = c, onOpenCamera = { onOpenCamera(c) })
                    }
                }
            }
        }
    }

    @Composable
    fun ContainerCard(
        item: ContainerItem,
        onOpenCamera: () -> Unit
    ) {
        var enabled by remember { mutableStateOf(true) }

        Card(
            modifier = Modifier.fillMaxWidth(),
            shape = RoundedCornerShape(16.dp)
        ) {
            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(14.dp),
                verticalAlignment = Alignment.CenterVertically
            ) {
                // Mini imagen placeholder
                Box(
                    modifier = Modifier
                        .size(68.dp)
                        .clip(RoundedCornerShape(12.dp))
                        .background(Color(0xFFE5E7EB))
                        .border(1.dp, Color(0xFFD1D5DB), RoundedCornerShape(12.dp)),
                    contentAlignment = Alignment.Center
                ) {
                    Text("IMG", color = Color(0xFF6B7280), fontSize = 12.sp)
                }

                Spacer(Modifier.width(12.dp))

                Column(modifier = Modifier.weight(1f)) {
                    Text(item.name, fontWeight = FontWeight.Bold)
                    Spacer(Modifier.height(2.dp))
                    Text(item.address, color = Color(0xFF6B7280), fontSize = 13.sp)

                    Spacer(Modifier.height(10.dp))

                    Row(verticalAlignment = Alignment.CenterVertically) {
                        Button(
                            onClick = onOpenCamera,
                            shape = RoundedCornerShape(10.dp),
                            contentPadding = PaddingValues(horizontal = 14.dp, vertical = 10.dp)
                        ) {
                            Text("Ver cámara")
                        }
                        Spacer(Modifier.width(10.dp))
                        Switch(checked = enabled, onCheckedChange = { enabled = it })
                    }
                }
            }
        }
    }

Ventana camara

@Composable
    fun CameraScreen(
        container: ContainerItem?,
        onBack: () -> Unit
    ) {
        val streamUrl = ApiConfig.STREAM_URL  

        Scaffold(
            topBar = { SimpleTopBar(title = "Cámara", onBack = onBack) }
        ) { pad ->
            Column(
                modifier = Modifier
                    .padding(pad)
                    .fillMaxSize()
                    .padding(16.dp),
                verticalArrangement = Arrangement.spacedBy(12.dp)
            ) {
                Text(
                    text = container?.name ?: "Contenedor",
                    fontWeight = FontWeight.Bold,
                    fontSize = 18.sp
                )
                Text(
                    text = container?.address ?: "",
                    color = Color(0xFF6B7280)
                )

                MjpegWebView(url = streamUrl)

                Text(
                    text = "Fuente: $streamUrl",
                    fontSize = 12.sp,
                    color = Color(0xFF6B7280)
                )
            }
        }
    }

    @Composable
    fun MjpegWebView(url: String) {
        AndroidView(
            modifier = Modifier
                .fillMaxWidth()
                .height(420.dp)
                .clip(RoundedCornerShape(16.dp))
                .border(1.dp, Color(0xFFE5E7EB), RoundedCornerShape(16.dp)),
            factory = { ctx ->
                WebView(ctx).apply {
                    webViewClient = WebViewClient()
                    settings.javaScriptEnabled = false
                    settings.loadWithOverviewMode = true
                    settings.useWideViewPort = true
                    loadUrl(url)
                }
            },
            update = { webView ->
                if (webView.url != url) webView.loadUrl(url)
            }
        )
    }

    @Composable
    fun EstadoSensorCard(
        estado: EstadoResponse?,
        loading: Boolean,
        onRefresh: () -> Unit
    ) {
        val pct = estado?.fill_percent ?: 0
        val label = when {
            loading -> "Cargando..." 
            estado == null -> "Sin datos" 
            estado.ok -> "Estado: ${estado.estado}" 
            else -> "Error: ${estado.error ?: "sensor_no_data"}" 
        }

        val color = when (estado?.estado) {
            "LLENO" -> Color(0xFFEF4444) // rojo
            "MEDIO" -> Color(0xFFF59E0B) // amarillo
            "VACIO" -> Color(0xFF22C55E) // verde
            else -> Color(0xFF6B7280)    // gris
        }

        Card(shape = RoundedCornerShape(16.dp), modifier = Modifier.fillMaxWidth()) {
            Column(Modifier.padding(14.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
                Row(verticalAlignment = Alignment.CenterVertically) {
                    Text("Estado del contenedor", fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f))
                    AssistChip(
                        onClick = {},
                        label = { Text(estado?.estado ?: "—") },
                        colors = AssistChipDefaults.assistChipColors(
                            labelColor = Color.White,
                            containerColor = color
                        )
                    )
                }

                Text(label, color = Color(0xFF374151))

                if (estado?.ok == true) {
                    Text("Distancia: ${estado.distance_cm} cm")
                    Text("Relleno: ${estado.fill_percent}%")
                    LinearProgressIndicator(
                        progress = (pct.coerceIn(0, 100) / 100f),
                        modifier = Modifier.fillMaxWidth()
                    )
                    Text(
                        "Edad dato: ${estado.sensor_age_ms ?: 0} ms",
                        fontSize = 12.sp,
                        color = Color(0xFF6B7280)
                    )
                }

                Button(
                    onClick = onRefresh,
                    enabled = !loading,
                    shape = RoundedCornerShape(12.dp),
                    modifier = Modifier.fillMaxWidth()
                ) {
                    Text(if (loading) "Actualizando..." else "Actualizar")
                }
            }
        }
    }