21 Commits

Author SHA1 Message Date
gary
7bd109b728 docs(qa): mark P1 issues 1-3 as resolved 2026-02-24 21:43:30 +01:00
gary
1a21bc18bb fix(flow): guard stepper and reset state on image-less project load 2026-02-24 21:43:28 +01:00
gary
fd72d53d2a feat(project): persist catalog snapshot in project data 2026-02-24 21:43:25 +01:00
gary
25d570c779 docs(qa): add static QA issue review for stepper/save-load/export flows 2026-02-24 21:34:56 +01:00
gary
df7406b494 Add independent review for changes since 814705c 2026-02-24 21:34:30 +01:00
gary
ab7aa625e6 Implement 4-step mosaic flow, robust project autosave, and JSON export 2026-02-24 21:30:33 +01:00
gary
814705cac6 feat(projects): add Projects tab with load/delete list 2026-02-23 08:25:06 +01:00
gary
3651b073bf feat(cap-detect): show top 3 dominant colors + mixed average 2026-02-22 18:40:33 +01:00
gary
b553c29d39 feat(mosaic): add zoom/pan source preview and project save/load 2026-02-22 16:09:38 +01:00
gary
84d649ac6d fix(catalog): prevent grid card overflow on mobile 2026-02-22 12:22:34 +01:00
gary
75a3a8dd60 feat(ui): frutiger aero redesign for mosaic and catalog flows 2026-02-22 03:34:07 +01:00
gary
9edaef23ff Separate catalog screen and show photo+color together 2026-02-22 02:01:45 +01:00
gary
4cc7de158e Fix camera review flow with dedicated route and lost-data recovery 2026-02-21 23:17:48 +01:00
gary
94eec93e13 Fix cap photo flow to always open review dialog with fallback 2026-02-21 22:51:44 +01:00
gary
e392b99bef Add interactive manual circle drag/pinch with live color preview 2026-02-21 22:46:35 +01:00
gary
b82d9a03e8 Fix rear camera default and robust cap color correction workflow 2026-02-21 21:03:38 +01:00
gary
448d9dc649 Improve cap photo color extraction with circle masking 2026-02-21 20:54:34 +01:00
gary
eb37322809 Add persistent cap catalog with image/name/color management 2026-02-21 20:45:33 +01:00
gary
2e0da448ba Add camera cap capture flow with center color detection 2026-02-21 20:37:18 +01:00
gary
4cbd4eb478 Add style controls, presets, and edge-aware variation quantization 2026-02-21 20:28:04 +01:00
gary
a00d456d03 Optimize mosaic generation and preview performance 2026-02-21 20:12:57 +01:00
11 changed files with 3985 additions and 252 deletions

View File

