Phase 1: Compose UI - capture bar, dashboard, themes, 7-tap dev menu
Browse files
app/src/main/java/io/github/abrarul/modularai/MainActivity.kt
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package io.github.abrarul.modularai
|
| 2 |
+
|
| 3 |
+
import android.content.Intent
|
| 4 |
+
import android.os.Bundle
|
| 5 |
+
import androidx.activity.ComponentActivity
|
| 6 |
+
import androidx.activity.compose.setContent
|
| 7 |
+
import androidx.compose.foundation.clickable
|
| 8 |
+
import androidx.compose.foundation.layout.*
|
| 9 |
+
import androidx.compose.foundation.lazy.LazyColumn
|
| 10 |
+
import androidx.compose.foundation.lazy.items
|
| 11 |
+
import androidx.compose.foundation.isSystemInDarkTheme
|
| 12 |
+
import androidx.compose.material3.*
|
| 13 |
+
import androidx.compose.runtime.*
|
| 14 |
+
import androidx.compose.ui.Modifier
|
| 15 |
+
import androidx.compose.ui.unit.dp
|
| 16 |
+
import io.github.abrarul.modularai.data.AppDb
|
| 17 |
+
import io.github.abrarul.modularai.data.Capture
|
| 18 |
+
import io.github.abrarul.modularai.model.DeviceProfiler
|
| 19 |
+
import io.github.abrarul.modularai.model.Profile
|
| 20 |
+
import io.github.abrarul.modularai.model.Recommendation
|
| 21 |
+
import io.github.abrarul.modularai.security.Vault
|
| 22 |
+
import kotlinx.coroutines.Dispatchers
|
| 23 |
+
import kotlinx.coroutines.launch
|
| 24 |
+
import kotlinx.coroutines.withContext
|
| 25 |
+
|
| 26 |
+
class MainActivity : ComponentActivity() {
|
| 27 |
+
override fun onCreate(savedInstanceState: Bundle?) {
|
| 28 |
+
super.onCreate(savedInstanceState)
|
| 29 |
+
val shared = intent.takeIf { it.action == Intent.ACTION_SEND }?.getStringExtra(Intent.EXTRA_TEXT)
|
| 30 |
+
setContent { App(shared) }
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
@OptIn(ExperimentalMaterial3Api::class)
|
| 34 |
+
@Composable
|
| 35 |
+
fun App(sharedText: String?) {
|
| 36 |
+
var mode by remember { mutableStateOf("system") } // system | dark | light
|
| 37 |
+
val dark = when (mode) { "dark" -> true; "light" -> false; else -> isSystemInDarkTheme() }
|
| 38 |
+
MaterialTheme(colorScheme = if (dark) darkColorScheme() else lightColorScheme()) {
|
| 39 |
+
val db = remember { AppDb.get(this) }
|
| 40 |
+
val items by db.captures().all().collectAsState(initial = emptyList())
|
| 41 |
+
val scope = rememberCoroutineScope()
|
| 42 |
+
var text by remember { mutableStateOf(sharedText.orEmpty()) }
|
| 43 |
+
var taps by remember { mutableIntStateOf(0) }
|
| 44 |
+
var devMenu by remember { mutableStateOf(false) }
|
| 45 |
+
var profile by remember { mutableStateOf<Pair<Profile, Recommendation>?>(null) }
|
| 46 |
+
|
| 47 |
+
LaunchedEffect(Unit) {
|
| 48 |
+
profile = withContext(Dispatchers.Default) {
|
| 49 |
+
val p = DeviceProfiler.scan(this@MainActivity); p to DeviceProfiler.recommend(p)
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
Scaffold(
|
| 54 |
+
topBar = {
|
| 55 |
+
TopAppBar(
|
| 56 |
+
title = {
|
| 57 |
+
Text("ModularAI", Modifier.clickable {
|
| 58 |
+
taps++
|
| 59 |
+
if (taps >= 7) { devMenu = !devMenu; taps = 0 }
|
| 60 |
+
})
|
| 61 |
+
},
|
| 62 |
+
actions = {
|
| 63 |
+
TextButton(onClick = {
|
| 64 |
+
mode = when (mode) { "system" -> "dark"; "dark" -> "light"; else -> "system" }
|
| 65 |
+
}) { Text(mode) }
|
| 66 |
+
}
|
| 67 |
+
)
|
| 68 |
+
},
|
| 69 |
+
bottomBar = {
|
| 70 |
+
Row(Modifier.fillMaxWidth().padding(12.dp).navigationBarsPadding()) {
|
| 71 |
+
OutlinedTextField(
|
| 72 |
+
value = text, onValueChange = { text = it },
|
| 73 |
+
modifier = Modifier.weight(1f), placeholder = { Text("Capture anything...") }
|
| 74 |
+
)
|
| 75 |
+
Spacer(Modifier.width(8.dp))
|
| 76 |
+
Button(onClick = {
|
| 77 |
+
val t = text.trim()
|
| 78 |
+
if (t.isNotEmpty()) scope.launch {
|
| 79 |
+
withContext(Dispatchers.IO) { db.captures().insert(Capture(cipher = Vault.encrypt(t))) }
|
| 80 |
+
text = ""
|
| 81 |
+
}
|
| 82 |
+
}) { Text("Save") }
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
) { pad ->
|
| 86 |
+
LazyColumn(Modifier.padding(pad).padding(horizontal = 12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
| 87 |
+
profile?.let { (p, r) ->
|
| 88 |
+
item {
|
| 89 |
+
Card(Modifier.fillMaxWidth()) {
|
| 90 |
+
Column(Modifier.padding(16.dp)) {
|
| 91 |
+
Text("Device: ${p.soc} | ${p.abi} | ${p.cores} cores", style = MaterialTheme.typography.titleSmall)
|
| 92 |
+
Text("RAM ${p.totalRamMb} MB (free ${p.availRamMb}) | Storage free ${p.freeStorageMb} MB | Disk ${"%.0f".format(p.storageMBps)} MB/s | Vulkan ${p.vulkan}")
|
| 93 |
+
Text("Tier: ${p.tier.label} (${p.tier.sizes})")
|
| 94 |
+
Text("Suggested: up to ~${"%.1f".format(r.maxParamsB)}B, ${r.quant}, ctx ${r.contextTokens}, est ${r.estTokensPerSec} tok/s")
|
| 95 |
+
}
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
if (devMenu) item {
|
| 100 |
+
Card(Modifier.fillMaxWidth()) {
|
| 101 |
+
Column(Modifier.padding(16.dp)) {
|
| 102 |
+
Text("Developer menu unlocked", style = MaterialTheme.typography.titleSmall)
|
| 103 |
+
Text("Termux bridge, plugins, force-run oversize models, update channel.")
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
items(items, key = { it.id }) { c ->
|
| 108 |
+
val plain = remember(c.cipher) { runCatching { Vault.decrypt(c.cipher) }.getOrDefault("[unreadable]") }
|
| 109 |
+
Card(Modifier.fillMaxWidth()) {
|
| 110 |
+
Row(Modifier.padding(16.dp)) {
|
| 111 |
+
Text(plain, Modifier.weight(1f))
|
| 112 |
+
TextButton(onClick = { scope.launch(Dispatchers.IO) { db.captures().delete(c) } }) { Text("Delete") }
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
}
|
| 118 |
+
}
|
| 119 |
+
}
|
| 120 |
+
}
|