No description
  • Kotlin 93.8%
  • AIDL 5.4%
  • Shell 0.8%
Find a file
Rune Jensen 9fcb799c3f
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/tag/build Pipeline was successful
Make price optional so receipts double as shopping lists
A line's unitPriceOre is now nullable. A price-less line prints as just its name
(with a "N × " prefix when quantity > 1) and is excluded from the total; the
"I ALT" total line is omitted entirely when no line is priced. Priced and
plain lines can be mixed — the total then reflects only the priced ones.

Quantity now defaults to 1 when blank, and a line only needs a name to count.
The editor hides the total for an unpriced list and marks the price field
optional; the receipts list shows an item count instead of "0,00 kr" for such
lists. So you can name a store "Groceries" and just fill in what to buy.

The nullable field defaults to null, so existing saved receipts (which always
had a price) deserialize unchanged.

Bumps version to 0.1.4 (versionCode 5).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UtdyPTKt54Zs3H1u1vMsMC
2026-07-23 17:39:51 +00:00
.woodpecker CI: drop the debug-APK build step on push 2026-07-22 19:35:35 +00:00
app Make price optional so receipts double as shopping lists 2026-07-23 17:39:51 +00:00
gradle Initial commit: Labellars — Sunmi receipt generator 2026-07-22 18:51:39 +00:00
.gitignore Initial commit: Labellars — Sunmi receipt generator 2026-07-22 18:51:39 +00:00
build.gradle.kts Initial commit: Labellars — Sunmi receipt generator 2026-07-22 18:51:39 +00:00
build.sh Initial commit: Labellars — Sunmi receipt generator 2026-07-22 18:51:39 +00:00
gradle.properties Initial commit: Labellars — Sunmi receipt generator 2026-07-22 18:51:39 +00:00
gradlew Initial commit: Labellars — Sunmi receipt generator 2026-07-22 18:51:39 +00:00
gradlew.bat Initial commit: Labellars — Sunmi receipt generator 2026-07-22 18:51:39 +00:00
logo.png Initial commit: Labellars — Sunmi receipt generator 2026-07-22 18:51:39 +00:00
README.md CI: drop the debug-APK build step on push 2026-07-22 19:35:35 +00:00
settings.gradle.kts Initial commit: Labellars — Sunmi receipt generator 2026-07-22 18:51:39 +00:00

Labellars

A small offline Android app for a Sunmi handheld with a built-in thermal receipt printer. Define reusable store profiles (name + header + footer), compose receipts as antal × vare @ enhedspris lines with an auto-summed total in Danish / DKK, and print them — either to the Sunmi built-in printer or a paired Bluetooth ESC/POS printer. Everything can be exported/imported as a single JSON file, so you can make (obviously fake, just-for-fun) receipts and reuse them later.

Built as a sibling of trafikapp: same toolchain, folder-per-feature MVVM, kotlinx.serialization, dynamic-color Material 3, Danish formatting, Docker-only builds and Woodpecker CI.

Screens

  • Kvitteringer — saved receipts: create, open/edit, duplicate, print, delete.
  • Butikker — store profiles: name + multi-line header (address, CVR, …) and footer ("Tak for besøget", …).
  • Kvittering-editor — pick a store, add product lines (antal / vare / pris), live total + a paper-accurate preview, optional custom date/time and note, then Gem or Gem & print.
  • Indstillinger — choose the printer backend (Sunmi / Bluetooth), pick a paired Bluetooth device, run a test print, and export/import all data.

Building & running

Everything runs in Docker, so no local Android toolchain is needed:

./build.sh                  # unit tests + debug APK (default)
./build.sh test             # just the unit tests
./build.sh assembleDebug    # just the APK -> app/build/outputs/apk/debug/
./build.sh assembleRelease  # R8-minified release APK -> app/build/outputs/apk/release/

Unit tests are plain JVM (JUnit over the pure receipt renderer, money formatting and export/import) — no emulator. assembleRelease works locally unsigned (produces app-release-unsigned.apk); signed APKs are only built in CI on tags.

Install the debug APK with adb install app/build/outputs/apk/debug/app-debug.apk.

The receipt printer

The app talks to a printer through a ReceiptPrinter abstraction (app/src/main/kotlin/dk/jaduer/labellars/printer/) with two backends, chosen in Indstillinger:

  • Sunmi (built-in) — binds the on-device Sunmi inner-printer AIDL service (woyou.aidlservice.jiuiv5) and drives it with typed text/alignment calls. The AIDL interface is vendored under app/src/main/aidl/woyou/aidlservice/jiuiv5/.

    ⚠️ AIDL binder transaction codes depend on method declaration order, so that file must match the interface the device's printer service actually exposes. It reproduces Sunmi's long-stable public interface; if printing ever misbehaves on new firmware, drop in the official IWoyouService.aidl from Sunmi's current SDK (same path) — it needs no code changes for the subset used.

  • Bluetooth (ESC/POS) — connects an RFCOMM (SPP) socket to a paired printer and streams ESC/POS bytes (EscPos.kt). Requires the BLUETOOTH_CONNECT / BLUETOOTH_SCAN runtime permissions (requested from Settings). Danish glyphs (æøåÆØÅ) use the WPC1252 code page.

Layout is rendered once, backend-agnostically, by ReceiptRenderer into a list of ReceiptElements (the same list feeds both backends and the on-screen preview). Default paper is 58 mm / 32 columns.

Data & backup

Stores and receipts are persisted as JSON files in the app's private storage (stores.json / receipts.json) via a small JsonStore helper — no database. Money is stored as integer øre so sums are exact. Each saved receipt keeps a snapshot of its store, so it stays printable even after the store is edited or deleted.

Indstillinger → Sikkerhedskopi exports an ExportBundle (all stores + receipts) to a JSON file via the Storage Access Framework, and imports one back (merging by id). The exported format is exactly the on-disk format.

Installing

Signed APKs are attached to Forgejo releases at https://code.jaduer.dk on every version tag; install directly or via Obtainium (auto-detects Forgejo/Gitea releases). No Play Store.

Release / signing (maintainers)

Generate the keystore once:

keytool -genkeypair -v -keystore labellars-release.jks -alias labellars -keyalg RSA -keysize 4096 -validity 10000

Then set these repo secrets in the Woodpecker UI (repo → Secrets):

Secret Value
labellars_keystore base64 -w0 labellars-release.jks
labellars_keystore_password keystore password
labellars_key_alias labellars
labellars_key_password signing key password
gitea_token Forgejo token with release write access

Push to main runs the unit tests only; pushing a tag builds a signed labellars-<tag>.apk and attaches it to the matching Forgejo release (.woodpecker/build.yaml). The release signing config in app/build.gradle.kts is only attached when LABELLARS_KEYSTORE is set, so local release builds stay unsigned rather than failing.

Stack

Native Kotlin + Jetpack Compose (Material 3), minSdk 33, targetSdk/compileSdk 36. Compose BOM, Navigation, Lifecycle/ViewModel, DataStore (settings), kotlinx.serialization + kotlinx.coroutines. JUnit for the plain-JVM tests. No networking, no Room, no DI framework.