Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ android {
applicationId 'org.crabcore.u1control'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 18
versionName "1.2.8"
versionCode 19
versionName "3.0.0"

buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
}
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,19 @@ object EdgeInsets {
}
ViewCompat.requestApplyInsets(root)
}

/**
* Pads only the bottom of [view] by the nav-bar inset, on top of the padding it
* already has. For bottom sheets, which sit against the bottom edge but must
* keep their own top and side padding untouched.
*/
fun applyBottom(view: View) {
val base = view.paddingBottom
ViewCompat.setOnApplyWindowInsetsListener(view) { target, insets ->
val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
target.setPadding(target.paddingLeft, target.paddingTop, target.paddingRight, base + bars.bottom)
insets
}
ViewCompat.requestApplyInsets(view)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package org.crabcore.u1control.slicing

import android.content.Context

/**
* What the RN app knows about each physical filament slot beyond its colour:
* material, sub type, brand, and whether the machine reports it loaded.
*
* Colours stay in [FilamentSlotColors] because paint and mesh palettes need them
* on their own terms. This store carries the descriptive half so native screens —
* the print preprocess sheet especially — can label a lane exactly as the RN
* sheet does without re-deriving anything from Moonraker. RN resolves printer
* truth against saved settings and pushes both halves through
* [HelixSlicerModule.setFilamentSlots].
*/
object FilamentSlotDetails {
/** Mirrors RN `FilamentSlotDisplay`; [status] is a `FilamentSlotStatus` value. */
data class Slot(
val material: String,
val mainType: String,
val subType: String,
val brand: String,
val status: String,
) {
val isEmpty: Boolean get() = status == "empty"
}

private const val PREFS = "helix_filament_slot_details"
private const val COUNT = 4

/** Stand-in until RN has pushed. `unknown` reads as usable, never as empty. */
private val UNKNOWN = Slot(material = "PLA", mainType = "PLA", subType = "", brand = "", status = "unknown")

fun read(context: Context): List<Slot> {
val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
return (0 until COUNT).map { index ->
Slot(
material = prefs.getString("material$index", null) ?: UNKNOWN.material,
mainType = prefs.getString("mainType$index", null) ?: UNKNOWN.mainType,
subType = prefs.getString("subType$index", null) ?: UNKNOWN.subType,
brand = prefs.getString("brand$index", null) ?: UNKNOWN.brand,
status = prefs.getString("status$index", null) ?: UNKNOWN.status,
)
}
}

fun write(context: Context, slots: List<Slot>) {
val editor = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit()
for (index in 0 until COUNT) {
val slot = slots.getOrNull(index) ?: UNKNOWN
editor.putString("material$index", slot.material)
editor.putString("mainType$index", slot.mainType)
editor.putString("subType$index", slot.subType)
editor.putString("brand$index", slot.brand)
editor.putString("status$index", slot.status)
}
editor.apply()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package org.crabcore.u1control.slicing

import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.Base64
import java.io.File

/**
* Reads the largest thumbnail a slicer embedded in a .gcode header.
*
* Shared by the RN bridge ([HelixSlicerModule.getGcodeThumbnail]) and the native
* print preprocess sheet, so both show the render baked in at slice time rather
* than waiting on a Moonraker upload.
*/
object GcodeThumbnailReader {
private const val BEGIN = "; thumbnail begin"
private const val END = "; thumbnail end"

/** Base64 PNG of the largest embedded thumbnail, or null when there is none. */
fun readBase64(path: String): String? {
val file = File(path.removePrefix("file://"))
if (!file.exists()) return null

var bestArea = 0
var best: String? = null
var currentArea = 0
var current: StringBuilder? = null

return try {
file.bufferedReader().use { reader ->
while (true) {
val line = reader.readLine() ?: break
val trimmed = line.trim()
when {
trimmed.startsWith(BEGIN) -> {
val dims = trimmed.removePrefix(BEGIN).trim().substringBefore(' ').split('x')
currentArea = (dims.getOrNull(0)?.toIntOrNull() ?: 0) *
(dims.getOrNull(1)?.toIntOrNull() ?: 0)
current = StringBuilder()
}
trimmed.startsWith(END) -> {
val block = current
if (block != null && currentArea > bestArea) {
bestArea = currentArea
best = block.toString()
}
current = null
}
current != null && trimmed.startsWith(";") ->
current?.append(trimmed.removePrefix(";").trim())
// Thumbnails live in the header; stop once real motion starts.
best != null && (trimmed.startsWith("G1") || trimmed.startsWith("G0")) -> return@use
}
}
}
best
} catch (_: Throwable) {
null
}
}

fun readDataUri(path: String): String? = readBase64(path)?.let { "data:image/png;base64,$it" }

fun readBitmap(path: String): Bitmap? {
val encoded = readBase64(path) ?: return null
return try {
val bytes = Base64.decode(encoded, Base64.DEFAULT)
BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
} catch (_: Throwable) {
null
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.crabcore.u1control.slicing

/**
* The RN Cockpit palette ([components/dashboard/shared.tsx]) mirrored for native
* views.
*
* The native print preprocess sheet is a port of its RN counterpart and has to
* match it, so it uses these tokens instead of the app-wide [HelixAppTheme]
* ones — including the fixed accent, which the RN sheet also uses in place of
* the user's chosen accent colour. Both preprocess dialogs then look identical
* no matter which one a print flow reaches.
*/
object HelixCockpit {
const val BG = 0xFF0B0D10.toInt()
const val SURFACE = 0xFF15181D.toInt()
const val SURFACE_ALT = 0xFF1D222A.toInt()
const val BORDER = 0xFF252A33.toInt()
const val TEXT = 0xFFF4F7FB.toInt()
const val DIM = 0xFF8B95A1.toInt()
const val ACCENT = 0xFF00D4C8.toInt()
const val ACCENT_FILL = 0xFF00B3A9.toInt()
const val ON_ACCENT = 0xFF00201E.toInt()
const val SUCCESS = 0xFF31D583.toInt()
const val WARN = 0xFFFFB020.toInt()
const val DANGER = 0xFFFF5A5C.toInt()
const val SCRIM = 0x9E000000.toInt()

/** `P.radius` / `P.gap` from the RN palette. */
const val RADIUS = 20
const val GAP = 16

/** RN `alpha()` — the same colour at a new opacity. */
fun alpha(color: Int, fraction: Float): Int =
(color and 0x00FFFFFF) or (((fraction.coerceIn(0f, 1f) * 255f).toInt()) shl 24)
}
Loading