Project

General

Profile

• Código e Implementación » History » Version 3

Nelson Ramirez, 12/23/2025 03:27 PM

1 1 Nelson Ramirez
h1. • Código e Implementación
2 2 Nelson Ramirez
3 3 Nelson Ramirez
Componentes bases.
4 3 Nelson Ramirez
<pre>
5 3 Nelson Ramirez
@OptIn(ExperimentalMaterial3Api::class)
6 3 Nelson Ramirez
    @Composable
7 3 Nelson Ramirez
    fun SimpleTopBar(title: String, onBack: () -> Unit) {
8 3 Nelson Ramirez
        CenterAlignedTopAppBar(
9 3 Nelson Ramirez
            title = { Text(text = title, fontWeight = FontWeight.Bold) },
10 3 Nelson Ramirez
            navigationIcon = {
11 3 Nelson Ramirez
                IconButton(onClick = onBack) {
12 3 Nelson Ramirez
                    Text(text = "←", fontSize = 20.sp)
13 3 Nelson Ramirez
                }
14 3 Nelson Ramirez
            }
15 3 Nelson Ramirez
        )
16 3 Nelson Ramirez
    }
17 3 Nelson Ramirez
18 3 Nelson Ramirez
    @Composable
19 3 Nelson Ramirez
    fun GradientBackground(content: @Composable ColumnScope.() -> Unit) {
20 3 Nelson Ramirez
        val bg = Brush.verticalGradient(
21 3 Nelson Ramirez
            colors = listOf(
22 3 Nelson Ramirez
                Color(0xFFEFF6FF),
23 3 Nelson Ramirez
                Color(0xFFFFFFFF)
24 3 Nelson Ramirez
            )
25 3 Nelson Ramirez
        )
26 3 Nelson Ramirez
        Column(
27 3 Nelson Ramirez
            modifier = Modifier
28 3 Nelson Ramirez
                .fillMaxSize()
29 3 Nelson Ramirez
                .background(bg)
30 3 Nelson Ramirez
                .padding(24.dp),
31 3 Nelson Ramirez
            horizontalAlignment = Alignment.CenterHorizontally,
32 3 Nelson Ramirez
            verticalArrangement = Arrangement.Center,
33 3 Nelson Ramirez
            content = content
34 3 Nelson Ramirez
        )
35 3 Nelson Ramirez
    }
36 3 Nelson Ramirez
37 3 Nelson Ramirez
    @Composable
38 3 Nelson Ramirez
    fun PrimaryPillButton(text: String, onClick: () -> Unit) {
39 3 Nelson Ramirez
        Button(
40 3 Nelson Ramirez
            onClick = onClick,
41 3 Nelson Ramirez
            modifier = Modifier
42 3 Nelson Ramirez
                .fillMaxWidth()
43 3 Nelson Ramirez
                .height(46.dp),
44 3 Nelson Ramirez
            shape = RoundedCornerShape(999.dp)
45 3 Nelson Ramirez
        ) {
46 3 Nelson Ramirez
            Text(text = text, fontWeight = FontWeight.SemiBold)
47 3 Nelson Ramirez
        }
48 3 Nelson Ramirez
    }
49 3 Nelson Ramirez
50 3 Nelson Ramirez
    @Composable
51 3 Nelson Ramirez
    fun OutlinePillButton(text: String, onClick: () -> Unit) {
52 3 Nelson Ramirez
        OutlinedButton(
53 3 Nelson Ramirez
            onClick = onClick,
54 3 Nelson Ramirez
            modifier = Modifier
55 3 Nelson Ramirez
                .fillMaxWidth()
56 3 Nelson Ramirez
                .height(46.dp),
57 3 Nelson Ramirez
            shape = RoundedCornerShape(999.dp)
58 3 Nelson Ramirez
        ) {
59 3 Nelson Ramirez
            Text(text = text, fontWeight = FontWeight.SemiBold)
60 3 Nelson Ramirez
        }
61 3 Nelson Ramirez
    }
