tawasuyu

sergio/humanoid

HTML 44Markdown 1TOML 10 Definiciones

⎇ 8ab12b6800f7b28dfd3aa1a0ee97dff7b1ca41b7 launcher/src/main/java/com/rizoma/orbita/ui/OrbitApp.kt 234 líneas · 8.7 KB Historia crudo

Órbita: alejar + fondo dinámico · Trazo: aprendizaje con umbral y bilingüe 8ab12b680 · sergio ·

1package com.rizoma.orbita.ui
2
3import androidx.activity.compose.BackHandler
4import androidx.compose.foundation.Image
5import androidx.compose.foundation.background
6import androidx.compose.foundation.clickable
7import androidx.compose.foundation.interaction.MutableInteractionSource
8import androidx.compose.foundation.layout.Box
9import androidx.compose.foundation.layout.Column
10import androidx.compose.foundation.layout.Row
11import androidx.compose.foundation.layout.Spacer
12import androidx.compose.foundation.layout.fillMaxSize
13import androidx.compose.foundation.layout.fillMaxWidth
14import androidx.compose.foundation.layout.height
15import androidx.compose.foundation.layout.padding
16import androidx.compose.foundation.layout.size
17import androidx.compose.foundation.layout.width
18import androidx.compose.foundation.lazy.LazyColumn
19import androidx.compose.foundation.lazy.items
20import androidx.compose.foundation.shape.CircleShape
21import androidx.compose.foundation.shape.RoundedCornerShape
22import androidx.compose.foundation.text.BasicTextField
23import androidx.compose.material3.Surface
24import androidx.compose.material3.Text
25import androidx.compose.runtime.Composable
26import androidx.compose.runtime.getValue
27import androidx.compose.runtime.mutableStateOf
28import androidx.compose.runtime.remember
29import androidx.compose.runtime.setValue
30import androidx.compose.runtime.LaunchedEffect
31import androidx.compose.ui.Alignment
32import androidx.compose.ui.Modifier
33import androidx.compose.ui.draw.clip
34import androidx.compose.ui.focus.FocusRequester
35import androidx.compose.ui.focus.focusRequester
36import androidx.compose.ui.graphics.SolidColor
37import androidx.compose.ui.res.stringResource
38import androidx.compose.ui.text.TextStyle
39import androidx.compose.ui.text.font.FontWeight
40import androidx.compose.ui.text.input.ImeAction
41import androidx.compose.ui.text.style.TextOverflow
42import androidx.compose.ui.unit.dp
43import androidx.compose.ui.unit.sp
44import androidx.compose.ui.window.Dialog
45import androidx.lifecycle.viewmodel.compose.viewModel
46import androidx.compose.runtime.collectAsState
47import com.rizoma.orbita.R
48import com.rizoma.orbita.model.AppNode
49import com.rizoma.orbita.ui.theme.NeonCian
50import com.rizoma.orbita.ui.theme.NeonRosa
51import com.rizoma.orbita.ui.theme.NeonVerde
52import com.rizoma.orbita.ui.theme.Superficie
53import com.rizoma.orbita.ui.theme.Tenue
54import com.rizoma.orbita.ui.theme.Vacio
55
56@Composable
57fun OrbitApp(vm: OrbitaViewModel = viewModel()) {
58    val nodes by vm.nodes.collectAsState()
59    var searchOpen by remember { mutableStateOf(false) }
60    var menuNode by remember { mutableStateOf<AppNode?>(null) }
61
62    // Como launcher: "atrás" cierra la búsqueda o no hace nada (nunca sale al sistema).
63    BackHandler(enabled = true) {
64        if (searchOpen) searchOpen = false
65    }
66
67    Box(Modifier.fillMaxSize().background(Vacio)) {
68        DynamicBackground()
69        OrbitLayout(
70            nodes = nodes,
71            onLaunch = vm::launch,
72            onLongPress = { menuNode = it },
73            onCenterTap = { searchOpen = true },
74        )
75
76        if (searchOpen) {
77            SearchOverlay(
78                nodes = nodes,
79                onLaunch = {
80                    vm.launch(it)
81                    searchOpen = false
82                },
83                onClose = { searchOpen = false },
84            )
85        }
86
87        menuNode?.let { node ->
88            NodeMenu(
89                node = node,
90                onPin = { vm.togglePin(node); menuNode = null },
91                onPush = { vm.pushAway(node); menuNode = null },
92                onInfo = { vm.openInfo(node); menuNode = null },
93                onUninstall = { vm.uninstall(node); menuNode = null },
94                onDismiss = { menuNode = null },
95            )
96        }
97    }
98}
99
100@Composable
101private fun SearchOverlay(
102    nodes: List<AppNode>,
103    onLaunch: (AppNode) -> Unit,
104    onClose: () -> Unit,
105) {
106    var query by remember { mutableStateOf("") }
107    val filtered = remember(query, nodes) {
108        if (query.isBlank()) nodes
109        else nodes.filter { it.label.contains(query.trim(), ignoreCase = true) }
110    }
111    val focus = remember { FocusRequester() }
112    LaunchedEffect(Unit) { focus.requestFocus() }
113
114    Box(
115        Modifier
116            .fillMaxSize()
117            .background(Vacio.copy(alpha = 0.97f))
118            // Tocar el fondo cierra; el contenido absorbe sus propios toques.
119            .clickable(
120                interactionSource = remember { MutableInteractionSource() },
121                indication = null,
122                onClick = onClose,
123            )
124    ) {
125        Column(
126            Modifier
127                .fillMaxSize()
128                .padding(horizontal = 28.dp)
129                .padding(top = 96.dp)
130        ) {
131            BasicTextField(
132                value = query,
133                onValueChange = { query = it },
134                singleLine = true,
135                textStyle = TextStyle(color = NeonVerde, fontSize = 26.sp, fontWeight = FontWeight.Light),
136                cursorBrush = SolidColor(NeonRosa),
137                keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(imeAction = ImeAction.Search),
138                modifier = Modifier.fillMaxWidth().focusRequester(focus),
139                decorationBox = { inner ->
140                    if (query.isEmpty()) {
141                        Text(
142                            text = stringResource(R.string.search_hint),
143                            color = Tenue,
144                            fontSize = 26.sp,
145                            fontWeight = FontWeight.Light,
146                        )
147                    }
148                    inner()
149                },
150            )
151            Spacer(Modifier.height(4.dp))
152            Box(Modifier.fillMaxWidth().height(1.dp).background(NeonVerde.copy(alpha = 0.4f)))
153            Spacer(Modifier.height(16.dp))
154
155            LazyColumn(Modifier.fillMaxSize()) {
156                items(filtered, key = { it.key }) { node ->
157                    Row(
158                        Modifier
159                            .fillMaxWidth()
160                            .clickable { onLaunch(node) }
161                            .padding(vertical = 8.dp),
162                        verticalAlignment = Alignment.CenterVertically,
163                    ) {
164                        Image(
165                            bitmap = node.icon,
166                            contentDescription = node.label,
167                            modifier = Modifier.size(40.dp).clip(CircleShape),
168                        )
169                        Spacer(Modifier.width(16.dp))
170                        Text(
171                            text = node.label,
172                            color = androidx.compose.ui.graphics.Color.White,
173                            fontSize = 16.sp,
174                            maxLines = 1,
175                            overflow = TextOverflow.Ellipsis,
176                        )
177                    }
178                }
179            }
180        }
181    }
182}
183
184@Composable
185private fun NodeMenu(
186    node: AppNode,
187    onPin: () -> Unit,
188    onPush: () -> Unit,
189    onInfo: () -> Unit,
190    onUninstall: () -> Unit,
191    onDismiss: () -> Unit,
192) {
193    Dialog(onDismissRequest = onDismiss) {
194        Surface(
195            color = Superficie,
196            shape = RoundedCornerShape(20.dp),
197        ) {
198            Column(Modifier.width(240.dp).padding(vertical = 10.dp)) {
199                Text(
200                    text = node.label,
201                    color = androidx.compose.ui.graphics.Color.White,
202                    fontSize = 15.sp,
203                    fontWeight = FontWeight.Medium,
204                    maxLines = 1,
205                    overflow = TextOverflow.Ellipsis,
206                    modifier = Modifier.padding(horizontal = 20.dp, vertical = 10.dp),
207                )
208                MenuRow(
209                    label = stringResource(if (node.pinned) R.string.menu_unpin else R.string.menu_pin),
210                    accent = NeonRosa,
211                    onClick = onPin,
212                )
213                MenuRow(label = stringResource(R.string.menu_push), accent = NeonVerde, onClick = onPush)
214                MenuRow(label = stringResource(R.string.menu_info), accent = NeonCian, onClick = onInfo)
215                MenuRow(label = stringResource(R.string.menu_uninstall), accent = NeonRosa, onClick = onUninstall)
216            }
217        }
218    }
219}
220
221@Composable
222private fun MenuRow(label: String, accent: androidx.compose.ui.graphics.Color, onClick: () -> Unit) {
223    Row(
224        Modifier
225            .fillMaxWidth()
226            .clickable(onClick = onClick)
227            .padding(horizontal = 20.dp, vertical = 14.dp),
228        verticalAlignment = Alignment.CenterVertically,
229    ) {
230        Box(Modifier.size(6.dp).clip(CircleShape).background(accent))
231        Spacer(Modifier.width(14.dp))
232        Text(text = label, color = androidx.compose.ui.graphics.Color.White, fontSize = 15.sp)
233    }
234}