tawasuyu

sergio/humanoid

HTML 38Markdown 1TOML 10 Definiciones

⎇ 3feee86e320e76ed9ddd32f09ed18d20260c4e2a launcher/src/main/java/com/rizoma/orbita/ui/OrbitApp.kt 230 líneas · 8.5 KB Historia crudo

Órbita: launcher orbital anti-rejilla (v0.1) 3feee86e3 · 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        OrbitLayout(
69            nodes = nodes,
70            onLaunch = vm::launch,
71            onLongPress = { menuNode = it },
72            onCenterTap = { searchOpen = true },
73        )
74
75        if (searchOpen) {
76            SearchOverlay(
77                nodes = nodes,
78                onLaunch = {
79                    vm.launch(it)
80                    searchOpen = false
81                },
82                onClose = { searchOpen = false },
83            )
84        }
85
86        menuNode?.let { node ->
87            NodeMenu(
88                node = node,
89                onPin = { vm.togglePin(node); menuNode = null },
90                onInfo = { vm.openInfo(node); menuNode = null },
91                onUninstall = { vm.uninstall(node); menuNode = null },
92                onDismiss = { menuNode = null },
93            )
94        }
95    }
96}
97
98@Composable
99private fun SearchOverlay(
100    nodes: List<AppNode>,
101    onLaunch: (AppNode) -> Unit,
102    onClose: () -> Unit,
103) {
104    var query by remember { mutableStateOf("") }
105    val filtered = remember(query, nodes) {
106        if (query.isBlank()) nodes
107        else nodes.filter { it.label.contains(query.trim(), ignoreCase = true) }
108    }
109    val focus = remember { FocusRequester() }
110    LaunchedEffect(Unit) { focus.requestFocus() }
111
112    Box(
113        Modifier
114            .fillMaxSize()
115            .background(Vacio.copy(alpha = 0.97f))
116            // Tocar el fondo cierra; el contenido absorbe sus propios toques.
117            .clickable(
118                interactionSource = remember { MutableInteractionSource() },
119                indication = null,
120                onClick = onClose,
121            )
122    ) {
123        Column(
124            Modifier
125                .fillMaxSize()
126                .padding(horizontal = 28.dp)
127                .padding(top = 96.dp)
128        ) {
129            BasicTextField(
130                value = query,
131                onValueChange = { query = it },
132                singleLine = true,
133                textStyle = TextStyle(color = NeonVerde, fontSize = 26.sp, fontWeight = FontWeight.Light),
134                cursorBrush = SolidColor(NeonRosa),
135                keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(imeAction = ImeAction.Search),
136                modifier = Modifier.fillMaxWidth().focusRequester(focus),
137                decorationBox = { inner ->
138                    if (query.isEmpty()) {
139                        Text(
140                            text = stringResource(R.string.search_hint),
141                            color = Tenue,
142                            fontSize = 26.sp,
143                            fontWeight = FontWeight.Light,
144                        )
145                    }
146                    inner()
147                },
148            )
149            Spacer(Modifier.height(4.dp))
150            Box(Modifier.fillMaxWidth().height(1.dp).background(NeonVerde.copy(alpha = 0.4f)))
151            Spacer(Modifier.height(16.dp))
152
153            LazyColumn(Modifier.fillMaxSize()) {
154                items(filtered, key = { it.key }) { node ->
155                    Row(
156                        Modifier
157                            .fillMaxWidth()
158                            .clickable { onLaunch(node) }
159                            .padding(vertical = 8.dp),
160                        verticalAlignment = Alignment.CenterVertically,
161                    ) {
162                        Image(
163                            bitmap = node.icon,
164                            contentDescription = node.label,
165                            modifier = Modifier.size(40.dp).clip(CircleShape),
166                        )
167                        Spacer(Modifier.width(16.dp))
168                        Text(
169                            text = node.label,
170                            color = androidx.compose.ui.graphics.Color.White,
171                            fontSize = 16.sp,
172                            maxLines = 1,
173                            overflow = TextOverflow.Ellipsis,
174                        )
175                    }
176                }
177            }
178        }
179    }
180}
181
182@Composable
183private fun NodeMenu(
184    node: AppNode,
185    onPin: () -> Unit,
186    onInfo: () -> Unit,
187    onUninstall: () -> Unit,
188    onDismiss: () -> Unit,
189) {
190    Dialog(onDismissRequest = onDismiss) {
191        Surface(
192            color = Superficie,
193            shape = RoundedCornerShape(20.dp),
194        ) {
195            Column(Modifier.width(240.dp).padding(vertical = 10.dp)) {
196                Text(
197                    text = node.label,
198                    color = androidx.compose.ui.graphics.Color.White,
199                    fontSize = 15.sp,
200                    fontWeight = FontWeight.Medium,
201                    maxLines = 1,
202                    overflow = TextOverflow.Ellipsis,
203                    modifier = Modifier.padding(horizontal = 20.dp, vertical = 10.dp),
204                )
205                MenuRow(
206                    label = stringResource(if (node.pinned) R.string.menu_unpin else R.string.menu_pin),
207                    accent = NeonRosa,
208                    onClick = onPin,
209                )
210                MenuRow(label = stringResource(R.string.menu_info), accent = NeonCian, onClick = onInfo)
211                MenuRow(label = stringResource(R.string.menu_uninstall), accent = NeonRosa, onClick = onUninstall)
212            }
213        }
214    }
215}
216
217@Composable
218private fun MenuRow(label: String, accent: androidx.compose.ui.graphics.Color, onClick: () -> Unit) {
219    Row(
220        Modifier
221            .fillMaxWidth()
222            .clickable(onClick = onClick)
223            .padding(horizontal = 20.dp, vertical = 14.dp),
224        verticalAlignment = Alignment.CenterVertically,
225    ) {
226        Box(Modifier.size(6.dp).clip(CircleShape).background(accent))
227        Spacer(Modifier.width(14.dp))
228        Text(text = label, color = androidx.compose.ui.graphics.Color.White, fontSize = 15.sp)
229    }
230}