- Python 51.7%
- HTML 38.4%
- Kotlin 8.1%
- CSS 1.7%
| app | ||
| gradle | ||
| server | ||
| tests | ||
| .gitignore | ||
| build.gradle.kts | ||
| COPYRIGHT | ||
| gradle.properties | ||
| gradlew | ||
| gradlew.bat | ||
| LICENSE | ||
| README.md | ||
| settings.gradle.kts | ||
Merkliste
Listen- und Einkaufs-App mit Gedächtnis — Ersatz für FastShopping.
Android-WebView-App wie Finanzplan/Ruderdaten: die gesamte Oberfläche und Logik
steckt in app/src/main/assets/app.html, die Kotlin-Schale speichert nur
(filesDir/data.json) und regelt Backup-Export/-Import.
Stufe 1 – 3 sind umgesetzt und im Betrieb, Abgleich vom Telefon getestet. Dazu die Rezept-Leseansicht (v1.2.0) und die Aufgaben-Ausbaustufe (v1.3.0). Stufe 4 des Plans (Rezept-Import per URL, Kategorien, Wochenplanung) steht noch aus.
Was drin ist
- Drei Listenarten — Einkauf, Packliste, ToDo. Anlegen, umbenennen, archivieren, löschen, alles mit Rückgängig-Band.
- Listenansicht: Eingabefeld mit Autovervollständigung aus dem Katalog,
Abgehaktes rutscht ausgegraut nach unten, Menge als Freitext
(
Mehl – 500 gwird beim Tippen automatisch getrennt), Sortiermodus (↕), „Abgehaktes entfernen", „Liste zurücksetzen" (der Packlisten-Fall). - Katalog wächst automatisch: Was auf einer Einkaufsliste abgehakt wird, landet dort mit Kaufzahl und den letzten 10 Kauf-Zeitpunkten. Haken wieder weg = Buchung wieder weg.
- Vorschläge: „Häufig" (Kaufzahl, nach Aktualität gewichtet, Halbwertszeit 60 Tage) und „Länger nicht mehr gekauft" (ab 3 Käufen, Median-Abstand × 1,2).
- Vorlagen & Rezepte — Rezept (Zutaten + Zubereitung + Portionen), Packliste, Baustein. Suche über Name/Zutat/Zubereitung, Tag-Filter. „Hinzufügen aus …" mit Mehrfachauswahl; doppelte Namen werden bewusst nicht verrechnet, sondern stehen nebeneinander.
- Rezepte lesen statt versehentlich ändern: Ein Rezept öffnet sich als
Leseansicht (Zutaten, nummerierte Schritte, Tags, Quelle) mit „🍳 Kochmodus"
und „🛒 Zutaten einkaufen". Das Formular kommt erst über ✏️ — im Webclient
entsprechend über
?bearbeiten=1. - Kochmodus (🍳 im Rezept): groß lesbar, Bildschirm bleibt an, Zutaten und Schritte lassen sich beim Kochen abhaken (flüchtig, nicht im Rezept).
- Gemeinsames Kochbuch (v1.11.0, Server 1.13.0): Die Rezepte aller Konten stehen einander offen — nur zum Lesen. Zu finden über 👥 auf dem Vorlagen-Bildschirm oder im Hauptmenü. Von dort lassen sich die Zutaten direkt einkaufen (ohne das Rezept zu übernehmen), oder man übernimmt es und bekommt eine eigene Kopie mit Herkunftsvermerk; ändern geht nur an der eigenen. Ein eigenes Rezept lässt sich mit einem Tipp aus dem Kochbuch nehmen („nur für dich") — sichtbar ist der Standard. Der Bestand wird geblättert, nicht abgeglichen: fremde Rezepte kommen nie ins Gerät.
- Listen teilen (v1.10.0, Server 1.11.0): Eine eigene Liste lässt sich einzelnen Konten zum Mitpflegen geben — Listenmenü → „Mit jemandem teilen". Wer sie bekommt, darf eintragen, ändern und abhaken; löschen und weitergeben kann nur der Eigentümer, für alle anderen heißt der Menüpunkt „Nicht mehr mitmachen". Die Liste bleibt dabei im Konto des Eigentümers, es entsteht keine Kopie. Der Katalog bleibt persönlich: wer in einer fremden Liste abhakt, füttert seine eigenen Vorschläge, nicht die des anderen. Rezepte sind davon unberührt — die sind nur zum Lesen (Etappe B).
- Aufgaben (ToDo-Listen): Fälligkeit mit Uhrzeit, Priorität (hoch / normal /
niedrig), Wiederholung (täglich / wöchentlich / monatlich) und Erinnerung als
echte Android-Benachrichtigung. Offene Aufgaben stehen nach Fälligkeit
gebündelt (Überfällig / Heute / Morgen / Diese Woche / Später / Ohne Datum).
Die Schnelleingabe versteht Zusätze:
Müll rausbringen morgen 18:00 !(heute/morgen/übermorgen, Wochentag,31.12., Uhrzeit,!= hoch,?= niedrig). Eine wiederkehrende Aufgabe rückt beim Abhaken auf ihren nächsten Termin, statt zu verschwinden. - Erinnerungen prüfen (Einstellungen → Erinnerungen): sagt, warum eine
Erinnerung ausgeblieben ist. Die Ursache liegt immer im System, nie in der
App — Benachrichtigungen abgelehnt, Kanal „Aufgaben" stummgeschaltet, keine
genauen Wecker, Akku-Optimierung, „Nicht stören"; jeder Punkt führt direkt zum
richtigen Android-Dialog. Dazu ein Probe-Wecker („Testerinnerung in 15
Sekunden"), der denselben Weg geht wie eine echte Erinnerung. Wichtig: ab
targetSdk 34 erteilt Android
SCHEDULE_EXACT_ALARMnicht mehr von allein — ohne genaue Wecker schiebt der Standby-Eimer eine Meldung um bis zu 24 Stunden. Deshalb hat das Manifest seit 1.9.0USE_EXACT_ALARM(gilt ab Android 13 ohne Nachfrage und ist genau für Erinnerungs-Apps gedacht). - Aufgaben im Kalender (Einstellungen → Kalender): einen Zielkalender wählen, dann trägt die App jede Aufgabe, die dafür angehakt ist (📅 im Aufgaben-Dialog), dort ein — und nimmt sie wieder heraus, sobald sie erledigt, verschoben oder gelöscht ist. Am saubersten mit einem eigenen Kalender nur dafür, etwa einem über DAVx⁵ eingebundenen CalDAV-Kalender „Aufgaben"; Löschungen wandern dann von allein auf den Server. Die Kalender-Rechte fragt die App erst beim Einschalten; ohne sie bleibt alles wie vorher.
- Papierkorb (Menü ☰, am Rechner in der Kopfzeile): Gelöschtes bleibt 90 Tage liegen und lässt sich zurückholen — auch das, was ein anderes Gerät gelöscht hat. Ein Eintrag aus einer gelöschten Liste holt die Liste mit. Erledigte Listen gehören eher ins Archiv (Listenmenü → „Ins Archiv legen"), das räumt den Start auf, ohne etwas wegzuwerfen. Mit dem letzten Haken einer Liste bietet die App das Archivieren von selbst an — ein Band unten, zwölf Sekunden lang; am Rechner steht der Vorschlag unter der fertigen Liste.
- Backup als JSON-Datei; beim Einspielen wahlweise ersetzen oder zusammenführen (dieselbe Regel wie beim Abgleich).
- Abgleich mit dem eigenen Server (Einstellungen → Abgleich): Adresse und Token eintragen, danach läuft es von allein. Ohne Eintrag bleibt alles auf dem Gerät. Serverteil und Rechner-Oberfläche: server/. Alles neu abgleichen schickt und holt einmal den kompletten Bestand — nötig, wenn Gerät und Server sich über einen Datensatz uneinig sind, den seither niemand mehr angefasst hat (im normalen Abgleich kommt der nie wieder zur Sprache). Nach jedem App-Update passiert das einmal von selbst.
Datenmodell
Ein JSON-Dokument mit listen[], vorlagen[], katalog[]. Sync-Vorbereitung
von Anfang an: jede Entität hat UUID, updated_at, device_id; Löschen setzt
deleted_at (Grabstein, wird nach 90 Tagen beim Start aufgeräumt) — solange
steht der Eintrag im Papierkorb.
Fensterschale: Tastatur und Systemleisten (v1.6.4)
Seit targetSdk 35 (Android 15) zeichnet die App zwangsweise edge-to-edge unter
Status- und Navigationsleiste, und android:windowSoftInputMode="adjustResize"
verkleinert das Fenster nicht mehr, wenn die Tastatur kommt. MainActivity.bauLayout()
regelt das deshalb selbst:
- WebView steckt in einem
FrameLayout,WindowCompat.setDecorFitsSystemWindows(window, false)— damit gilt auf allen Android-Versionen derselbe Weg. - Der Insets-Listener auf der Wurzel setzt
systemBars()undime()als Ränder (layoutParams.topMarginusw., untenmaxOf(Tastatur, Navigationsleiste)) auf die WebView und gibtWindowInsetsCompat.CONSUMEDzurück. Der untere Rand hält auch den „+ Neue Liste"-Knopf von den Navigationssymbolen frei. - Darüber liegt ein
View-Streifen in Markenfarbe mit Höhe = oberer Inset, sonst wirkt die Statusleiste abgeschnitten.
Wichtig, nicht zurückdrehen: Der Abstand muss über die Ränder laufen. Padding auf
die WebView (so der erste Anlauf in v1.6.2) beschneidet den Inhalt nur — die Seite bricht
nicht neu um, 100vh/position:fixed/scrollbare Blätter bleiben hinter der Tastatur, und
etwa die Server-Adresse musste man blind eintippen.
Die Seite zieht mit: unter der Bridge setzt sie --safe-t/--safe-b auf 0
(sonst zählt der Abstand doppelt), rückt bei focusin das beschriebene Feld per
feldInDenBlick() in den Blick und legt body.gesperrt (position:fixed mit gemerkter
Scrollposition) an, solange ein Blatt offen ist — sonst scrollt die Seite unter dem
offenen Menü weg. go() schließt beim Seitenwechsel ein offenes Blatt mit.
Dunkelmodus (v1.8.0)
Einstellungen → Darstellung → Erscheinungsbild: Folgt dem System · Immer hell · Immer dunkel. Drei Stellen greifen ineinander:
- Seite: ein kurzes Skript im
<head>löst die Wahl auf und setztdata-theme="hell|dunkel"am<html>— noch vor dem ersten Malen, sonst blitzt die helle Seite auf. Das CSS kennt nur noch dieses Attribut. Das Blau der Marke bekommt im Dunkeln ein aufgehelltes Geschwister--accent, weil es als Schrift auf fast schwarzem Grund untergeht. - Schale: ohne
WebSettingsCompat.setAlgorithmicDarkeningAllowed(…, true)meldet die WebView seit targetSdk 33+ immer „hell" — der Dunkelmodus wäre auf dem Gerät nie zu sehen.<meta name="color-scheme" content="light dark">sagt Android zugleich, dass die Seite ihren eigenen mitbringt und nichts nachgedunkelt werden muss. - Merken:
Android.setTheme(pref)legt die Wahl in den SharedPreferences ab;MainActivity.onCreatesetzt daraussetDefaultNightMode, damit Fensterhintergrund und Systemleisten passen. Zur Laufzeit wird nur der Fensterhintergrund umgefärbt — ein Moduswechsel würde die Activity neu bauen und die offene Liste mitreißen.
ml_theme liegt in localStorage (Gerätesache, nicht im Abgleich).
Installieren
APK per adb install oder direkt auf dem Gerät. Beim Sprung von v1.0.x auf
v1.1.x einmal deinstallieren statt zu aktualisieren: v1.1.0 hat die
INTERNET-Berechtigung nachgerüstet, und nachgerüstete Berechtigungen greifen bei
einem Update über eine bestehende Installation nicht — der Abgleich meldet dann
nur „Kein Kontakt zum Server". Vorher über Einstellungen → Backup sichern, die
Daten überleben das Deinstallieren nicht. Innerhalb von v1.1.x sind normale
Updates in Ordnung.
Bauen und testen
JAVA_HOME=/usr/lib/jvm/java-17-temurin-jdk ./gradlew assembleDebug
# → app/build/outputs/apk/debug/app-debug.apk
python3 tests/test_merkliste.py # 44 Durchklick-Checks (Playwright, ohne Gerät)
python3 tests/test_gruppen.py # Unterpunkte
python3 tests/test_vorrat.py # Katalog-Vorrat „hab ich immer da"
python3 tests/test_zutaten.py # Zutaten mit Menge, Zwischenüberschriften
python3 tests/test_theme.py # Dunkelmodus (System/Hell/Dunkel)
python3 tests/test_sync.py # 26 Checks: App gegen echten Server
python3 tests/test_teilen_e2e.py # geteilte Listen, zwei Konten, zwei Geräte
Die Web-App läuft ohne Android: app.html direkt im Browser öffnen, dann wird
statt der Bridge localStorage benutzt.
Release: assembleRelease bauen, das APK mit dem eigenen Schlüssel signieren
(zipalign + apksigner). versionCode dabei immer hochzählen, sonst bietet
F-Droid kein Update an.