62 3 Nelson Ramirez
</pre>
63 3 Nelson Ramirez
Ventana principal
64 3 Nelson Ramirez
<pre>
65 3 Nelson Ramirez
@Composable
66 3 Nelson Ramirez
    fun HomeScreen(
67 3 Nelson Ramirez
        onGoContainers: () -> Unit,
68 3 Nelson Ramirez
        onGoNotifications: () -> Unit,
69 3 Nelson Ramirez
        onGoLogin: () -> Unit
70 3 Nelson Ramirez
    ) {
71 3 Nelson Ramirez
        GradientBackground {
72 3 Nelson Ramirez
            // “Logo” simple estilo BR (placeholder)
73 3 Nelson Ramirez
            Image(
74 3 Nelson Ramirez
                painter = painterResource(id = R.drawable.logo_binraiders),
75 3 Nelson Ramirez
                contentDescription = "Logo Bin Raiders",
76 3 Nelson Ramirez
                modifier = Modifier
77 3 Nelson Ramirez
                    .size(300.dp)
78 3 Nelson Ramirez
                    .clip(CircleShape),
79 3 Nelson Ramirez
                contentScale = ContentScale.Crop
80 3 Nelson Ramirez
            )
81 3 Nelson Ramirez
82 3 Nelson Ramirez
83 3 Nelson Ramirez
            Spacer(Modifier.height(18.dp))
84 3 Nelson Ramirez
85 3 Nelson Ramirez
            Text("¡Bienvenidos a\nBin Raiders!", fontSize = 28.sp, fontWeight = FontWeight.Bold)
86 3 Nelson Ramirez
            Spacer(Modifier.height(28.dp))
87 3 Nelson Ramirez
88 3 Nelson Ramirez
            PrimaryPillButton("Ver contenedores", onGoContainers)
89 3 Nelson Ramirez
            Spacer(Modifier.height(12.dp))
90 3 Nelson Ramirez
            OutlinePillButton("Notificaciones", onGoNotifications)
91 3 Nelson Ramirez
            Spacer(Modifier.height(12.dp))
92 3 Nelson Ramirez
            PrimaryPillButton("Acceder", onGoLogin)
93 3 Nelson Ramirez
94 3 Nelson Ramirez
            Spacer(Modifier.height(18.dp))
95 3 Nelson Ramirez
            Text("Powered by Raspberry Pi 4", color = Color(0xFF6B7280), fontSize = 13.sp)
96 3 Nelson Ramirez
        }
97 3 Nelson Ramirez
    }
98 3 Nelson Ramirez
</pre>
99 3 Nelson Ramirez
100 3 Nelson Ramirez
Contenedores
101 3 Nelson Ramirez
<pre>
102 3 Nelson Ramirez
data class ContainerItem(
103 3 Nelson Ramirez
        val id: Int,
104 3 Nelson Ramirez
        val name: String,
105 3 Nelson Ramirez
        val address: String,
106 3 Nelson Ramirez
        val imageHint: String = ""
107 3 Nelson Ramirez
    )