@@ -2,56 +2,67 @@
Prototype Flutter app for generating bottle-cap mosaics from imported images. Prototype Flutter app for generating bottle-cap mosaics from imported images.
## Implemented MVP ## Features
- Import target image from gallery (`image_picker`) - Import target image from gallery (`image_picker`)
- Resolution controls: - Resolution controls:
- explicit grid width/height - explicit grid width/height
- or auto grid by approximate cap size in source image pixels - or auto grid by approximate cap size in source image pixels
- Cap palette management: - Persistent **Cap Catalog** (local JSON in app documents directory):
- list caps with name + color - each entry stores `name`, `color` (hex/swatch), and optional preview image path
- add color via picker and/or manual hex - survives app restarts
- remove caps - Catalog management:
- Mosaic generation: - add entries manually (name + hex/color picker + optional photo)
- resize source to grid - **Deckel fotografieren**: robust circular cap detection (edge-based circle search with fallback), color is computed only from masked cap interior pixels
- nearest cap color match using CIELAB + DeltaE (CIE76) - selectable extraction mode in photo review dialog: **Dominante Farbe** or **Durchschnitt**
- fallback concept is RGB distance, but LAB path is implemented directly - dedicated catalog browser with **list/grid** modes
- Output: - edit existing entry name/color
- mosaic grid preview - delete entries (with thumbnail cleanup)
- bill of materials counts per cap color - Mosaic preview + bill of materials counts per cap color
- Mosaic palette source is always the current catalog entries
## Current blocker on this machine ## Style controls (new)
`flutter` SDK is not installed (`flutter: command not found`), so I could not run: User-facing controls are integrated directly in the main screen:
- `flutter create` - **Fidelity ↔ Structure** slider
- `flutter pub get` - fidelity side keeps direct color faithfulness
- `flutter analyze` - structure side applies edge-aware smoothing for cleaner large forms
- `flutter build apk --debug` - **Dithering strength** slider
- scales FloydSteinberg error diffusion
- **Edge emphasis** slider
- boosts edge readability during color assignment
- **Color tolerance / variation** slider
- allows controlled variation among similar cap colors in flatter regions
## Setup commands (Ubuntu/Debian) ## Presets
Three presets are provided and selectable via segmented buttons:
- **Realistisch**
- **Ausgewogen**
- **Künstlerisch**
Selecting a preset sets all four style controls at once.
## Pipeline notes
The generation pipeline still uses fast CIELAB (`DeltaE CIE76`) nearest-color matching,
with precomputed palette LAB values, LAB caching, and low-cost edge-aware processing to keep performance responsive.
## Build (arm64 split debug)
```bash ```bash
cd /home/yadciel/.openclaw/workspace
sudo snap install flutter --classic
# OR: install manually from flutter.dev and add to PATH
flutter doctor
cd /home/yadciel/.openclaw/workspace/korken_mosaic cd /home/yadciel/.openclaw/workspace/korken_mosaic
flutter create .
flutter pub get flutter pub get
flutter run flutter build apk --debug --split-per-abi
flutter build apk --debug
``` ```
Expected APK artifact: Expected arm64 artifact:
`build/app/outputs/flutter-apk/app-debug.apk` `build/app/outputs/flutter-apk/app-arm64-v8a-debug.apk`
## Project files ## Main files
- `lib/main.dart` complete MVP UI + mosaic logic - `lib/main.dart` UI + mosaic logic
- `pubspec.yaml` dependencies - `README.md` overview and build instructions
- `analysis_options.yaml`
- `.gitignore`

View File

@@ -0,0 +1,47 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.CAMERA" />
<application
android:label="korken_mosaic"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

136
docs/qa/issues-20260224.md Normal file
View File

