/// JSON backup document: products, trips, settings, and embedded photos. library; import 'dart:convert'; const int kBackupVersion = 1; class BackupDocument { const BackupDocument({ required this.version, required this.exportedAt, required this.darkMode, required this.supermarkets, required this.products, required this.shops, required this.shopItems, required this.images, }); final int version; final String exportedAt; final bool darkMode; final List> supermarkets; final List> products; final List> shops; final List> shopItems; final Map images; Map toJson() => { 'format': 'receipity-backup', 'version': version, 'exportedAt': exportedAt, 'darkMode': darkMode, 'supermarkets': supermarkets, 'products': products, 'shops': shops, 'shopItems': shopItems, 'images': images, }; String encode() => const JsonEncoder.withIndent(' ').convert(toJson()); static BackupDocument parse(String json) { final decoded = jsonDecode(json); if (decoded is! Map) { throw const FormatException('Backup is not a JSON object.'); } if (decoded['format'] != 'receipity-backup') { throw const FormatException('This file is not a Receipity backup.'); } final version = decoded['version']; if (version is! int || version < 1 || version > kBackupVersion) { throw const FormatException('Unsupported backup version.'); } return BackupDocument( version: version, exportedAt: decoded['exportedAt'] as String? ?? '', darkMode: decoded['darkMode'] == true, supermarkets: _maps(decoded['supermarkets']), products: _maps(decoded['products']), shops: _maps(decoded['shops']), shopItems: _maps(decoded['shopItems']), images: _strings(decoded['images']), ); } static List> _maps(Object? value) { if (value == null) return []; if (value is! List) { throw const FormatException('Backup tables must be lists.'); } return [ for (final item in value) if (item is Map) Map.from( item.map((key, val) => MapEntry(key.toString(), val)), ), ]; } static Map _strings(Object? value) { if (value == null) return {}; if (value is! Map) return {}; return { for (final entry in value.entries) if (entry.value is String) entry.key.toString(): entry.value as String, }; } } String? backupImageKey(String? absolutePath) { if (absolutePath == null || absolutePath.isEmpty) return null; final normalized = absolutePath.replaceAll('\\', '/'); final products = normalized.split('/products/'); if (products.length == 2) return 'products/${products.last}'; final receipts = normalized.split('/receipts/'); if (receipts.length == 2) return 'receipts/${receipts.last}'; return null; }