diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
deleted file mode 100644
index d1703d2..0000000
--- a/android/app/build.gradle.kts
+++ /dev/null
@@ -1,57 +0,0 @@
-plugins {
- id("com.android.application")
- id("org.jetbrains.kotlin.android")
- id("org.jetbrains.kotlin.plugin.compose")
- id("org.jetbrains.kotlin.plugin.serialization")
-}
-
-android {
- namespace = "com.kxbridge"
- compileSdk = 34
-
- defaultConfig {
- applicationId = "com.kxbridge"
- minSdk = 26
- targetSdk = 34
- versionCode = 1
- versionName = "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 {
- val composeBom = platform("androidx.compose:compose-bom:2024.05.00")
- implementation(composeBom)
- implementation("androidx.compose.ui:ui")
- implementation("androidx.compose.ui:ui-tooling-preview")
- implementation("androidx.compose.material3:material3")
- implementation("androidx.activity:activity-compose:1.9.0")
- implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.0")
- implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.0")
- implementation("com.squareup.okhttp3:okhttp:4.12.0")
- implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
- implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
- debugImplementation("androidx.compose.ui:ui-tooling")
-}
diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro
deleted file mode 100644
index 1d5102f..0000000
--- a/android/app/proguard-rules.pro
+++ /dev/null
@@ -1,2 +0,0 @@
--keep class com.kxbridge.data.model.** { *; }
--keepattributes *Annotation*
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
deleted file mode 100644
index c54fa50..0000000
--- a/android/app/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/android/app/src/main/java/com/kxbridge/MainActivity.kt b/android/app/src/main/java/com/kxbridge/MainActivity.kt
deleted file mode 100644
index 64996e0..0000000
--- a/android/app/src/main/java/com/kxbridge/MainActivity.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-package com.kxbridge
-
-import android.content.Context
-import android.os.Bundle
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.activity.enableEdgeToEdge
-import androidx.activity.viewModels
-import androidx.compose.runtime.*
-import com.kxbridge.ui.PrinterScreen
-import com.kxbridge.ui.SetupScreen
-import com.kxbridge.ui.theme.KxBridgeTheme
-import com.kxbridge.viewmodel.PrinterViewModel
-
-private const val PREFS = "kxbridge"
-private const val KEY_URL = "server_url"
-
-class MainActivity : ComponentActivity() {
-
- private val viewModel: PrinterViewModel by viewModels()
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- enableEdgeToEdge()
-
- val prefs = getSharedPreferences(PREFS, Context.MODE_PRIVATE)
- val savedUrl = prefs.getString(KEY_URL, null)
- if (savedUrl != null) {
- viewModel.connect(savedUrl)
- }
-
- setContent {
- KxBridgeTheme {
- var serverUrl by remember { mutableStateOf(savedUrl ?: "") }
- var showSetup by remember { mutableStateOf(savedUrl == null) }
-
- if (showSetup) {
- SetupScreen(
- initialUrl = serverUrl,
- onConnect = { url ->
- prefs.edit().putString(KEY_URL, url).apply()
- serverUrl = url
- showSetup = false
- viewModel.connect(url)
- },
- )
- } else {
- PrinterScreen(
- viewModel = viewModel,
- onChangeServer = { showSetup = true },
- )
- }
- }
- }
- }
-}
diff --git a/android/app/src/main/java/com/kxbridge/ui/PrinterScreen.kt b/android/app/src/main/java/com/kxbridge/ui/PrinterScreen.kt
deleted file mode 100644
index 3f84cbc..0000000
--- a/android/app/src/main/java/com/kxbridge/ui/PrinterScreen.kt
+++ /dev/null
@@ -1,594 +0,0 @@
-package com.kxbridge.ui
-
-import android.graphics.Bitmap
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.text.KeyboardOptions
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Settings
-import androidx.compose.material3.*
-import androidx.compose.runtime.*
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.asImageBitmap
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.input.KeyboardType
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.unit.dp
-import com.kxbridge.data.model.AmsSlot
-import com.kxbridge.data.model.PrinterState
-import com.kxbridge.ui.theme.Blue300
-import com.kxbridge.ui.theme.ErrorRed
-import com.kxbridge.ui.theme.Orange400
-import com.kxbridge.viewmodel.PrinterViewModel
-import com.kxbridge.viewmodel.UiState
-import kotlin.math.roundToInt
-
-private enum class TempTarget { NOZZLE, BED }
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun PrinterScreen(
- viewModel: PrinterViewModel,
- onChangeServer: () -> Unit,
-) {
- val uiState by viewModel.uiState.collectAsState()
- val cameraFrame by viewModel.cameraFrame.collectAsState()
- val cameraEnabled by viewModel.cameraEnabled.collectAsState()
- var showCancelDialog by remember { mutableStateOf(false) }
-
- if (showCancelDialog) {
- AlertDialog(
- onDismissRequest = { showCancelDialog = false },
- title = { Text("Stop print?") },
- text = { Text("This will stop the current print job.") },
- confirmButton = {
- TextButton(onClick = {
- showCancelDialog = false
- viewModel.cancel()
- }) { Text("Stop print", color = ErrorRed) }
- },
- dismissButton = {
- TextButton(onClick = { showCancelDialog = false }) { Text("Keep printing") }
- },
- )
- }
-
- Scaffold(
- topBar = {
- TopAppBar(
- title = {
- val name = (uiState as? UiState.Success)?.state?.printerName
- ?.takeIf { it.isNotBlank() } ?: "KX-Bridge"
- Text(name, maxLines = 1, overflow = TextOverflow.Ellipsis)
- },
- actions = {
- IconButton(onClick = onChangeServer) {
- Icon(Icons.Default.Settings, contentDescription = "Change server")
- }
- },
- colors = TopAppBarDefaults.topAppBarColors(
- containerColor = MaterialTheme.colorScheme.surface,
- ),
- )
- },
- ) { padding ->
- Box(
- modifier = Modifier
- .fillMaxSize()
- .padding(padding),
- contentAlignment = Alignment.Center,
- ) {
- when (val s = uiState) {
- is UiState.Loading -> CircularProgressIndicator()
- is UiState.Error -> ErrorContent(s.message)
- is UiState.Success -> PrinterContent(
- state = s.state,
- cameraFrame = cameraFrame,
- cameraEnabled = cameraEnabled,
- onPause = viewModel::pause,
- onResume = viewModel::resume,
- onCancel = { showCancelDialog = true },
- onSetTemp = viewModel::setTemperature,
- onToggleLight = { on -> viewModel.setLight(on) },
- onToggleCamera = viewModel::setCameraEnabled,
- )
- }
- }
- }
-}
-
-@Composable
-private fun ErrorContent(message: String) {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.spacedBy(8.dp),
- ) {
- Text("Cannot reach bridge", style = MaterialTheme.typography.titleMedium, color = ErrorRed)
- Text(
- message,
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
- )
- }
-}
-
-@Composable
-private fun PrinterContent(
- state: PrinterState,
- cameraFrame: Bitmap?,
- cameraEnabled: Boolean,
- onPause: () -> Unit,
- onResume: () -> Unit,
- onCancel: () -> Unit,
- onSetTemp: (nozzle: Double, bed: Double) -> Unit,
- onToggleLight: (Boolean) -> Unit,
- onToggleCamera: (Boolean) -> Unit,
-) {
- var tempDialog by remember { mutableStateOf(null) }
-
- if (tempDialog != null) {
- TempDialog(
- target = tempDialog!!,
- currentNozzle = state.nozzleTarget,
- currentBed = state.bedTarget,
- onConfirm = { nozzle, bed ->
- onSetTemp(nozzle, bed)
- tempDialog = null
- },
- onDismiss = { tempDialog = null },
- )
- }
-
- Column(
- modifier = Modifier
- .fillMaxSize()
- .verticalScroll(rememberScrollState())
- .padding(16.dp),
- verticalArrangement = Arrangement.spacedBy(12.dp),
- ) {
- StatusBadge(state.printState)
-
- LightCard(
- isOn = state.lightOn,
- brightness = state.lightBrightness,
- onToggle = onToggleLight,
- )
-
- CameraCard(
- cameraFrame = cameraFrame,
- cameraEnabled = cameraEnabled,
- onToggle = onToggleCamera,
- )
-
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.spacedBy(12.dp),
- ) {
- TempCard(
- label = "Nozzle",
- current = state.nozzleTemp,
- target = state.nozzleTarget,
- activeColor = Orange400,
- modifier = Modifier.weight(1f),
- onClick = { tempDialog = TempTarget.NOZZLE },
- )
- TempCard(
- label = "Bed",
- current = state.bedTemp,
- target = state.bedTarget,
- activeColor = Blue300,
- modifier = Modifier.weight(1f),
- onClick = { tempDialog = TempTarget.BED },
- )
- }
-
- if (state.isActive || state.printState == "complete") {
- PrintProgressCard(state)
- }
-
- if (state.isActive) {
- PrintControls(
- isPrinting = state.isPrinting,
- onPause = onPause,
- onResume = onResume,
- onCancel = onCancel,
- )
- }
-
- if (state.amsSlots.isNotEmpty()) {
- FilamentCard(slots = state.amsSlots, mode = state.filamentMode)
- }
- }
-}
-
-@Composable
-private fun StatusBadge(printState: String) {
- val (label, color) = when (printState) {
- "printing" -> "Printing" to Orange400
- "paused" -> "Paused" to Blue300
- "complete" -> "Complete" to Color(0xFF66BB6A)
- "error" -> "Error" to ErrorRed
- "cancelled" -> "Cancelled" to MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f)
- else -> "Standby" to MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f)
- }
- Surface(
- color = color.copy(alpha = 0.15f),
- shape = MaterialTheme.shapes.small,
- ) {
- Text(
- text = label,
- color = color,
- style = MaterialTheme.typography.labelLarge,
- fontWeight = FontWeight.Bold,
- modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
- )
- }
-}
-
-@Composable
-private fun CameraCard(
- cameraFrame: Bitmap?,
- cameraEnabled: Boolean,
- onToggle: (Boolean) -> Unit,
-) {
- Card(
- modifier = Modifier.fillMaxWidth(),
- colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
- ) {
- Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Text(
- "Camera",
- style = MaterialTheme.typography.labelLarge,
- color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
- )
- Switch(checked = cameraEnabled, onCheckedChange = onToggle)
- }
- if (cameraEnabled) {
- if (cameraFrame != null) {
- Image(
- bitmap = cameraFrame.asImageBitmap(),
- contentDescription = "Camera stream",
- contentScale = ContentScale.Fit,
- modifier = Modifier
- .fillMaxWidth()
- .aspectRatio(4f / 3f),
- )
- } else {
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .aspectRatio(4f / 3f)
- .background(Color.Black.copy(alpha = 0.4f)),
- contentAlignment = Alignment.Center,
- ) {
- CircularProgressIndicator()
- }
- }
- }
- }
- }
-}
-
-@Composable
-private fun TempCard(
- label: String,
- current: Double,
- target: Double,
- activeColor: Color,
- modifier: Modifier = Modifier,
- onClick: () -> Unit = {},
-) {
- val isHeating = target > 0.0
- val indicatorColor = if (isHeating) activeColor else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f)
-
- Card(
- onClick = onClick,
- modifier = modifier,
- colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
- ) {
- Column(
- modifier = Modifier.padding(16.dp),
- verticalArrangement = Arrangement.spacedBy(4.dp),
- ) {
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.Top,
- ) {
- Text(
- label,
- style = MaterialTheme.typography.labelMedium,
- color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
- )
- Text(
- "Tap to set",
- style = MaterialTheme.typography.labelSmall,
- color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f),
- )
- }
- Text(
- text = "${current.roundToInt()}°",
- style = MaterialTheme.typography.headlineMedium,
- color = indicatorColor,
- fontWeight = FontWeight.Bold,
- )
- Text(
- text = if (isHeating) "→ ${target.roundToInt()}°" else "Off",
- style = MaterialTheme.typography.bodySmall,
- color = indicatorColor.copy(alpha = 0.7f),
- )
- }
- }
-}
-
-@Composable
-private fun PrintProgressCard(state: PrinterState) {
- Card(
- modifier = Modifier.fillMaxWidth(),
- colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
- ) {
- Column(
- modifier = Modifier.padding(16.dp),
- verticalArrangement = Arrangement.spacedBy(8.dp),
- ) {
- if (state.filename.isNotBlank()) {
- Text(
- text = state.filename,
- style = MaterialTheme.typography.bodyMedium,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f),
- )
- }
-
- val pct = (state.progress * 100).roundToInt()
- LinearProgressIndicator(
- progress = { state.progress.toFloat().coerceIn(0f, 1f) },
- modifier = Modifier.fillMaxWidth(),
- color = MaterialTheme.colorScheme.primary,
- )
-
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Text("$pct%", style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold)
- val curr = state.currLayer
- val total = state.totalLayers
- if (curr != null && total != null && total > 0) {
- Text(
- "Layer $curr / $total",
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
- )
- }
- }
-
- val remain = state.remainTime
- if (state.printDuration > 0 || (remain != null && remain > 0)) {
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.spacedBy(12.dp),
- ) {
- if (state.printDuration > 0) {
- TimeLabel(
- label = "Elapsed",
- seconds = state.printDuration.toInt(),
- modifier = Modifier.weight(1f),
- )
- }
- if (remain != null && remain > 0) {
- val estimate = state.printDuration.toInt() + remain
- TimeLabel(label = "Est. Total", seconds = estimate, modifier = Modifier.weight(1f))
- TimeLabel(label = "Remaining", seconds = remain, modifier = Modifier.weight(1f))
- }
- }
- }
- }
- }
-}
-
-@Composable
-private fun TimeLabel(label: String, seconds: Int, modifier: Modifier = Modifier) {
- Column(modifier = modifier) {
- Text(
- label,
- style = MaterialTheme.typography.labelSmall,
- color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
- )
- Text(
- formatDuration(seconds),
- style = MaterialTheme.typography.bodySmall,
- fontWeight = FontWeight.Medium,
- )
- }
-}
-
-private fun formatDuration(totalSeconds: Int): String {
- val h = totalSeconds / 3600
- val m = (totalSeconds % 3600) / 60
- val s = totalSeconds % 60
- return when {
- h > 0 -> "${h}h ${m}m"
- m > 0 -> "${m}m ${s}s"
- else -> "${s}s"
- }
-}
-
-@Composable
-private fun PrintControls(
- isPrinting: Boolean,
- onPause: () -> Unit,
- onResume: () -> Unit,
- onCancel: () -> Unit,
-) {
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.spacedBy(12.dp),
- ) {
- if (isPrinting) {
- Button(onClick = onPause, modifier = Modifier.weight(1f)) { Text("Pause") }
- } else {
- Button(onClick = onResume, modifier = Modifier.weight(1f)) { Text("Resume") }
- }
- OutlinedButton(
- onClick = onCancel,
- modifier = Modifier.weight(1f),
- colors = ButtonDefaults.outlinedButtonColors(contentColor = ErrorRed),
- ) {
- Text("Stop")
- }
- }
-}
-
-@Composable
-private fun LightCard(isOn: Boolean, brightness: Int, onToggle: (Boolean) -> Unit) {
- Card(
- modifier = Modifier.fillMaxWidth(),
- colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
- ) {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Column {
- Text("Light", style = MaterialTheme.typography.labelLarge)
- Text(
- if (isOn) "$brightness%" else "Off",
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
- )
- }
- Switch(
- checked = isOn,
- onCheckedChange = onToggle,
- colors = SwitchDefaults.colors(
- checkedThumbColor = Orange400,
- checkedTrackColor = Orange400.copy(alpha = 0.5f),
- ),
- )
- }
- }
-}
-
-@Composable
-private fun FilamentCard(slots: List, mode: String) {
- Card(
- modifier = Modifier.fillMaxWidth(),
- colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
- ) {
- Column(
- modifier = Modifier.padding(16.dp),
- verticalArrangement = Arrangement.spacedBy(10.dp),
- ) {
- Text(
- text = when (mode) {
- "ace_direct" -> "AMS Direct"
- "ace_hub" -> "AMS Hub"
- else -> "Filament"
- },
- style = MaterialTheme.typography.labelLarge,
- color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
- )
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.spacedBy(8.dp),
- ) {
- slots.forEach { slot -> FilamentSlot(slot, Modifier.weight(1f)) }
- }
- }
- }
-}
-
-@Composable
-private fun FilamentSlot(slot: AmsSlot, modifier: Modifier = Modifier) {
- val slotColor = if (slot.color.size >= 3) {
- Color(slot.color[0] / 255f, slot.color[1] / 255f, slot.color[2] / 255f)
- } else {
- MaterialTheme.colorScheme.onSurface.copy(alpha = 0.25f)
- }
-
- Column(
- modifier = modifier,
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.spacedBy(4.dp),
- ) {
- Box(
- modifier = Modifier
- .size(32.dp)
- .background(slotColor, shape = CircleShape),
- )
- Text(
- text = slot.type.ifBlank { "–" },
- style = MaterialTheme.typography.labelSmall,
- textAlign = TextAlign.Center,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- )
- if (slot.brand.isNotBlank()) {
- Text(
- text = slot.brand,
- style = MaterialTheme.typography.labelSmall,
- color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
- textAlign = TextAlign.Center,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- )
- }
- }
-}
-
-@Composable
-private fun TempDialog(
- target: TempTarget,
- currentNozzle: Double,
- currentBed: Double,
- onConfirm: (nozzle: Double, bed: Double) -> Unit,
- onDismiss: () -> Unit,
-) {
- val label = if (target == TempTarget.NOZZLE) "Nozzle" else "Bed"
- val currentValue = if (target == TempTarget.NOZZLE) currentNozzle else currentBed
- var input by remember { mutableStateOf(currentValue.roundToInt().toString()) }
-
- AlertDialog(
- onDismissRequest = onDismiss,
- title = { Text("Set $label Temperature") },
- text = {
- OutlinedTextField(
- value = input,
- onValueChange = { input = it.filter { c -> c.isDigit() } },
- label = { Text("Temperature (°C)") },
- suffix = { Text("°C") },
- keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
- singleLine = true,
- )
- },
- confirmButton = {
- TextButton(onClick = {
- val newTemp = input.toDoubleOrNull() ?: currentValue
- if (target == TempTarget.NOZZLE) onConfirm(newTemp, currentBed)
- else onConfirm(currentNozzle, newTemp)
- }) { Text("Set") }
- },
- dismissButton = {
- TextButton(onClick = onDismiss) { Text("Cancel") }
- },
- )
-}
diff --git a/android/app/src/main/java/com/kxbridge/ui/SetupScreen.kt b/android/app/src/main/java/com/kxbridge/ui/SetupScreen.kt
deleted file mode 100644
index fe58b9b..0000000
--- a/android/app/src/main/java/com/kxbridge/ui/SetupScreen.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-package com.kxbridge.ui
-
-import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.text.KeyboardActions
-import androidx.compose.foundation.text.KeyboardOptions
-import androidx.compose.material3.*
-import androidx.compose.runtime.*
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.text.input.ImeAction
-import androidx.compose.ui.text.input.KeyboardType
-import androidx.compose.ui.unit.dp
-
-@Composable
-fun SetupScreen(initialUrl: String = "", onConnect: (String) -> Unit) {
- var url by remember { mutableStateOf(initialUrl.ifBlank { "http://" }) }
-
- fun submit() {
- val trimmed = url.trim()
- if (trimmed.length > 7) onConnect(trimmed)
- }
-
- Column(
- modifier = Modifier
- .fillMaxSize()
- .padding(32.dp),
- verticalArrangement = Arrangement.Center,
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- Text(
- text = "KX-Bridge",
- style = MaterialTheme.typography.headlineLarge,
- color = MaterialTheme.colorScheme.primary,
- )
-
- Spacer(Modifier.height(8.dp))
-
- Text(
- text = "Enter your bridge server URL",
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- )
-
- Spacer(Modifier.height(40.dp))
-
- OutlinedTextField(
- value = url,
- onValueChange = { url = it },
- label = { Text("Server URL") },
- placeholder = { Text("http://192.168.1.x:7125") },
- singleLine = true,
- keyboardOptions = KeyboardOptions(
- keyboardType = KeyboardType.Uri,
- imeAction = ImeAction.Done,
- ),
- keyboardActions = KeyboardActions(onDone = { submit() }),
- modifier = Modifier.fillMaxWidth(),
- )
-
- Spacer(Modifier.height(24.dp))
-
- Button(
- onClick = ::submit,
- modifier = Modifier.fillMaxWidth(),
- enabled = url.trim().length > 7,
- ) {
- Text("Connect")
- }
- }
-}
diff --git a/android/app/src/main/java/com/kxbridge/ui/theme/Color.kt b/android/app/src/main/java/com/kxbridge/ui/theme/Color.kt
deleted file mode 100644
index f171246..0000000
--- a/android/app/src/main/java/com/kxbridge/ui/theme/Color.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.kxbridge.ui.theme
-
-import androidx.compose.ui.graphics.Color
-
-val Orange400 = Color(0xFFFF6B35)
-val Orange600 = Color(0xFFE84E0F) // darker for light-mode contrast on white
-val Blue300 = Color(0xFF4FC3F7)
-val Blue600 = Color(0xFF0288D1) // darker for light-mode contrast on white
-val Surface900 = Color(0xFF121212)
-val Surface800 = Color(0xFF1E1E1E)
-val Surface700 = Color(0xFF2C2C2C)
-val OnSurfaceDark = Color(0xFFE0E0E0)
-val ErrorRed = Color(0xFFCF6679)
diff --git a/android/app/src/main/java/com/kxbridge/ui/theme/Theme.kt b/android/app/src/main/java/com/kxbridge/ui/theme/Theme.kt
deleted file mode 100644
index 5cea4e7..0000000
--- a/android/app/src/main/java/com/kxbridge/ui/theme/Theme.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.kxbridge.ui.theme
-
-import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.darkColorScheme
-import androidx.compose.material3.lightColorScheme
-import androidx.compose.runtime.Composable
-
-private val DarkColors = darkColorScheme(
- primary = Orange400,
- secondary = Blue300,
- background = Surface900,
- surface = Surface800,
- surfaceVariant = Surface700,
- onBackground = OnSurfaceDark,
- onSurface = OnSurfaceDark,
- error = ErrorRed,
-)
-
-private val LightColors = lightColorScheme(
- primary = Orange600,
- secondary = Blue600,
- error = ErrorRed,
-)
-
-@Composable
-fun KxBridgeTheme(content: @Composable () -> Unit) {
- val colors = if (isSystemInDarkTheme()) DarkColors else LightColors
- MaterialTheme(colorScheme = colors, content = content)
-}
diff --git a/android/app/src/main/java/com/kxbridge/viewmodel/PrinterViewModel.kt b/android/app/src/main/java/com/kxbridge/viewmodel/PrinterViewModel.kt
deleted file mode 100644
index 1754d16..0000000
--- a/android/app/src/main/java/com/kxbridge/viewmodel/PrinterViewModel.kt
+++ /dev/null
@@ -1,83 +0,0 @@
-package com.kxbridge.viewmodel
-
-import android.graphics.Bitmap
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import com.kxbridge.data.PrinterRepository
-import com.kxbridge.data.model.PrinterState
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.launch
-
-sealed interface UiState {
- data object Loading : UiState
- data class Error(val message: String) : UiState
- data class Success(val state: PrinterState) : UiState
-}
-
-class PrinterViewModel : ViewModel() {
-
- private val _uiState = MutableStateFlow(UiState.Loading)
- val uiState: StateFlow = _uiState
-
- private val _cameraFrame = MutableStateFlow(null)
- val cameraFrame: StateFlow = _cameraFrame
-
- private val _cameraEnabled = MutableStateFlow(false)
- val cameraEnabled: StateFlow = _cameraEnabled
-
- private var pollJob: Job? = null
- private var cameraJob: Job? = null
- private var repo: PrinterRepository? = null
-
- fun connect(baseUrl: String) {
- pollJob?.cancel()
- repo = PrinterRepository(baseUrl.trimEnd('/'))
- _uiState.value = UiState.Loading
- pollJob = viewModelScope.launch(Dispatchers.IO) {
- repo!!.stateFlow().collect { result ->
- _uiState.value = result.fold(
- onSuccess = { UiState.Success(it) },
- onFailure = { UiState.Error(it.message ?: "Connection failed") },
- )
- }
- }
- }
-
- fun pause() = command { it.pause() }
- fun resume() = command { it.resume() }
- fun cancel() = command { it.cancel() }
-
- fun setLight(on: Boolean, brightness: Int = 80) = command { it.setLight(on, brightness) }
- fun setTemperature(nozzle: Double, bed: Double) = command { it.setTemperature(nozzle, bed) }
-
- fun setCameraEnabled(enabled: Boolean) {
- _cameraEnabled.value = enabled
- if (enabled) {
- command { it.cameraStart() }
- cameraJob?.cancel()
- cameraJob = viewModelScope.launch(Dispatchers.IO) {
- repo?.cameraFrames()?.collect { _cameraFrame.value = it }
- }
- } else {
- cameraJob?.cancel()
- cameraJob = null
- _cameraFrame.value = null
- command { it.cameraStop() }
- }
- }
-
- override fun onCleared() {
- super.onCleared()
- cameraJob?.cancel()
- if (_cameraEnabled.value) {
- viewModelScope.launch(Dispatchers.IO) { repo?.cameraStop() }
- }
- }
-
- private fun command(block: (PrinterRepository) -> Unit) {
- viewModelScope.launch(Dispatchers.IO) { repo?.let(block) }
- }
-}
diff --git a/android/app/src/main/res/drawable/ic_launcher_background.xml b/android/app/src/main/res/drawable/ic_launcher_background.xml
deleted file mode 100644
index 65f94ad..0000000
--- a/android/app/src/main/res/drawable/ic_launcher_background.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
diff --git a/android/app/src/main/res/drawable/ic_launcher_foreground.xml b/android/app/src/main/res/drawable/ic_launcher_foreground.xml
deleted file mode 100644
index b9f1e2e..0000000
--- a/android/app/src/main/res/drawable/ic_launcher_foreground.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
deleted file mode 100644
index 6b78462..0000000
--- a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
deleted file mode 100644
index 6b78462..0000000
--- a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
diff --git a/android/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi/ic_launcher.xml
deleted file mode 100644
index 6b78462..0000000
--- a/android/app/src/main/res/mipmap-anydpi/ic_launcher.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
diff --git a/android/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
deleted file mode 100644
index 6b78462..0000000
--- a/android/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
diff --git a/android/app/src/main/res/values-night/themes.xml b/android/app/src/main/res/values-night/themes.xml
deleted file mode 100644
index c08b4c9..0000000
--- a/android/app/src/main/res/values-night/themes.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
deleted file mode 100644
index 99f41e2..0000000
--- a/android/app/src/main/res/values/strings.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
- KX-Bridge
-
diff --git a/android/app/src/main/res/values/themes.xml b/android/app/src/main/res/values/themes.xml
deleted file mode 100644
index c8eca6e..0000000
--- a/android/app/src/main/res/values/themes.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
diff --git a/android/build.gradle.kts b/android/build.gradle.kts
deleted file mode 100644
index a4cd58a..0000000
--- a/android/build.gradle.kts
+++ /dev/null
@@ -1,6 +0,0 @@
-plugins {
- id("com.android.application") version "8.13.2" apply false
- id("org.jetbrains.kotlin.android") version "2.0.0" apply false
- id("org.jetbrains.kotlin.plugin.compose") version "2.0.0" apply false
- id("org.jetbrains.kotlin.plugin.serialization") version "2.0.0" apply false
-}
diff --git a/android/gradle.properties b/android/gradle.properties
deleted file mode 100644
index f0a2e55..0000000
--- a/android/gradle.properties
+++ /dev/null
@@ -1,4 +0,0 @@
-org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
-android.useAndroidX=true
-kotlin.code.style=official
-android.nonTransitiveRClass=true
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
deleted file mode 100644
index 37f853b..0000000
--- a/android/gradle/wrapper/gradle-wrapper.properties
+++ /dev/null
@@ -1,7 +0,0 @@
-distributionBase=GRADLE_USER_HOME
-distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
-networkTimeout=10000
-validateDistributionUrl=true
-zipStoreBase=GRADLE_USER_HOME
-zipStorePath=wrapper/dists
diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts
deleted file mode 100644
index 930a847..0000000
--- a/android/settings.gradle.kts
+++ /dev/null
@@ -1,17 +0,0 @@
-pluginManagement {
- repositories {
- google()
- mavenCentral()
- gradlePluginPortal()
- }
-}
-dependencyResolutionManagement {
- repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
- repositories {
- google()
- mavenCentral()
- }
-}
-
-rootProject.name = "KX-Bridge"
-include(":app")