108 3 Nelson Ramirez
109 3 Nelson Ramirez
</pre>
110 3 Nelson Ramirez
111 3 Nelson Ramirez
112 3 Nelson Ramirez
Ventana Contenedores
113 3 Nelson Ramirez
<pre>
114 3 Nelson Ramirez
@Composable
115 3 Nelson Ramirez
    fun ContainersScreen(
116 3 Nelson Ramirez
        onBack: () -> Unit,
117 3 Nelson Ramirez
        onOpenCamera: (ContainerItem) -> Unit
118 3 Nelson Ramirez
    ) {
119 3 Nelson Ramirez
        val containers = remember {
120 3 Nelson Ramirez
            listOf(
121 3 Nelson Ramirez
                ContainerItem(1, "Contenedor 1", "Dirección: Av. Diego Portales & Las Torres"),
122 3 Nelson Ramirez
                ContainerItem(2, "Contenedor 2", "Dirección: Av. Edmundo Flores 112"),
123 3 Nelson Ramirez
                ContainerItem(3, "Contenedor 3", "Dirección: Av. Santa María 421")
124 3 Nelson Ramirez
            )
125 3 Nelson Ramirez
        }
126 3 Nelson Ramirez
127 3 Nelson Ramirez
        var estado by remember { mutableStateOf<EstadoResponse?>(null) }
128 3 Nelson Ramirez
        var loading by remember { mutableStateOf(false) }
129 3 Nelson Ramirez
        val scope = rememberCoroutineScope()
130 3 Nelson Ramirez
131 3 Nelson Ramirez
        suspend fun refreshEstado() {
132 3 Nelson Ramirez
            loading = true
133 3 Nelson Ramirez
            val res = withContext(Dispatchers.IO) { ApiClient.getEstado() }
134 3 Nelson Ramirez
            estado = res
135 3 Nelson Ramirez
            loading = false
136 3 Nelson Ramirez
        }
137 3 Nelson Ramirez
138 3 Nelson Ramirez
        // 1) Primera carga
139 3 Nelson Ramirez
        LaunchedEffect(Unit) {
140 3 Nelson Ramirez
            refreshEstado()
141 3 Nelson Ramirez
        }
142 3 Nelson Ramirez
143 3 Nelson Ramirez
        // 2) Auto-refresh para demo (cada 2.5s)
144 3 Nelson Ramirez
        LaunchedEffect(Unit) {
145 3 Nelson Ramirez
            while (true) {
146 3 Nelson Ramirez
                delay(2500)
147 3 Nelson Ramirez
                refreshEstado()
148 3 Nelson Ramirez
            }
149 3 Nelson Ramirez
        }
150 3 Nelson Ramirez
151 3 Nelson Ramirez
        Scaffold(
152 3 Nelson Ramirez
            topBar = { SimpleTopBar(title = "Contenedores", onBack = onBack) }
153 3 Nelson Ramirez
        ) { pad ->
154 3 Nelson Ramirez
            Column(
155 3 Nelson Ramirez
                modifier = Modifier
156 3 Nelson Ramirez
                    .padding(pad)
157 3 Nelson Ramirez
                    .fillMaxSize()
158 3 Nelson Ramirez
                    .padding(16.dp),
159 3 Nelson Ramirez
                verticalArrangement = Arrangement.spacedBy(14.dp)
160 3 Nelson Ramirez
            ) {
161 3 Nelson Ramirez
                EstadoSensorCard(
162 3 Nelson Ramirez
                    estado = estado,
163 3 Nelson Ramirez
                    loading = loading,
164 3 Nelson Ramirez
                    onRefresh = { scope.launch { refreshEstado() } }
165 3 Nelson Ramirez
                )
166 3 Nelson Ramirez
167 3 Nelson Ramirez
                LazyColumn(
168 3 Nelson Ramirez
                    modifier = Modifier.fillMaxSize(),
169 3 Nelson Ramirez
                    verticalArrangement = Arrangement.spacedBy(12.dp)
170 3 Nelson Ramirez
                ) {
171 3 Nelson Ramirez
                    items(containers) { c ->
172 3 Nelson Ramirez
                        ContainerCard(item = c, onOpenCamera = { onOpenCamera(c) })
173 3 Nelson Ramirez
                    }
174 3 Nelson Ramirez
                }
175 3 Nelson Ramirez
            }
176 3 Nelson Ramirez
        }
177 3 Nelson Ramirez
    }
178 3 Nelson Ramirez
179 3 Nelson Ramirez
180 3 Nelson Ramirez
    @Composable