@@ -0,0 +1,136 @@
# QA Issues 2026-02-24
Scope: Agentische, statische QA-Review (Flutter/Dart CLI lokal nicht verfügbar: `flutter` fehlt). Fokus auf 4-Step-Flow, Save/Load/Delete, JSON-Export und UX-Kantenfälle.
## Issue 1 (P1) 4-Step-Flow lässt Fortschritt ohne Pflichtdaten zu ✅ ERLEDIGT (feat/mosaic-stepper-export)
**Bereich:** 4-Step-Flow
**Beobachtung**
- Im `Stepper` kann man per `Weiter`/Tap bis Schritt 4 springen, auch ohne Bildauswahl.
- Auf Schritt 4 ist „Generate Mosaic“ aktiv, aber `_generate()` bricht stillschweigend ab, wenn kein Bild vorhanden ist.
**Code-Hinweis**
- `lib/main.dart:1068-1127` (`Stepper` ohne Guard/Validation)
- `lib/main.dart:1250-1255` (Generate-Button nur an `_isGenerating` gebunden)
- `lib/main.dart:884`ff (`_generate()` returnt bei `_sourceImageBytes == null` ohne User-Feedback)
**Repro-Schritte**
1. App öffnen (kein Bild geladen).
2. Im Mosaic-Stepper mehrfach „Weiter“ klicken oder direkt auf Schritt 4 tippen.
3. „Generate Mosaic“ klicken.
4. Es passiert visuell nichts (kein Ergebnis, kein Hinweis).
**Akzeptanzkriterium**
- Schritt-Navigation ist zustandsbasiert: Schritt 2-4 erst zugänglich, wenn notwendige Voraussetzungen erfüllt sind.
- „Generate Mosaic“ ist deaktiviert, solange kein Bild geladen ist.
- Alternativ/zusätzlich: verständliche Fehlermeldung (Snackbar), falls Generierung ohne Bild ausgelöst wird.
---
## Issue 2 (P1) Projekt-Load mit leerem Bild leert bestehenden Zustand nicht ✅ ERLEDIGT (feat/mosaic-stepper-export)
**Bereich:** Save/Load-Flow
**Beobachtung**
- Beim Laden werden `_sourceImageBytes` und `_result` nur gesetzt, wenn `data.sourceImageBytes != null`.
- Lädt man ein Projekt ohne Bild, bleibt ggf. ein altes Bild/Ergebnis aus vorherigem Zustand erhalten.
**Code-Hinweis**
- `lib/main.dart:308-313` (fehlender `else`-Zweig zum expliziten Zurücksetzen)
**Repro-Schritte**
1. Projekt A mit Bild laden/generieren.
2. Projekt B laden, das ohne Bild gespeichert wurde (oder manuell erstellt wurde).
3. Erwartung: leerer/initialer Zustand.
4. Ist-Zustand: vorheriges Bild kann bestehen bleiben.
**Akzeptanzkriterium**
- Beim Laden eines Projekts ohne `sourceImageBytes` wird Zustand explizit zurückgesetzt (`_sourceImageBytes = null`, `_result = null`, ggf. Step auf 1).
- UI zeigt konsistent den tatsächlich geladenen Projektzustand.
---
## Issue 3 (P1) Projekt-Snapshots sind nicht reproduzierbar, da Katalog nicht versioniert wird ✅ ERLEDIGT (feat/mosaic-stepper-export)
**Bereich:** Save/Load-Flow + JSON-Export
**Beobachtung**
- Gespeicherte Projekte enthalten Parameter + Bild, aber **keinen Snapshot des verwendeten Farb-Katalogs**.
- Beim Laden wird mit aktuellem globalen `_catalog` neu generiert → Ergebnis kann von ursprünglichem Snapshot abweichen.
**Code-Hinweis**
- `lib/project_codec.dart` (`MosaicProjectData` ohne Katalogdaten)
- `lib/main.dart:316` (`_generate()` nach Load nutzt aktuellen `_catalog`)
**Repro-Schritte**
1. Mit Katalog-Set A ein Projekt erzeugen/speichern.
2. Katalogfarben ändern (hinzufügen/löschen/umfärben).
3. Projekt laden.
4. Ergebnisfarben/Zuordnung unterscheiden sich vom ursprünglichen Stand.
**Akzeptanzkriterium**
- Projektspeicherung enthält eine Palette/Katalog-Snapshot-Version (mind. Name + Farbe pro Eintrag).
- Load nutzt standardmäßig den gespeicherten Snapshot (mit klarer UX bei Konflikten/Optionen).
- Reproduzierbarkeit des Mosaiks ist gewährleistet.
---
## Issue 4 (P2) Export JSON nicht klar reproduzierbar ohne Ergebnis
**Bereich:** JSON-Export
**Beobachtung**
- Export enthält `project` immer, `result` aber nur falls bereits generiert.
- Wenn vor Export keine Generierung lief, fehlt Kerninformation (`assignments`/`palette`) ohne klaren Nutzerhinweis.
**Code-Hinweis**
- `lib/main.dart:378-392`
**Repro-Schritte**
1. Bild laden, aber nicht generieren.
2. „Export JSON“ klicken.
3. Exportdatei enthält kein `result`-Objekt.
**Akzeptanzkriterium**
- UX-Entscheidung explizit umsetzen:
- entweder Export-Button nur mit vorhandenem Ergebnis aktivieren, oder
- vor Export automatisch generieren, oder
- deutlichen Hinweisdialog anzeigen („Export ohne Ergebnisdaten“).
---
## Issue 5 (P3) „Fertig“-CTA im letzten Step ohne klaren Effekt
**Bereich:** 4-Step-Flow UX
**Beobachtung**
- Im letzten Step zeigt der Continue-Button „Fertig“, führt aber funktional zu keiner sichtbaren Aktion.
**Code-Hinweis**
- `lib/main.dart:1071-1093` (`onStepContinue` erhöht nur bis max. letztem Step)
**Repro-Schritte**
1. Bis Schritt 4 navigieren.
2. „Fertig“ klicken.
3. Kein Abschluss-Feedback/State-Change.
**Akzeptanzkriterium**
- Letzter CTA hat klare Bedeutung (z. B. „Generieren“, „Abschließen“, „Zum Export“) oder wird im letzten Step ausgeblendet.
- Nutzer erhält eindeutiges Abschlussfeedback.
---
## Issue 6 (P3) Lösch-Flow ohne Hinweis auf betroffenen Arbeitsstand
**Bereich:** Delete-Flow UX
**Beobachtung**
- Beim Löschen eines Snapshots gibt es nur Dateiname + Bestätigung.
- Kein Hinweis, ob gerade geladener Stand betroffen ist bzw. wie sich das auf „Letzten Stand laden“ auswirkt.
**Code-Hinweis**
- `lib/main.dart:333-367`
**Repro-Schritte**
1. Snapshot laden.
2. In Projekte denselben Snapshot löschen.
3. Nutzer bleibt ohne Kontext, ob aktiver Stand/Latest-Verhalten beeinflusst ist.
**Akzeptanzkriterium**
- Dialog/Feedback benennt Auswirkungen klar (z. B. „Aktuell geladener Zustand bleibt im Speicher bis Wechsel/Neustart“).
- Optional: Markierung des aktuell geladenen Snapshots in der Liste.

