feat: 4-step flow, robust project persistence, JSON export + QA/review docs #4
@@ -1,6 +1,28 @@
|
||||
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;
|
||||
@@ -12,6 +34,7 @@ class MosaicProjectData {
|
||||
final double colorVariation;
|
||||
final String selectedPreset;
|
||||
final Uint8List? sourceImageBytes;
|
||||
final List<MosaicPaletteSnapshotEntry> catalogSnapshot;
|
||||
final DateTime savedAt;
|
||||
|
||||
const MosaicProjectData({
|
||||
@@ -25,6 +48,7 @@ class MosaicProjectData {
|
||||
required this.colorVariation,
|
||||
required this.selectedPreset,
|
||||
required this.sourceImageBytes,
|
||||
required this.catalogSnapshot,
|
||||
required this.savedAt,
|
||||
});
|
||||
|
||||
@@ -40,23 +64,32 @@ class MosaicProjectData {
|
||||
'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,
|
||||
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),
|
||||
savedAt: DateTime.tryParse(json['savedAt'] as String? ?? '') ?? DateTime.now(),
|
||||
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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:korken_mosaic/project_codec.dart';
|
||||
|
||||
void main() {
|
||||
test('MosaicProjectData json roundtrip keeps values', () {
|
||||
test('MosaicProjectData json roundtrip keeps values including catalog snapshot', () {
|
||||
final original = MosaicProjectData(
|
||||
useCapSize: true,
|
||||
gridWidth: '50',
|
||||
@@ -16,6 +16,10 @@ void main() {
|
||||
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'),
|
||||
);
|
||||
|
||||
@@ -28,5 +32,22 @@ void main() {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user