181 3 Nelson Ramirez
    fun ContainerCard(
182 3 Nelson Ramirez
        item: ContainerItem,
183 3 Nelson Ramirez
        onOpenCamera: () -> Unit
184 3 Nelson Ramirez
    ) {
185 3 Nelson Ramirez
        var enabled by remember { mutableStateOf(true) }
186 3 Nelson Ramirez
187 3 Nelson Ramirez
        Card(
188 3 Nelson Ramirez
            modifier = Modifier.fillMaxWidth(),
189 3 Nelson Ramirez
            shape = RoundedCornerShape(16.dp)
190 3 Nelson Ramirez
        ) {
191 3 Nelson Ramirez
            Row(
192 3 Nelson Ramirez
                modifier = Modifier
193 3 Nelson Ramirez
                    .fillMaxWidth()
194 3 Nelson Ramirez
                    .padding(14.dp),
195 3 Nelson Ramirez
                verticalAlignment = Alignment.CenterVertically
196 3 Nelson Ramirez
            ) {
197 3 Nelson Ramirez
                // Mini imagen placeholder
198 3 Nelson Ramirez
                Box(
199 3 Nelson Ramirez
                    modifier = Modifier
200 3 Nelson Ramirez
                        .size(68.dp)
201 3 Nelson Ramirez
                        .clip(RoundedCornerShape(12.dp))
202 3 Nelson Ramirez
                        .background(Color(0xFFE5E7EB))
203 3 Nelson Ramirez
                        .border(1.dp, Color(0xFFD1D5DB), RoundedCornerShape(12.dp)),
204 3 Nelson Ramirez
                    contentAlignment = Alignment.Center
205 3 Nelson Ramirez
                ) {
206 3 Nelson Ramirez
                    Text("IMG", color = Color(0xFF6B7280), fontSize = 12.sp)
207 3 Nelson Ramirez
                }
208 3 Nelson Ramirez
209 3 Nelson Ramirez
                Spacer(Modifier.width(12.dp))
210 3 Nelson Ramirez
211 3 Nelson Ramirez
                Column(modifier = Modifier.weight(1f)) {
212 3 Nelson Ramirez
                    Text(item.name, fontWeight = FontWeight.Bold)
213 3 Nelson Ramirez
                    Spacer(Modifier.height(2.dp))
214 3 Nelson Ramirez
                    Text(item.address, color = Color(0xFF6B7280), fontSize = 13.sp)
215 3 Nelson Ramirez
216 3 Nelson Ramirez
                    Spacer(Modifier.height(10.dp))
217 3 Nelson Ramirez
218 3 Nelson Ramirez
                    Row(verticalAlignment = Alignment.CenterVertically) {
219 3 Nelson Ramirez
                        Button(
220 3 Nelson Ramirez
                            onClick = onOpenCamera,
221 3 Nelson Ramirez
                            shape = RoundedCornerShape(10.dp),
222 3 Nelson Ramirez
                            contentPadding = PaddingValues(horizontal = 14.dp, vertical = 10.dp)
223 3 Nelson Ramirez
                        ) {
224 3 Nelson Ramirez
                            Text("Ver cámara")
225 3 Nelson Ramirez
                        }
226 3 Nelson Ramirez
                        Spacer(Modifier.width(10.dp))
227 3 Nelson Ramirez
                        Switch(checked = enabled, onCheckedChange = { enabled = it })
228 3 Nelson Ramirez
                    }
229 3 Nelson Ramirez
                }
230 3 Nelson Ramirez
            }
231 3 Nelson Ramirez
        }
232 3 Nelson Ramirez
    }
