feat: implement dynamic launcher shortcuts, launch intent routing, dynamic theme color overrides, and initial profiles
This commit is contained in:
@@ -31,6 +31,39 @@ import meatbag.common.AttentionTier
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
private var onProfileLaunch: ((String?) -> Unit)? = null
|
||||
|
||||
override fun onNewIntent(intent: android.content.Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
val profileId = intent?.getStringExtra("LAUNCH_PROFILE_ID")
|
||||
onProfileLaunch?.invoke(profileId)
|
||||
}
|
||||
|
||||
private fun pinShortcutForProfile(profileId: String, profileName: String) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val shortcutManager = getSystemService(android.content.pm.ShortcutManager::class.java)
|
||||
if (shortcutManager != null && shortcutManager.isRequestPinShortcutSupported) {
|
||||
val intent = android.content.Intent(this, MainActivity::class.java).apply {
|
||||
action = android.content.Intent.ACTION_VIEW
|
||||
putExtra("LAUNCH_PROFILE_ID", profileId)
|
||||
flags = android.content.Intent.FLAG_ACTIVITY_NEW_TASK or android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
}
|
||||
val shortcut = android.content.pm.ShortcutInfo.Builder(this, profileId)
|
||||
.setShortLabel(profileName)
|
||||
.setLongLabel("Launch $profileName")
|
||||
.setIcon(android.graphics.drawable.Icon.createWithResource(this, applicationInfo.icon))
|
||||
.setIntent(intent)
|
||||
.build()
|
||||
|
||||
val successIntent = android.content.Intent("com.whetforge.shortcut.PINNED")
|
||||
val successPendingIntent = android.app.PendingIntent.getBroadcast(
|
||||
this, 0, successIntent, android.app.PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
shortcutManager.requestPinShortcut(shortcut, successPendingIntent.intentSender)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -64,7 +97,7 @@ class MainActivity : ComponentActivity() {
|
||||
val prefs = getSharedPreferences("meatbag_prefs", Context.MODE_PRIVATE)
|
||||
|
||||
setContent {
|
||||
var host by remember { mutableStateOf(prefs.getString("host", "10.0.0.5") ?: "10.0.0.5") }
|
||||
var host by remember { mutableStateOf(prefs.getString("host", "10.0.0.7") ?: "10.0.0.7") }
|
||||
var portStr by remember { mutableStateOf(prefs.getString("port", "8080") ?: "8080") }
|
||||
var authKey by remember { mutableStateOf(prefs.getString("auth_key", "") ?: "") }
|
||||
|
||||
@@ -76,11 +109,47 @@ class MainActivity : ComponentActivity() {
|
||||
val context = this
|
||||
var errorMessage by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
var activeProfileId by remember { mutableStateOf(intent.getStringExtra("LAUNCH_PROFILE_ID")) }
|
||||
LaunchedEffect(Unit) {
|
||||
onProfileLaunch = { id ->
|
||||
activeProfileId = id
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-connect when a specific profile is launched via shortcut or intent
|
||||
LaunchedEffect(activeProfileId) {
|
||||
if (activeProfileId != null && connectionState == ConnectionState.DISCONNECTED && clientState == null) {
|
||||
errorMessage = null
|
||||
scope.launch(Dispatchers.Default) {
|
||||
val newClient = MeatbagClient(host.trim(), portStr.trim().toIntOrNull() ?: 8080, feedback)
|
||||
clientState = newClient
|
||||
newClient.connect(deviceType, activeProfileId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dynamic theme colors based on active profile
|
||||
val themePrimary = when (activeProfileId) {
|
||||
"lm_studio_remote" -> Color(0xFFBB86FC)
|
||||
"meatbag" -> AccentTeal
|
||||
else -> AccentTeal
|
||||
}
|
||||
val themeSecondary = when (activeProfileId) {
|
||||
"lm_studio_remote" -> Color(0xFF03DAC6)
|
||||
"meatbag" -> AccentPurple
|
||||
else -> AccentPurple
|
||||
}
|
||||
val themeBackground = when (activeProfileId) {
|
||||
"lm_studio_remote" -> Color(0xFF121212)
|
||||
"meatbag" -> DeepBg
|
||||
else -> DeepBg
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colors = darkColors(
|
||||
primary = AccentTeal,
|
||||
secondary = AccentPurple,
|
||||
background = DeepBg,
|
||||
primary = themePrimary,
|
||||
secondary = themeSecondary,
|
||||
background = themeBackground,
|
||||
surface = CardBg
|
||||
)
|
||||
) {
|
||||
@@ -231,6 +300,7 @@ class MainActivity : ComponentActivity() {
|
||||
clientState?.disconnect()
|
||||
TailscaleUserspace.stop()
|
||||
clientState = null
|
||||
activeProfileId = null
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(backgroundColor = DangerRed),
|
||||
shape = RoundedCornerShape(if (isWatch) 8.dp else 12.dp),
|
||||
@@ -248,14 +318,26 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
} else {
|
||||
// Settings & Connection Panel
|
||||
val appTitle = when (activeProfileId) {
|
||||
"lm_studio_remote" -> "LM Studio Lite"
|
||||
"meatbag" -> "Meatbag Agent"
|
||||
else -> "WhetForge Hub"
|
||||
}
|
||||
Text(
|
||||
text = "MEATBAG",
|
||||
color = AccentTeal,
|
||||
text = appTitle,
|
||||
color = themePrimary,
|
||||
fontSize = if (isWatch) 16.sp else 24.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
style = MaterialTheme.typography.h5
|
||||
)
|
||||
|
||||
if (activeProfileId != null && !isWatch) {
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
TextButton(onClick = { activeProfileId = null }) {
|
||||
Text("← Exit to Hub Dashboard", color = themePrimary, fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
|
||||
Text(
|
||||
@@ -326,7 +408,7 @@ class MainActivity : ComponentActivity() {
|
||||
socksPort = socksPort
|
||||
)
|
||||
clientState = newClient
|
||||
newClient.connect(deviceType)
|
||||
newClient.connect(deviceType, activeProfileId)
|
||||
}
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(backgroundColor = AccentPurple),
|
||||
@@ -354,6 +436,106 @@ class MainActivity : ComponentActivity() {
|
||||
modifier = Modifier.padding(horizontal = 4.dp)
|
||||
)
|
||||
}
|
||||
|
||||
if (activeProfileId == null && !isWatch) {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Divider(color = Color.White.copy(alpha = 0.1f))
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = "INSTALLED MARKETPLACE TILES",
|
||||
color = TextSecondary,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.align(Alignment.Start)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
// Profile 1: Meatbag Agent
|
||||
Card(
|
||||
backgroundColor = CardBg,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column {
|
||||
Text("Meatbag Agent", color = Color.White, fontWeight = FontWeight.Bold, fontSize = 15.sp)
|
||||
Text("AI Agent approvals & haptics", color = TextSecondary, fontSize = 12.sp)
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(
|
||||
onClick = {
|
||||
activeProfileId = "meatbag"
|
||||
errorMessage = null
|
||||
scope.launch(Dispatchers.Default) {
|
||||
val newClient = MeatbagClient(host.trim(), portStr.trim().toIntOrNull() ?: 8080, feedback)
|
||||
clientState = newClient
|
||||
newClient.connect(deviceType, "meatbag")
|
||||
}
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(backgroundColor = AccentTeal),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Text("Launch", color = DeepBg, fontWeight = FontWeight.Bold, fontSize = 11.sp)
|
||||
}
|
||||
Button(
|
||||
onClick = { pinShortcutForProfile("meatbag", "Meatbag Agent") },
|
||||
colors = ButtonDefaults.buttonColors(backgroundColor = DeepBg),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Text("Pin Tile", color = Color.White, fontSize = 11.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Profile 2: LM Studio Lite
|
||||
Card(
|
||||
backgroundColor = CardBg,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column {
|
||||
Text("LM Studio Lite", color = Color.White, fontWeight = FontWeight.Bold, fontSize = 15.sp)
|
||||
Text("Control local LLM models", color = TextSecondary, fontSize = 12.sp)
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(
|
||||
onClick = {
|
||||
activeProfileId = "lm_studio_remote"
|
||||
errorMessage = null
|
||||
scope.launch(Dispatchers.Default) {
|
||||
val newClient = MeatbagClient(host.trim(), portStr.trim().toIntOrNull() ?: 8080, feedback)
|
||||
clientState = newClient
|
||||
newClient.connect(deviceType, "lm_studio_remote")
|
||||
}
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(backgroundColor = Color(0xFFBB86FC)),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Text("Launch", color = Color.Black, fontWeight = FontWeight.Bold, fontSize = 11.sp)
|
||||
}
|
||||
Button(
|
||||
onClick = { pinShortcutForProfile("lm_studio_remote", "LM Studio Lite") },
|
||||
colors = ButtonDefaults.buttonColors(backgroundColor = DeepBg),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Text("Pin Tile", color = Color.White, fontSize = 11.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,18 +71,23 @@ class MeatbagClient(
|
||||
private var wsSession: DefaultClientWebSocketSession? = null
|
||||
private var connectionJob: Job? = null
|
||||
|
||||
fun connect(deviceType: String) {
|
||||
fun connect(deviceType: String, profileId: String? = null) {
|
||||
connectionJob?.cancel()
|
||||
connectionJob = CoroutineScope(Dispatchers.Default).launch {
|
||||
while (isActive) {
|
||||
try {
|
||||
log("Connecting to ws://$host:$port/ws/connect?deviceType=$deviceType ...")
|
||||
val queryParams = if (profileId != null) {
|
||||
"?deviceType=$deviceType&profileId=$profileId"
|
||||
} else {
|
||||
"?deviceType=$deviceType"
|
||||
}
|
||||
log("Connecting to ws://$host:$port/ws/connect$queryParams ...")
|
||||
_connectionState.value = ConnectionState.CONNECTING
|
||||
client.webSocket(
|
||||
method = HttpMethod.Get,
|
||||
host = host,
|
||||
port = port,
|
||||
path = "/ws/connect?deviceType=$deviceType"
|
||||
path = "/ws/connect$queryParams"
|
||||
) {
|
||||
wsSession = this
|
||||
_isConnected.value = true
|
||||
@@ -194,6 +199,36 @@ class MeatbagClient(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getProfile(id: String): meatbag.common.UserProfile? {
|
||||
return try {
|
||||
val response = client.get("http://$host:$port/api/profiles/$id")
|
||||
if (response.status.value == 200) {
|
||||
val jsonStr = response.bodyAsText()
|
||||
jsonConfig.decodeFromString(meatbag.common.UserProfile.serializer(), jsonStr)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
log("Error getting profile $id: ${e.message}", LogType.ERROR)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun listProfiles(): List<meatbag.common.UserProfile> {
|
||||
return try {
|
||||
val response = client.get("http://$host:$port/api/profiles")
|
||||
if (response.status.value == 200) {
|
||||
val jsonStr = response.bodyAsText()
|
||||
jsonConfig.decodeFromString(kotlinx.serialization.builtins.ListSerializer(meatbag.common.UserProfile.serializer()), jsonStr)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
log("Error listing profiles: ${e.message}", LogType.ERROR)
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun log(message: String, type: LogType = LogType.INFO) {
|
||||
println("[MeatbagClient] $type: $message")
|
||||
_events.tryEmit(ClientEvent.LogMessage(message, type))
|
||||
|
||||
Reference in New Issue
Block a user