View File

@@ -0,0 +1,52 @@
# Code Review Änderungen von `814705c` bis `HEAD` (`ab7aa62`)
## 1) High-risk Findings
### Keine blocker-kritischen Defekte gefunden
Ich habe im betrachteten Commit keine eindeutigen Crash-/Datenverlust-Blocker gefunden, die einen sofortigen Release-Stopp erzwingen.
> Hinweis: Es gibt aber eine **relevante funktionale Regression** (siehe Nitpicks #1), die je nach Produktanforderung als Go/No-Go-Kriterium gewertet werden kann.
---
## 2) Nitpicks / Verbesserungen
1. **Funktionale Regression in der UI (ehemals 4 Style-Regler, jetzt nur noch 1 sichtbar)**
In `lib/main.dart` zeigt `_buildColorStep()` nur noch den Slider `fidelityStructure`.
Die bisherigen Feineinstellungen `ditheringStrength`, `edgeEmphasis`, `colorVariation` sind weiter im State/Codec vorhanden, aber nicht mehr direkt im UI editierbar.
**Impact:** Nutzer verlieren granulare Qualitätskontrolle; gespeicherte Werte bleiben zwar erhalten, sind aber kaum noch aktiv manipulierbar.
2. **Stepper-Logik ohne Validierung / Guidance**
`onStepContinue` lässt das Voranschreiten ohne notwendige Preconditions zu (z. B. ohne Bildauswahl bis Schritt 4). Das ist technisch robust (Generate guarded), aber UX-seitig irreführend.
**Vorschlag:** Continue-Button je Schritt konditional deaktivieren oder klare Inline-Hinweise anzeigen.
3. **`Fertig`-Button im letzten Step ohne echte Aktion**
In `controlsBuilder` wird im letzten Schritt der Label-Text auf `Fertig` gesetzt, aber `onStepContinue` führt dort effektiv nichts mehr aus.
**Vorschlag:** Auf letzter Stufe Button ausblenden oder mit sinnvoller Aktion belegen (z. B. Export, Speichern, Zur Projektliste).
4. **Projekt-Ladesemantik setzt immer auf Step `result`**
Beim Laden mit Bild springt der Flow direkt auf `MosaicFlowStep.result`. Das ist für „Quick resume“ okay, nimmt aber ggf. den Guided-Flow-Charakter.
**Vorschlag:** Optionales Verhalten (z. B. Restore des letzten Steps oder Konfiguration `resumeAtResult`).
5. **Testabdeckung für neue Kernpfade noch dünn**
Es gibt gute Basis-Tests (`project_codec_test`, einfacher Stepper-Smoke-Test), aber es fehlen Tests für:
- latest/autosave-Verhalten (`latest_project.json`),
- manuell vs. automatisch speichern,
- Export-JSON-Struktur inkl. Result-Payload,
- Laden aus Snapshot vs. latest.
---
## 3) Go/No-Go Empfehlung
**Empfehlung: GO mit Auflagen (kein Hard No-Go).**
Begründung:
- Die neuen Features (4-Step-Flow, autosave/latest, JSON-Export, Bestätigungsdialog beim Löschen) sind grundsätzlich sinnvoll umgesetzt.
- Kein klarer, reproduzierbarer Blocker im Diff erkennbar.
- Vor Release sollten jedoch mindestens die UX-/Funktionsregressionspunkte (insb. fehlende 3 Style-Regler) bewusst entschieden/fixiert werden.
**Release-Auflagen (kurz):**
1. Entscheiden/fixen, ob die 3 fehlenden Style-Regler absichtliche Scope-Reduktion oder Regression sind.
2. Stepper-UX (Continue/Finish-Verhalten) konsistenter machen.
3. 23 gezielte Tests für Save/Load/Export-Pfade ergänzen.

72
ios/Runner/Info.plist Normal file
View File

@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Korken Mosaic</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>korken_mosaic</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>Die Kamera wird verwendet, um einen Flaschendeckel für die Farbpalette zu fotografieren.</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

File diff suppressed because it is too large Load Diff

95
lib/project_codec.dart Normal file
View File

@@ -0,0 +1,95 @@
import 'dart:convert';
import 'dart:typed_data';
class MosaicPaletteSnapshotEntry {
final String name;
final int colorValue;
const MosaicPaletteSnapshotEntry({
required this.name,
required this.colorValue,
});
Map<String, dynamic> toJson() => {
'name': name,
'colorValue': colorValue,
};
factory MosaicPaletteSnapshotEntry.fromJson(Map<String, dynamic> json) {
return MosaicPaletteSnapshotEntry(
name: json['name'] as String? ?? 'Unbenannt',
colorValue: (json['colorValue'] as num?)?.toInt() ?? 0xFF000000,
);
}
}
class MosaicProjectData {
final bool useCapSize;
final String gridWidth;
final String gridHeight;
final String capSize;
final double fidelityStructure;
final double ditheringStrength;
final double edgeEmphasis;
final double colorVariation;
final String selectedPreset;
final Uint8List? sourceImageBytes;
final List<MosaicPaletteSnapshotEntry> catalogSnapshot;
final DateTime savedAt;
const MosaicProjectData({
required this.useCapSize,
required this.gridWidth,
required this.gridHeight,
required this.capSize,
required this.fidelityStructure,
required this.ditheringStrength,
required this.edgeEmphasis,
required this.colorVariation,
required this.selectedPreset,
required this.sourceImageBytes,
required this.catalogSnapshot,
required this.savedAt,
});
Map<String, dynamic> toJson() => {
'useCapSize': useCapSize,
'gridWidth': gridWidth,
'gridHeight': gridHeight,
'capSize': capSize,
'fidelityStructure': fidelityStructure,
'ditheringStrength': ditheringStrength,
'edgeEmphasis': edgeEmphasis,
'colorVariation': colorVariation,
'selectedPreset': selectedPreset,
'sourceImageBase64':
sourceImageBytes == null ? null : base64Encode(sourceImageBytes!),
'catalogSnapshot': catalogSnapshot.map((entry) => entry.toJson()).toList(),
'savedAt': savedAt.toIso8601String(),
};
factory MosaicProjectData.fromJson(Map<String, dynamic> json) {
final sourceB64 = json['sourceImageBase64'] as String?;
final snapshotRaw = (json['catalogSnapshot'] as List?) ?? const [];
return MosaicProjectData(
useCapSize: json['useCapSize'] as bool? ?? false,
gridWidth: json['gridWidth'] as String? ?? '40',
gridHeight: json['gridHeight'] as String? ?? '30',
capSize: json['capSize'] as String? ?? '12',
fidelityStructure: (json['fidelityStructure'] as num?)?.toDouble() ?? 0.5,
ditheringStrength:
(json['ditheringStrength'] as num?)?.toDouble() ?? 0.35,
edgeEmphasis: (json['edgeEmphasis'] as num?)?.toDouble() ?? 0.4,
colorVariation: (json['colorVariation'] as num?)?.toDouble() ?? 0.3,
selectedPreset: json['selectedPreset'] as String? ?? 'ausgewogen',
sourceImageBytes: sourceB64 == null ? null : base64Decode(sourceB64),
catalogSnapshot: snapshotRaw
.whereType<Map>()
.map((entry) => MosaicPaletteSnapshotEntry.fromJson(
Map<String, dynamic>.from(entry)))
.toList(growable: false),
savedAt:
DateTime.tryParse(json['savedAt'] as String? ?? '') ?? DateTime.now(),
);
}
}

578
pubspec.lock Normal file
View File

@@ -0,0 +1,578 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
archive:
dependency: transitive
description:
name: archive
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
url: "https://pub.dev"
source: hosted
version: "4.0.9"
async:
dependency: transitive
description:
name: async
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "https://pub.dev"
source: hosted
version: "2.13.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.1"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
code_assets:
dependency: transitive
description:
name: code_assets
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
cross_file:
dependency: transitive
description:
name: cross_file
sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937"
url: "https://pub.dev"
source: hosted
version: "0.3.5+2"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.7"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
url: "https://pub.dev"
source: hosted
version: "1.0.8"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
ffi:
dependency: transitive
description:
name: ffi
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
file_selector_linux:
dependency: transitive
description:
name: file_selector_linux
sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
url: "https://pub.dev"
source: hosted
version: "0.9.4"
file_selector_macos:
dependency: transitive
description:
name: file_selector_macos
sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
url: "https://pub.dev"
source: hosted
version: "0.9.5"
file_selector_platform_interface:
dependency: transitive
description:
name: file_selector_platform_interface
sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85"
url: "https://pub.dev"
source: hosted
version: "2.7.0"
file_selector_windows:
dependency: transitive
description:
name: file_selector_windows
sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
url: "https://pub.dev"
source: hosted
version: "0.9.3+5"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_colorpicker:
dependency: "direct main"
description:
name: flutter_colorpicker
sha256: "969de5f6f9e2a570ac660fb7b501551451ea2a1ab9e2097e89475f60e07816ea"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1
url: "https://pub.dev"
source: hosted
version: "2.0.33"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.3"
hooks:
dependency: transitive
description:
name: hooks
sha256: "7a08a0d684cb3b8fb604b78455d5d352f502b68079f7b80b831c62220ab0a4f6"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
http:
dependency: transitive
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev"
source: hosted
version: "1.6.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
image:
dependency: "direct main"
description:
name: image
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
url: "https://pub.dev"
source: hosted
version: "4.8.0"
image_picker:
dependency: "direct main"
description:
name: image_picker
sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
image_picker_android:
dependency: transitive
description:
name: image_picker_android
sha256: eda9b91b7e266d9041084a42d605a74937d996b87083395c5e47835916a86156
url: "https://pub.dev"
source: hosted
version: "0.8.13+14"
image_picker_for_web:
dependency: transitive
description:
name: image_picker_for_web
sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
image_picker_ios:
dependency: transitive
description:
name: image_picker_ios
sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588
url: "https://pub.dev"
source: hosted
version: "0.8.13+6"
image_picker_linux:
dependency: transitive
description:
name: image_picker_linux
sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
url: "https://pub.dev"
source: hosted
version: "0.2.2"
image_picker_macos:
dependency: transitive
description:
name: image_picker_macos
sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91"
url: "https://pub.dev"
source: hosted
version: "0.2.2+1"
image_picker_platform_interface:
dependency: transitive
description:
name: image_picker_platform_interface
sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c"
url: "https://pub.dev"
source: hosted
version: "2.11.1"
image_picker_windows:
dependency: transitive
description:
name: image_picker_windows
sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
url: "https://pub.dev"
source: hosted
version: "0.2.2"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lints:
dependency: transitive
description:
name: lints
sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
url: "https://pub.dev"
source: hosted
version: "0.12.18"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.17.0"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
native_toolchain_c:
dependency: transitive
description:
name: native_toolchain_c
sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac"
url: "https://pub.dev"
source: hosted
version: "0.17.4"
objective_c:
dependency: transitive
description:
name: objective_c
sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52"
url: "https://pub.dev"
source: hosted
version: "9.3.0"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_provider:
dependency: "direct main"
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e
url: "https://pub.dev"
source: hosted
version: "2.2.22"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
url: "https://pub.dev"
source: hosted
version: "2.6.0"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
url: "https://pub.dev"
source: hosted
version: "7.0.2"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
posix:
dependency: transitive
description:
name: posix
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
url: "https://pub.dev"
source: hosted
version: "6.0.3"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
url: "https://pub.dev"
source: hosted
version: "0.7.9"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
url: "https://pub.dev"
source: hosted
version: "15.0.2"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
xml:
dependency: transitive
description:
name: xml
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
url: "https://pub.dev"
source: hosted
version: "6.6.1"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.10.3 <4.0.0"
flutter: ">=3.38.4"

View File

@@ -13,6 +13,7 @@ dependencies:
image_picker: ^1.1.2 image_picker: ^1.1.2
image: ^4.2.0 image: ^4.2.0
flutter_colorpicker: ^1.1.0 flutter_colorpicker: ^1.1.0
path_provider: ^2.1.5
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@@ -0,0 +1,53 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:korken_mosaic/project_codec.dart';
void main() {
test('MosaicProjectData json roundtrip keeps values including catalog snapshot', () {
final original = MosaicProjectData(
useCapSize: true,
gridWidth: '50',
gridHeight: '40',
capSize: '10',
fidelityStructure: 0.3,
ditheringStrength: 0.2,
edgeEmphasis: 0.4,
colorVariation: 0.5,
selectedPreset: 'realistisch',
sourceImageBytes: Uint8List.fromList([1, 2, 3]),
catalogSnapshot: const [
MosaicPaletteSnapshotEntry(name: 'White', colorValue: 0xFFF2F2F2),
MosaicPaletteSnapshotEntry(name: 'Blue', colorValue: 0xFF3F6FD8),
],
savedAt: DateTime.parse('2026-01-01T12:00:00Z'),
);
final decoded = MosaicProjectData.fromJson(original.toJson());
expect(decoded.useCapSize, isTrue);
expect(decoded.gridWidth, '50');
expect(decoded.gridHeight, '40');
expect(decoded.capSize, '10');
expect(decoded.selectedPreset, 'realistisch');
expect(decoded.sourceImageBytes, isNotNull);
expect(decoded.sourceImageBytes!, [1, 2, 3]);
expect(decoded.catalogSnapshot.length, 2);
expect(decoded.catalogSnapshot.first.name, 'White');
expect(decoded.catalogSnapshot.last.colorValue, 0xFF3F6FD8);
});
test('MosaicProjectData defaults to empty snapshot when old project has none', () {
final decoded = MosaicProjectData.fromJson({
'useCapSize': false,
'gridWidth': '40',
'gridHeight': '30',
'capSize': '12',
'selectedPreset': 'ausgewogen',
'savedAt': '2026-01-01T12:00:00Z',
});
expect(decoded.catalogSnapshot, isEmpty);
expect(decoded.sourceImageBytes, isNull);
});
}

35
test/widget_test.dart Normal file
View File

@@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:korken_mosaic/main.dart';
void main() {
testWidgets('stepper blocks forward navigation without image and shows hint',
(WidgetTester tester) async {
await tester.pumpWidget(const KorkenMosaicApp());
expect(find.text('1) Bild'), findsOneWidget);
expect(find.text('2) Größe'), findsOneWidget);
expect(find.text('3) Farben'), findsOneWidget);
expect(find.text('4) Ergebnis'), findsOneWidget);
await tester.tap(find.text('Weiter'));
await tester.pump();
// Stays on step 1 because no source image is available yet.
expect(find.text('Import target image'), findsOneWidget);
expect(find.text('Bitte zuerst ein Bild auswählen.'), findsOneWidget);
});
testWidgets('generate actions are disabled without image',
(WidgetTester tester) async {
await tester.pumpWidget(const KorkenMosaicApp());
final fab = tester.widget<FloatingActionButton>(
find.byType(FloatingActionButton),
);
expect(fab.onPressed, isNull);
// Result-step action is not reachable before image selection.
expect(find.byKey(const Key('generate-btn')), findsNothing);
});
}