233 3 Nelson Ramirez
</pre>
234 3 Nelson Ramirez
235 3 Nelson Ramirez
Ventana camara
236 3 Nelson Ramirez
<pre>
237 3 Nelson Ramirez
@Composable
238 3 Nelson Ramirez
    fun CameraScreen(
239 3 Nelson Ramirez
        container: ContainerItem?,
240 3 Nelson Ramirez
        onBack: () -> Unit
241 3 Nelson Ramirez
    ) {
242 3 Nelson Ramirez
        val streamUrl = ApiConfig.STREAM_URL  
243 3 Nelson Ramirez
244 3 Nelson Ramirez
        Scaffold(
245 3 Nelson Ramirez
            topBar = { SimpleTopBar(title = "Cámara", onBack = onBack) }
246 3 Nelson Ramirez
        ) { pad ->
247 3 Nelson Ramirez
            Column(
248 3 Nelson Ramirez
                modifier = Modifier
249 3 Nelson Ramirez
                    .padding(pad)
250 3 Nelson Ramirez
                    .fillMaxSize()
251 3 Nelson Ramirez
                    .padding(16.dp),
252 3 Nelson Ramirez
                verticalArrangement = Arrangement.spacedBy(12.dp)
253 3 Nelson Ramirez
            ) {
254 3 Nelson Ramirez
                Text(
255 3 Nelson Ramirez
                    text = container?.name ?: "Contenedor",
256 3 Nelson Ramirez
                    fontWeight = FontWeight.Bold,
257 3 Nelson Ramirez
                    fontSize = 18.sp
258 3 Nelson Ramirez
                )
259 3 Nelson Ramirez
                Text(
260 3 Nelson Ramirez
                    text = container?.address ?: "",
261 3 Nelson Ramirez
                    color = Color(0xFF6B7280)
262 3 Nelson Ramirez
                )
263 3 Nelson Ramirez
264 3 Nelson Ramirez
                MjpegWebView(url = streamUrl)
265 3 Nelson Ramirez
266 3 Nelson Ramirez
                Text(
267 3 Nelson Ramirez
                    text = "Fuente: $streamUrl",
268 3 Nelson Ramirez
                    fontSize = 12.sp,
269 3 Nelson Ramirez
                    color = Color(0xFF6B7280)
270 3 Nelson Ramirez
                )
271 3 Nelson Ramirez
            }
272 3 Nelson Ramirez
        }
273 3 Nelson Ramirez
    }
274 3 Nelson Ramirez
275 3 Nelson Ramirez
    @Composable
276 3 Nelson Ramirez
    fun MjpegWebView(url: String) {
277 3 Nelson Ramirez
        AndroidView(
278 3 Nelson Ramirez
            modifier = Modifier
279 3 Nelson Ramirez
                .fillMaxWidth()
280 3 Nelson Ramirez
                .height(420.dp)
281 3 Nelson Ramirez
                .clip(RoundedCornerShape(16.dp))
282 3 Nelson Ramirez
                .border(1.dp, Color(0xFFE5E7EB), RoundedCornerShape(16.dp)),
283 3 Nelson Ramirez
            factory = { ctx ->
284 3 Nelson Ramirez
                WebView(ctx).apply {
285 3 Nelson Ramirez
                    webViewClient = WebViewClient()
286 3 Nelson Ramirez
                    settings.javaScriptEnabled = false
287 3 Nelson Ramirez
                    settings.loadWithOverviewMode = true
288 3 Nelson Ramirez
                    settings.useWideViewPort = true
289 3 Nelson Ramirez
                    loadUrl(url)
290 3 Nelson Ramirez
                }
291 3 Nelson Ramirez
            },
292 3 Nelson Ramirez
            update = { webView ->
293 3 Nelson Ramirez
                if (webView.url != url) webView.loadUrl(url)
294 3 Nelson Ramirez
            }
295 3 Nelson Ramirez
        )
296 3 Nelson Ramirez
    }
297 3 Nelson Ramirez
298 3 Nelson Ramirez
299 3 Nelson Ramirez
300 3 Nelson Ramirez
301 3 Nelson Ramirez
    @Composable
