Órbita: launcher orbital anti-rejilla (v0.1)
Primera versión funcional del launcher del rizoma: - Carga de apps lanzables vía PackageManager con iconos rasterizados. - Disposición orbital: nodos en órbitas concéntricas según su calor (uso); fijados y más usados caen a órbitas internas. Anillos contrarrotantes. - Toque para lanzar, mantener para menú (fijar/soltar, info, quitar). - Núcleo central pulsante → búsqueda en vivo sobre todo el rizoma. - Persistencia de uso y fijados con DataStore; refresco ante instalar/desinstalar. Build: añade material3 y lifecycle-viewmodel-compose; :keyboard pospuesto. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+13
@@ -0,0 +1,13 @@
|
|||||||
|
*.iml
|
||||||
|
.gradle/
|
||||||
|
/local.properties
|
||||||
|
.idea/
|
||||||
|
.DS_Store
|
||||||
|
build/
|
||||||
|
captures/
|
||||||
|
.externalNativeBuild/
|
||||||
|
.cxx/
|
||||||
|
*.apk
|
||||||
|
*.aab
|
||||||
|
*.keystore
|
||||||
|
!debug.keystore
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.application) apply false
|
||||||
|
alias(libs.plugins.kotlin.android) apply false
|
||||||
|
alias(libs.plugins.kotlin.compose) apply false
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx3g -Dfile.encoding=UTF-8
|
||||||
|
org.gradle.java.home=/home/sergio/jdks/jdk-21.0.11+10
|
||||||
|
org.gradle.parallel=true
|
||||||
|
org.gradle.caching=true
|
||||||
|
android.useAndroidX=true
|
||||||
|
android.nonTransitiveRClass=true
|
||||||
|
kotlin.code.style=official
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
[versions]
|
||||||
|
agp = "8.11.1"
|
||||||
|
kotlin = "2.2.0"
|
||||||
|
coreKtx = "1.16.0"
|
||||||
|
lifecycle = "2.9.1"
|
||||||
|
activityCompose = "1.10.1"
|
||||||
|
composeBom = "2025.06.01"
|
||||||
|
datastore = "1.1.7"
|
||||||
|
coroutines = "1.10.2"
|
||||||
|
|
||||||
|
[libraries]
|
||||||
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
|
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
|
||||||
|
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" }
|
||||||
|
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
||||||
|
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
||||||
|
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||||
|
androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
|
||||||
|
androidx-compose-ui-util = { group = "androidx.compose.ui", name = "ui-util" }
|
||||||
|
androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" }
|
||||||
|
androidx-compose-animation = { group = "androidx.compose.animation", name = "animation" }
|
||||||
|
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||||
|
androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
|
||||||
|
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" }
|
||||||
|
|
||||||
|
[plugins]
|
||||||
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||||
|
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.application)
|
||||||
|
alias(libs.plugins.kotlin.android)
|
||||||
|
alias(libs.plugins.kotlin.compose)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.rizoma.orbita"
|
||||||
|
compileSdk = 36
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = "com.rizoma.orbita"
|
||||||
|
minSdk = 26
|
||||||
|
targetSdk = 36
|
||||||
|
versionCode = 1
|
||||||
|
versionName = "0.1.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
isMinifyEnabled = true
|
||||||
|
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "17"
|
||||||
|
}
|
||||||
|
buildFeatures {
|
||||||
|
compose = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.androidx.core.ktx)
|
||||||
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
|
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||||
|
implementation(libs.androidx.activity.compose)
|
||||||
|
implementation(platform(libs.androidx.compose.bom))
|
||||||
|
implementation(libs.androidx.compose.ui)
|
||||||
|
implementation(libs.androidx.compose.ui.graphics)
|
||||||
|
implementation(libs.androidx.compose.ui.util)
|
||||||
|
implementation(libs.androidx.compose.foundation)
|
||||||
|
implementation(libs.androidx.compose.animation)
|
||||||
|
implementation(libs.androidx.compose.material3)
|
||||||
|
implementation(libs.androidx.datastore.preferences)
|
||||||
|
implementation(libs.kotlinx.coroutines.android)
|
||||||
|
}
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
# Reglas específicas del launcher (vacío por ahora)
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<!-- Necesario para listar todas las apps lanzables en Android 11+ -->
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:theme="@style/Theme.Orbita"
|
||||||
|
android:supportsRtl="true">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:stateNotNeeded="true"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
|
android:configChanges="orientation|screenSize|keyboard|keyboardHidden|uiMode">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.HOME" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.rizoma.orbita
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import com.rizoma.orbita.ui.OrbitApp
|
||||||
|
import com.rizoma.orbita.ui.theme.OrbitaTheme
|
||||||
|
|
||||||
|
class MainActivity : ComponentActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
setContent {
|
||||||
|
OrbitaTheme {
|
||||||
|
OrbitApp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.rizoma.orbita.data
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
|
import com.rizoma.orbita.model.AppNode
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cosecha del sistema: todas las actividades lanzables se vuelven nodos del rizoma.
|
||||||
|
* El icono se rasteriza una sola vez aquí (operación cara → fuera del hilo principal).
|
||||||
|
*/
|
||||||
|
class AppRepository(context: Context) {
|
||||||
|
|
||||||
|
private val appContext = context.applicationContext
|
||||||
|
private val pm = appContext.packageManager
|
||||||
|
|
||||||
|
fun loadApps(): List<AppNode> {
|
||||||
|
val intent = Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER)
|
||||||
|
val self = appContext.packageName
|
||||||
|
return pm.queryIntentActivities(intent, 0)
|
||||||
|
.asSequence()
|
||||||
|
.filter { it.activityInfo.packageName != self }
|
||||||
|
.map { ri ->
|
||||||
|
val icon = ri.loadIcon(pm).toBitmap(ICON_PX, ICON_PX).asImageBitmap()
|
||||||
|
AppNode(
|
||||||
|
packageName = ri.activityInfo.packageName,
|
||||||
|
activityName = ri.activityInfo.name,
|
||||||
|
label = ri.loadLabel(pm).toString(),
|
||||||
|
icon = icon,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val ICON_PX = 144
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.rizoma.orbita.data
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import androidx.datastore.preferences.core.edit
|
||||||
|
import androidx.datastore.preferences.core.intPreferencesKey
|
||||||
|
import androidx.datastore.preferences.core.stringSetPreferencesKey
|
||||||
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
private val Context.orbitaStore: DataStore<Preferences> by preferencesDataStore(name = "orbita_usage")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Memoria orbital: cuántas veces se ha lanzado cada nodo (su calor) y cuáles están fijados.
|
||||||
|
* El calor define a qué órbita cae el nodo; lo fijado nunca se desprende del centro.
|
||||||
|
*/
|
||||||
|
class UsageStore(context: Context) {
|
||||||
|
|
||||||
|
private val store = context.applicationContext.orbitaStore
|
||||||
|
|
||||||
|
fun counts(): Flow<Map<String, Int>> = store.data.map { prefs ->
|
||||||
|
buildMap {
|
||||||
|
prefs.asMap().forEach { (key, value) ->
|
||||||
|
if (key.name.startsWith(COUNT_PREFIX) && value is Int) {
|
||||||
|
put(key.name.removePrefix(COUNT_PREFIX), value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pinned(): Flow<Set<String>> = store.data.map { it[PINNED] ?: emptySet() }
|
||||||
|
|
||||||
|
suspend fun record(nodeKey: String) {
|
||||||
|
store.edit { prefs ->
|
||||||
|
val k = intPreferencesKey(COUNT_PREFIX + nodeKey)
|
||||||
|
prefs[k] = (prefs[k] ?: 0) + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setPinned(nodeKey: String, pinned: Boolean) {
|
||||||
|
store.edit { prefs ->
|
||||||
|
val current = prefs[PINNED]?.toMutableSet() ?: mutableSetOf()
|
||||||
|
if (pinned) current.add(nodeKey) else current.remove(nodeKey)
|
||||||
|
prefs[PINNED] = current
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val COUNT_PREFIX = "c:"
|
||||||
|
val PINNED = stringSetPreferencesKey("pinned")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.rizoma.orbita.model
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Un nodo del rizoma: una app lanzable con su calor de uso.
|
||||||
|
* [score] es el peso orbital — más alto = órbita más interna.
|
||||||
|
*/
|
||||||
|
data class AppNode(
|
||||||
|
val packageName: String,
|
||||||
|
val activityName: String,
|
||||||
|
val label: String,
|
||||||
|
val icon: ImageBitmap,
|
||||||
|
val score: Float = 0f,
|
||||||
|
val pinned: Boolean = false,
|
||||||
|
) {
|
||||||
|
val key: String get() = "$packageName/$activityName"
|
||||||
|
}
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
package com.rizoma.orbita.ui
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
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.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import com.rizoma.orbita.R
|
||||||
|
import com.rizoma.orbita.model.AppNode
|
||||||
|
import com.rizoma.orbita.ui.theme.NeonCian
|
||||||
|
import com.rizoma.orbita.ui.theme.NeonRosa
|
||||||
|
import com.rizoma.orbita.ui.theme.NeonVerde
|
||||||
|
import com.rizoma.orbita.ui.theme.Superficie
|
||||||
|
import com.rizoma.orbita.ui.theme.Tenue
|
||||||
|
import com.rizoma.orbita.ui.theme.Vacio
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun OrbitApp(vm: OrbitaViewModel = viewModel()) {
|
||||||
|
val nodes by vm.nodes.collectAsState()
|
||||||
|
var searchOpen by remember { mutableStateOf(false) }
|
||||||
|
var menuNode by remember { mutableStateOf<AppNode?>(null) }
|
||||||
|
|
||||||
|
// Como launcher: "atrás" cierra la búsqueda o no hace nada (nunca sale al sistema).
|
||||||
|
BackHandler(enabled = true) {
|
||||||
|
if (searchOpen) searchOpen = false
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(Modifier.fillMaxSize().background(Vacio)) {
|
||||||
|
OrbitLayout(
|
||||||
|
nodes = nodes,
|
||||||
|
onLaunch = vm::launch,
|
||||||
|
onLongPress = { menuNode = it },
|
||||||
|
onCenterTap = { searchOpen = true },
|
||||||
|
)
|
||||||
|
|
||||||
|
if (searchOpen) {
|
||||||
|
SearchOverlay(
|
||||||
|
nodes = nodes,
|
||||||
|
onLaunch = {
|
||||||
|
vm.launch(it)
|
||||||
|
searchOpen = false
|
||||||
|
},
|
||||||
|
onClose = { searchOpen = false },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
menuNode?.let { node ->
|
||||||
|
NodeMenu(
|
||||||
|
node = node,
|
||||||
|
onPin = { vm.togglePin(node); menuNode = null },
|
||||||
|
onInfo = { vm.openInfo(node); menuNode = null },
|
||||||
|
onUninstall = { vm.uninstall(node); menuNode = null },
|
||||||
|
onDismiss = { menuNode = null },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SearchOverlay(
|
||||||
|
nodes: List<AppNode>,
|
||||||
|
onLaunch: (AppNode) -> Unit,
|
||||||
|
onClose: () -> Unit,
|
||||||
|
) {
|
||||||
|
var query by remember { mutableStateOf("") }
|
||||||
|
val filtered = remember(query, nodes) {
|
||||||
|
if (query.isBlank()) nodes
|
||||||
|
else nodes.filter { it.label.contains(query.trim(), ignoreCase = true) }
|
||||||
|
}
|
||||||
|
val focus = remember { FocusRequester() }
|
||||||
|
LaunchedEffect(Unit) { focus.requestFocus() }
|
||||||
|
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Vacio.copy(alpha = 0.97f))
|
||||||
|
// Tocar el fondo cierra; el contenido absorbe sus propios toques.
|
||||||
|
.clickable(
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = null,
|
||||||
|
onClick = onClose,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = 28.dp)
|
||||||
|
.padding(top = 96.dp)
|
||||||
|
) {
|
||||||
|
BasicTextField(
|
||||||
|
value = query,
|
||||||
|
onValueChange = { query = it },
|
||||||
|
singleLine = true,
|
||||||
|
textStyle = TextStyle(color = NeonVerde, fontSize = 26.sp, fontWeight = FontWeight.Light),
|
||||||
|
cursorBrush = SolidColor(NeonRosa),
|
||||||
|
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(imeAction = ImeAction.Search),
|
||||||
|
modifier = Modifier.fillMaxWidth().focusRequester(focus),
|
||||||
|
decorationBox = { inner ->
|
||||||
|
if (query.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.search_hint),
|
||||||
|
color = Tenue,
|
||||||
|
fontSize = 26.sp,
|
||||||
|
fontWeight = FontWeight.Light,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
inner()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
Box(Modifier.fillMaxWidth().height(1.dp).background(NeonVerde.copy(alpha = 0.4f)))
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
|
LazyColumn(Modifier.fillMaxSize()) {
|
||||||
|
items(filtered, key = { it.key }) { node ->
|
||||||
|
Row(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { onLaunch(node) }
|
||||||
|
.padding(vertical = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
bitmap = node.icon,
|
||||||
|
contentDescription = node.label,
|
||||||
|
modifier = Modifier.size(40.dp).clip(CircleShape),
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(16.dp))
|
||||||
|
Text(
|
||||||
|
text = node.label,
|
||||||
|
color = androidx.compose.ui.graphics.Color.White,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun NodeMenu(
|
||||||
|
node: AppNode,
|
||||||
|
onPin: () -> Unit,
|
||||||
|
onInfo: () -> Unit,
|
||||||
|
onUninstall: () -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
) {
|
||||||
|
Dialog(onDismissRequest = onDismiss) {
|
||||||
|
Surface(
|
||||||
|
color = Superficie,
|
||||||
|
shape = RoundedCornerShape(20.dp),
|
||||||
|
) {
|
||||||
|
Column(Modifier.width(240.dp).padding(vertical = 10.dp)) {
|
||||||
|
Text(
|
||||||
|
text = node.label,
|
||||||
|
color = androidx.compose.ui.graphics.Color.White,
|
||||||
|
fontSize = 15.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.padding(horizontal = 20.dp, vertical = 10.dp),
|
||||||
|
)
|
||||||
|
MenuRow(
|
||||||
|
label = stringResource(if (node.pinned) R.string.menu_unpin else R.string.menu_pin),
|
||||||
|
accent = NeonRosa,
|
||||||
|
onClick = onPin,
|
||||||
|
)
|
||||||
|
MenuRow(label = stringResource(R.string.menu_info), accent = NeonCian, onClick = onInfo)
|
||||||
|
MenuRow(label = stringResource(R.string.menu_uninstall), accent = NeonRosa, onClick = onUninstall)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MenuRow(label: String, accent: androidx.compose.ui.graphics.Color, onClick: () -> Unit) {
|
||||||
|
Row(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(horizontal = 20.dp, vertical = 14.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Box(Modifier.size(6.dp).clip(CircleShape).background(accent))
|
||||||
|
Spacer(Modifier.width(14.dp))
|
||||||
|
Text(text = label, color = androidx.compose.ui.graphics.Color.White, fontSize = 15.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
package com.rizoma.orbita.ui
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.LinearEasing
|
||||||
|
import androidx.compose.animation.core.RepeatMode
|
||||||
|
import androidx.compose.animation.core.animateFloat
|
||||||
|
import androidx.compose.animation.core.infiniteRepeatable
|
||||||
|
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.scale
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.rizoma.orbita.model.AppNode
|
||||||
|
import com.rizoma.orbita.ui.theme.NeonCian
|
||||||
|
import com.rizoma.orbita.ui.theme.NeonRosa
|
||||||
|
import com.rizoma.orbita.ui.theme.NeonVerde
|
||||||
|
import com.rizoma.orbita.ui.theme.Tenue
|
||||||
|
import kotlin.math.PI
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.floor
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
import kotlin.math.sin
|
||||||
|
|
||||||
|
private data class Placement(
|
||||||
|
val node: AppNode,
|
||||||
|
val ring: Int,
|
||||||
|
val angle: Double,
|
||||||
|
val radius: Float,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val ringColors = listOf(NeonVerde, NeonRosa, NeonCian)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Distribución anti-rejilla: los nodos caen en órbitas concéntricas según su calor.
|
||||||
|
* Las órbitas internas (más cerca del centro) son las apps más usadas y las fijadas.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun OrbitLayout(
|
||||||
|
nodes: List<AppNode>,
|
||||||
|
onLaunch: (AppNode) -> Unit,
|
||||||
|
onLongPress: (AppNode) -> Unit,
|
||||||
|
onCenterTap: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val transition = rememberInfiniteTransition(label = "orbita")
|
||||||
|
val spin by transition.animateFloat(
|
||||||
|
initialValue = 0f,
|
||||||
|
targetValue = (2 * PI).toFloat(),
|
||||||
|
animationSpec = infiniteRepeatable(tween(180_000, easing = LinearEasing), RepeatMode.Restart),
|
||||||
|
label = "spin",
|
||||||
|
)
|
||||||
|
|
||||||
|
BoxWithConstraints(modifier.fillMaxSize()) {
|
||||||
|
val wPx = with(density) { maxWidth.toPx() }
|
||||||
|
val hPx = with(density) { maxHeight.toPx() }
|
||||||
|
val cx = wPx / 2f
|
||||||
|
val cy = hPx / 2f
|
||||||
|
|
||||||
|
val firstR = with(density) { 116.dp.toPx() }
|
||||||
|
val ringGap = with(density) { 86.dp.toPx() }
|
||||||
|
val spacing = with(density) { 76.dp.toPx() }
|
||||||
|
val colHalf = with(density) { 32.dp.toPx() }
|
||||||
|
val iconHalf = with(density) { 27.dp.toPx() }
|
||||||
|
|
||||||
|
val placements = remember(nodes, wPx, hPx) {
|
||||||
|
computePlacements(nodes, firstR, ringGap, spacing)
|
||||||
|
}
|
||||||
|
val rings = (placements.maxOfOrNull { it.ring } ?: -1) + 1
|
||||||
|
|
||||||
|
// Halo ambiental + anillos orbitales.
|
||||||
|
Canvas(Modifier.fillMaxSize()) {
|
||||||
|
drawCircle(
|
||||||
|
brush = Brush.radialGradient(
|
||||||
|
colors = listOf(NeonVerde.copy(alpha = 0.06f), androidx.compose.ui.graphics.Color.Transparent),
|
||||||
|
center = Offset(cx, cy),
|
||||||
|
radius = maxOf(wPx, hPx) * 0.6f,
|
||||||
|
),
|
||||||
|
radius = maxOf(wPx, hPx) * 0.6f,
|
||||||
|
center = Offset(cx, cy),
|
||||||
|
)
|
||||||
|
for (r in 0 until rings) {
|
||||||
|
drawCircle(
|
||||||
|
color = ringColors[r % ringColors.size].copy(alpha = 0.12f),
|
||||||
|
radius = firstR + r * ringGap,
|
||||||
|
center = Offset(cx, cy),
|
||||||
|
style = Stroke(width = 1.2f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Núcleo: toque para buscar en el rizoma.
|
||||||
|
CenterHub(
|
||||||
|
modifier = Modifier.align(Alignment.Center),
|
||||||
|
onTap = onCenterTap,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Nodos en órbita.
|
||||||
|
placements.forEach { p ->
|
||||||
|
// Anillos contiguos contrarrotan. Factor entero (±1) → el bucle es continuo.
|
||||||
|
val dir = if (p.ring % 2 == 0) 1f else -1f
|
||||||
|
val angle = p.angle + spin * dir
|
||||||
|
val x = cx + p.radius * cos(angle).toFloat() - colHalf
|
||||||
|
val y = cy + p.radius * sin(angle).toFloat() - iconHalf
|
||||||
|
NodeItem(
|
||||||
|
node = p.node,
|
||||||
|
modifier = Modifier.offset { IntOffset(x.roundToInt(), y.roundToInt()) },
|
||||||
|
onLaunch = onLaunch,
|
||||||
|
onLongPress = onLongPress,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun NodeItem(
|
||||||
|
node: AppNode,
|
||||||
|
modifier: Modifier,
|
||||||
|
onLaunch: (AppNode) -> Unit,
|
||||||
|
onLongPress: (AppNode) -> Unit,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier.width(64.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
val iconMod = Modifier
|
||||||
|
.size(54.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = { onLaunch(node) },
|
||||||
|
onLongClick = { onLongPress(node) },
|
||||||
|
)
|
||||||
|
.then(
|
||||||
|
if (node.pinned) Modifier.border(1.5.dp, NeonRosa, CircleShape) else Modifier
|
||||||
|
)
|
||||||
|
Image(
|
||||||
|
bitmap = node.icon,
|
||||||
|
contentDescription = node.label,
|
||||||
|
modifier = iconMod,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = node.label,
|
||||||
|
color = Tenue,
|
||||||
|
fontSize = 10.sp,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun CenterHub(modifier: Modifier, onTap: () -> Unit) {
|
||||||
|
val transition = rememberInfiniteTransition(label = "hub")
|
||||||
|
val pulse by transition.animateFloat(
|
||||||
|
initialValue = 0.88f,
|
||||||
|
targetValue = 1.08f,
|
||||||
|
animationSpec = infiniteRepeatable(tween(2600, easing = LinearEasing), RepeatMode.Reverse),
|
||||||
|
label = "pulse",
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.size(76.dp)
|
||||||
|
.scale(pulse)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(
|
||||||
|
Brush.radialGradient(
|
||||||
|
listOf(NeonVerde.copy(alpha = 0.22f), androidx.compose.ui.graphics.Color.Transparent)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.clickable(
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = null,
|
||||||
|
onClick = onTap,
|
||||||
|
),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "✦",
|
||||||
|
color = NeonVerde,
|
||||||
|
fontSize = 26.sp,
|
||||||
|
fontWeight = FontWeight.Light,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reparte los nodos (ya ordenados) en anillos. Cada anillo crece hacia afuera y su
|
||||||
|
* capacidad depende de su circunferencia, así nada se amontona.
|
||||||
|
*/
|
||||||
|
private fun computePlacements(
|
||||||
|
nodes: List<AppNode>,
|
||||||
|
firstR: Float,
|
||||||
|
ringGap: Float,
|
||||||
|
spacing: Float,
|
||||||
|
): List<Placement> {
|
||||||
|
if (nodes.isEmpty()) return emptyList()
|
||||||
|
val result = ArrayList<Placement>(nodes.size)
|
||||||
|
var index = 0
|
||||||
|
var ring = 0
|
||||||
|
while (index < nodes.size) {
|
||||||
|
val radius = firstR + ring * ringGap
|
||||||
|
val circumference = 2.0 * PI * radius
|
||||||
|
val capacity = max(4, floor(circumference / spacing).toInt())
|
||||||
|
val count = min(capacity, nodes.size - index)
|
||||||
|
val base = ring * 0.5
|
||||||
|
for (j in 0 until count) {
|
||||||
|
val angle = base + 2.0 * PI * j / count
|
||||||
|
result.add(Placement(nodes[index + j], ring, angle, radius))
|
||||||
|
}
|
||||||
|
index += count
|
||||||
|
ring++
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package com.rizoma.orbita.ui
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.Settings
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.rizoma.orbita.data.AppRepository
|
||||||
|
import com.rizoma.orbita.data.UsageStore
|
||||||
|
import com.rizoma.orbita.model.AppNode
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class OrbitaViewModel(app: Application) : AndroidViewModel(app) {
|
||||||
|
|
||||||
|
private val repo = AppRepository(app)
|
||||||
|
private val usage = UsageStore(app)
|
||||||
|
|
||||||
|
private val rawApps = MutableStateFlow<List<AppNode>>(emptyList())
|
||||||
|
|
||||||
|
/** Nodos ya ponderados y ordenados: fijados primero, luego por calor, luego alfabético. */
|
||||||
|
val nodes: StateFlow<List<AppNode>> = combine(
|
||||||
|
rawApps,
|
||||||
|
usage.counts(),
|
||||||
|
usage.pinned(),
|
||||||
|
) { apps, counts, pinned ->
|
||||||
|
apps.map { node ->
|
||||||
|
node.copy(
|
||||||
|
score = (counts[node.key] ?: 0).toFloat(),
|
||||||
|
pinned = node.key in pinned,
|
||||||
|
)
|
||||||
|
}.sortedWith(
|
||||||
|
compareByDescending<AppNode> { it.pinned }
|
||||||
|
.thenByDescending { it.score }
|
||||||
|
.thenBy { it.label.lowercase() }
|
||||||
|
)
|
||||||
|
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
|
||||||
|
|
||||||
|
private val pkgReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) = refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
refresh()
|
||||||
|
val filter = IntentFilter().apply {
|
||||||
|
addAction(Intent.ACTION_PACKAGE_ADDED)
|
||||||
|
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
||||||
|
addAction(Intent.ACTION_PACKAGE_CHANGED)
|
||||||
|
addDataScheme("package")
|
||||||
|
}
|
||||||
|
getApplication<Application>().registerReceiver(pkgReceiver, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refresh() {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
rawApps.value = repo.loadApps()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launch(node: AppNode) {
|
||||||
|
val intent = Intent(Intent.ACTION_MAIN)
|
||||||
|
.addCategory(Intent.CATEGORY_LAUNCHER)
|
||||||
|
.setComponent(ComponentName(node.packageName, node.activityName))
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
runCatching { getApplication<Application>().startActivity(intent) }
|
||||||
|
viewModelScope.launch { usage.record(node.key) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun togglePin(node: AppNode) {
|
||||||
|
viewModelScope.launch { usage.setPinned(node.key, !node.pinned) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openInfo(node: AppNode) {
|
||||||
|
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||||
|
.setData(Uri.fromParts("package", node.packageName, null))
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
runCatching { getApplication<Application>().startActivity(intent) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun uninstall(node: AppNode) {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
val intent = Intent(Intent.ACTION_DELETE)
|
||||||
|
.setData(Uri.fromParts("package", node.packageName, null))
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
runCatching { getApplication<Application>().startActivity(intent) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
runCatching { getApplication<Application>().unregisterReceiver(pkgReceiver) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.rizoma.orbita.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.darkColorScheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
// Paleta del rizoma: neón sobre vacío.
|
||||||
|
val Vacio = Color(0xFF050507)
|
||||||
|
val Superficie = Color(0xFF0C0C12)
|
||||||
|
val NeonVerde = Color(0xFF7CFF4F)
|
||||||
|
val NeonRosa = Color(0xFFFF2E88)
|
||||||
|
val NeonCian = Color(0xFF00E5FF)
|
||||||
|
val Tenue = Color(0xB3FFFFFF)
|
||||||
|
|
||||||
|
private val EsquemaOrbita = darkColorScheme(
|
||||||
|
primary = NeonVerde,
|
||||||
|
onPrimary = Vacio,
|
||||||
|
secondary = NeonRosa,
|
||||||
|
onSecondary = Vacio,
|
||||||
|
tertiary = NeonCian,
|
||||||
|
onTertiary = Vacio,
|
||||||
|
background = Vacio,
|
||||||
|
onBackground = Color.White,
|
||||||
|
surface = Superficie,
|
||||||
|
onSurface = Color.White,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun OrbitaTheme(content: @Composable () -> Unit) {
|
||||||
|
MaterialTheme(colorScheme = EsquemaOrbita, content = content)
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<!-- arcos orbitales excéntricos -->
|
||||||
|
<path
|
||||||
|
android:pathData="M 30 78 A 26 26 0 1 1 78 66"
|
||||||
|
android:strokeColor="#7CFF4F"
|
||||||
|
android:strokeWidth="3.5"
|
||||||
|
android:strokeLineCap="round" />
|
||||||
|
<path
|
||||||
|
android:pathData="M 24 60 A 36 36 0 0 1 84 42"
|
||||||
|
android:strokeColor="#FF2E88"
|
||||||
|
android:strokeWidth="2.5"
|
||||||
|
android:strokeLineCap="round" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#7CFF4F"
|
||||||
|
android:pathData="M 54 78 m -7 0 a 7 7 0 1 1 14 0 a 7 7 0 1 1 -14 0" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF2E88"
|
||||||
|
android:pathData="M 78 42 m -4.5 0 a 4.5 4.5 0 1 1 9 0 a 4.5 4.5 0 1 1 -9 0" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00E5FF"
|
||||||
|
android:pathData="M 30 50 m -3.5 0 a 3.5 3.5 0 1 1 7 0 a 3.5 3.5 0 1 1 -7 0" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_bg" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_fg" />
|
||||||
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_bg">#050507</color>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">Órbita</string>
|
||||||
|
<string name="search_hint">buscar en el rizoma…</string>
|
||||||
|
<string name="menu_pin">fijar</string>
|
||||||
|
<string name="menu_unpin">soltar</string>
|
||||||
|
<string name="menu_info">info</string>
|
||||||
|
<string name="menu_uninstall">quitar</string>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<style name="Theme.Orbita" parent="android:Theme.Material.NoActionBar">
|
||||||
|
<item name="android:windowBackground">#050507</item>
|
||||||
|
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||||
|
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||||
|
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
google {
|
||||||
|
content {
|
||||||
|
includeGroupByRegex("com\\.android.*")
|
||||||
|
includeGroupByRegex("com\\.google.*")
|
||||||
|
includeGroupByRegex("androidx.*")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.name = "rizoma"
|
||||||
|
include(":launcher")
|
||||||
|
// :keyboard se habilitará cuando exista el módulo (siguiente fase)
|
||||||
|
// include(":keyboard")
|
||||||
Reference in New Issue
Block a user