feat(project): persist catalog snapshot in project data

This commit is contained in:
gary
2026-02-24 21:43:25 +01:00
parent 25d570c779
commit fd72d53d2a
2 changed files with 57 additions and 3 deletions

View File

@@ -1,6 +1,28 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data'; 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 { class MosaicProjectData {
final bool useCapSize; final bool useCapSize;
final String gridWidth; final String gridWidth;
@@ -12,6 +34,7 @@ class MosaicProjectData {
final double colorVariation; final double colorVariation;
final String selectedPreset; final String selectedPreset;
final Uint8List? sourceImageBytes; final Uint8List? sourceImageBytes;
final List<MosaicPaletteSnapshotEntry> catalogSnapshot;
final DateTime savedAt; final DateTime savedAt;
const MosaicProjectData({ const MosaicProjectData({
@@ -25,6 +48,7 @@ class MosaicProjectData {
required this.colorVariation, required this.colorVariation,
required this.selectedPreset, required this.selectedPreset,
required this.sourceImageBytes, required this.sourceImageBytes,
required this.catalogSnapshot,
required this.savedAt, required this.savedAt,
}); });
@@ -40,23 +64,32 @@ class MosaicProjectData {
'selectedPreset': selectedPreset, 'selectedPreset': selectedPreset,
'sourceImageBase64': 'sourceImageBase64':
sourceImageBytes == null ? null : base64Encode(sourceImageBytes!), sourceImageBytes == null ? null : base64Encode(sourceImageBytes!),
'catalogSnapshot': catalogSnapshot.map((entry) => entry.toJson()).toList(),
'savedAt': savedAt.toIso8601String(), 'savedAt': savedAt.toIso8601String(),
}; };
factory MosaicProjectData.fromJson(Map<String, dynamic> json) { factory MosaicProjectData.fromJson(Map<String, dynamic> json) {
final sourceB64 = json['sourceImageBase64'] as String?; final sourceB64 = json['sourceImageBase64'] as String?;
final snapshotRaw = (json['catalogSnapshot'] as List?) ?? const [];
return MosaicProjectData( return MosaicProjectData(
useCapSize: json['useCapSize'] as bool? ?? false, useCapSize: json['useCapSize'] as bool? ?? false,
gridWidth: json['gridWidth'] as String? ?? '40', gridWidth: json['gridWidth'] as String? ?? '40',
gridHeight: json['gridHeight'] as String? ?? '30', gridHeight: json['gridHeight'] as String? ?? '30',
capSize: json['capSize'] as String? ?? '12', capSize: json['capSize'] as String? ?? '12',
fidelityStructure: (json['fidelityStructure'] as num?)?.toDouble() ?? 0.5, 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, edgeEmphasis: (json['edgeEmphasis'] as num?)?.toDouble() ?? 0.4,
colorVariation: (json['colorVariation'] as num?)?.toDouble() ?? 0.3, colorVariation: (json['colorVariation'] as num?)?.toDouble() ?? 0.3,
selectedPreset: json['selectedPreset'] as String? ?? 'ausgewogen', selectedPreset: json['selectedPreset'] as String? ?? 'ausgewogen',
sourceImageBytes: sourceB64 == null ? null : base64Decode(sourceB64), 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(),
); );
} }
} }

View File

@@ -4,7 +4,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:korken_mosaic/project_codec.dart'; import 'package:korken_mosaic/project_codec.dart';
void main() { void main() {
test('MosaicProjectData json roundtrip keeps values', () { test('MosaicProjectData json roundtrip keeps values including catalog snapshot', () {
final original = MosaicProjectData( final original = MosaicProjectData(
useCapSize: true, useCapSize: true,
gridWidth: '50', gridWidth: '50',
@@ -16,6 +16,10 @@ void main() {
colorVariation: 0.5, colorVariation: 0.5,
selectedPreset: 'realistisch', selectedPreset: 'realistisch',
sourceImageBytes: Uint8List.fromList([1, 2, 3]), 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'), savedAt: DateTime.parse('2026-01-01T12:00:00Z'),
); );
@@ -28,5 +32,22 @@ void main() {
expect(decoded.selectedPreset, 'realistisch'); expect(decoded.selectedPreset, 'realistisch');
expect(decoded.sourceImageBytes, isNotNull); expect(decoded.sourceImageBytes, isNotNull);
expect(decoded.sourceImageBytes!, [1, 2, 3]); 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);
}); });
} }