302 3 Nelson Ramirez
    fun EstadoSensorCard(
303 3 Nelson Ramirez
        estado: EstadoResponse?,
304 3 Nelson Ramirez
        loading: Boolean,
305 3 Nelson Ramirez
        onRefresh: () -> Unit
306 3 Nelson Ramirez
    ) {
307 3 Nelson Ramirez
        val pct = estado?.fill_percent ?: 0
308 3 Nelson Ramirez
        val label = when {
309 3 Nelson Ramirez
            loading -> "Cargando..."
310 3 Nelson Ramirez
            estado == null -> "Sin datos"
311 3 Nelson Ramirez
            estado.ok -> "Estado: ${estado.estado}"
312 3 Nelson Ramirez
            else -> "Error: ${estado.error ?: "sensor_no_data"}"
313 3 Nelson Ramirez
        }
314 3 Nelson Ramirez
315 3 Nelson Ramirez
        val color = when (estado?.estado) {
316 3 Nelson Ramirez
            "LLENO" -> Color(0xFFEF4444) // rojo
317 3 Nelson Ramirez
            "MEDIO" -> Color(0xFFF59E0B) // amarillo
318 3 Nelson Ramirez
            "VACIO" -> Color(0xFF22C55E) // verde
319 3 Nelson Ramirez
            else -> Color(0xFF6B7280)    // gris
320 3 Nelson Ramirez
        }
321 3 Nelson Ramirez
322 3 Nelson Ramirez
        Card(shape = RoundedCornerShape(16.dp), modifier = Modifier.fillMaxWidth()) {
323 3 Nelson Ramirez
            Column(Modifier.padding(14.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
324 3 Nelson Ramirez
                Row(verticalAlignment = Alignment.CenterVertically) {
325 3 Nelson Ramirez
                    Text("Estado del contenedor", fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f))
326 3 Nelson Ramirez
                    AssistChip(
327 3 Nelson Ramirez
                        onClick = {},
328 3 Nelson Ramirez
                        label = { Text(estado?.estado ?: "—") },
329 3 Nelson Ramirez
                        colors = AssistChipDefaults.assistChipColors(
330 3 Nelson Ramirez
                            labelColor = Color.White,
331 3 Nelson Ramirez
                            containerColor = color
332 3 Nelson Ramirez
                        )
333 3 Nelson Ramirez
                    )
334 3 Nelson Ramirez
                }
335 3 Nelson Ramirez
336 3 Nelson Ramirez
                Text(label, color = Color(0xFF374151))
337 3 Nelson Ramirez
338 3 Nelson Ramirez
                if (estado?.ok == true) {
339 3 Nelson Ramirez
                    Text("Distancia: ${estado.distance_cm} cm")
340 3 Nelson Ramirez
                    Text("Relleno: ${estado.fill_percent}%")
341 3 Nelson Ramirez
                    LinearProgressIndicator(
342 3 Nelson Ramirez
                        progress = (pct.coerceIn(0, 100) / 100f),
343 3 Nelson Ramirez
                        modifier = Modifier.fillMaxWidth()
344 3 Nelson Ramirez
                    )
345 3 Nelson Ramirez
                    Text(
346 3 Nelson Ramirez
                        "Edad dato: ${estado.sensor_age_ms ?: 0} ms",
347 3 Nelson Ramirez
                        fontSize = 12.sp,
348 3 Nelson Ramirez
                        color = Color(0xFF6B7280)
349 3 Nelson Ramirez
                    )
350 3 Nelson Ramirez
                }
351 3 Nelson Ramirez
352 3 Nelson Ramirez
                Button(
353 3 Nelson Ramirez
                    onClick = onRefresh,
354 3 Nelson Ramirez
                    enabled = !loading,
355 3 Nelson Ramirez
                    shape = RoundedCornerShape(12.dp),
356 3 Nelson Ramirez
                    modifier = Modifier.fillMaxWidth()
357 3 Nelson Ramirez
                ) {
358 3 Nelson Ramirez
                    Text(if (loading) "Actualizando..." else "Actualizar")
359 3 Nelson Ramirez
                }
360 3 Nelson Ramirez
            }
361 3 Nelson Ramirez
        }
362 3 Nelson Ramirez
    }
363 3 Nelson Ramirez
</pre>
364 2 Nelson Ramirez
Interfaz grafica: