creek

The AI Image Editor of 2030

lib/services/stylesheet_service.dart (8350B)


  1import 'dart:convert';
  2import 'package:flutter/material.dart';
  3import 'package:google_fonts/google_fonts.dart';
  4
  5// A simple model to hold the extracted design tokens
  6class StylesheetData {
  7  final List<Color> colors;
  8  final List<String> fonts;
  9  final List<String> graphics;
 10  final List<String> compositions;
 11  final List<String> materialLook;
 12  final List<String> textures;
 13  final List<String> lighting;
 14  final List<String> style;
 15  final List<String> era;
 16  final List<String> emotions;
 17
 18  StylesheetData({
 19    required this.colors,
 20    required this.fonts,
 21    this.graphics = const [],
 22    this.compositions = const [],
 23    this.materialLook = const [],
 24    this.textures = const [],
 25    this.lighting = const [],
 26    this.style = const [],
 27    this.era = const [],
 28    this.emotions = const [],
 29  });
 30}
 31
 32class StylesheetService {
 33  // Main entry point: Parses a raw (potentially dirty) JSON string
 34  // and returns a structured [StylesheetData] object
 35  StylesheetData parse(String? rawJson) {
 36    if (rawJson == null || rawJson.isEmpty) {
 37      return StylesheetData(colors: [], fonts: []);
 38    }
 39
 40    // 1. Parse the string into a Map
 41    Map<String, dynamic> data = _parseRawJson(rawJson);
 42
 43    // 2. Extract and Process Fonts
 44    List<String> fonts = _extractFonts(data);
 45
 46    // 3. Extract and Process Colors
 47    List<Color> colors = _extractColors(data);
 48
 49    // 4. Extract other attributes
 50    return StylesheetData(
 51      colors: colors,
 52      fonts: fonts,
 53      graphics: _extractStrings(data, [
 54        'Graphics',
 55        'graphics',
 56      ], valueKey: 'path'),
 57      compositions: _extractStrings(data, [
 58        'Compositions',
 59        'Composition',
 60        'compositions',
 61      ]),
 62      materialLook: _extractStrings(data, [
 63        'Material look',
 64        'Material Look',
 65        'material_look',
 66      ]),
 67      textures: _extractStrings(data, [
 68        'Textures',
 69        'Background/Texture',
 70        'textures',
 71      ]),
 72      lighting: _extractStrings(data, ['Lighting', 'lighting']),
 73      style: _extractStrings(data, ['Style', 'style']),
 74      era: _extractStrings(data, ['Era/Cultural Reference', 'Era', 'era']),
 75      emotions: _extractStrings(data, ['Emotions', 'Emotional', 'emotions']),
 76    );
 77  }
 78
 79  // ---------------------------------------------------------------------------
 80  // PARSING LOGIC
 81  // ---------------------------------------------------------------------------
 82
 83  // Safely parses the raw string into a Map, handling dirty AI output
 84  Map<String, dynamic> _parseRawJson(String rawString) {
 85    try {
 86      // Try standard decode first
 87      return _normalizeResult(jsonDecode(rawString));
 88    } catch (e) {
 89      try {
 90        // Try cleaning regex then decoding
 91        final cleaned = _cleanJsonString(rawString);
 92        return _normalizeResult(jsonDecode(cleaned));
 93      } catch (_) {
 94        return {};
 95      }
 96    }
 97  }
 98
 99  /// Cleans "dirty" JSON strings by fixing quotes and unquoted keys
100  String _cleanJsonString(String raw) {
101    String cleaned = raw;
102
103    // Remove Markdown code blocks if present (common AI artifact)
104    cleaned = cleaned.replaceAll(RegExp(r'^```json\s*|\s*```$'), '');
105
106    // Add quotes to keys
107    cleaned = cleaned.replaceAllMapped(
108      RegExp(r'([{,]\s*)([a-zA-Z0-9_\s/]+)(\s*:)'),
109      (match) => '${match[1]}"${match[2]?.trim()}"${match[3]}',
110    );
111    // Add quotes to string values that aren't booleans or numbers
112    cleaned = cleaned.replaceAllMapped(
113      RegExp(r'(:\s*)([a-zA-Z0-9_\-\.\/\s]+)(?=\s*[,}])'),
114      (match) {
115        String val = match[2]!.trim();
116        if (val == 'true' ||
117            val == 'false' ||
118            val == 'null' ||
119            double.tryParse(val) != null) {
120          return match[0]!;
121        }
122        return '${match[1]}"$val"';
123      },
124    );
125    return cleaned;
126  }
127
128  // Normalizes the structure if the API returns { "results": ... }
129  Map<String, dynamic> _normalizeResult(dynamic parsed) {
130    if (parsed is String) {
131      try {
132        parsed = jsonDecode(parsed);
133      } catch (_) {}
134    }
135
136    if (parsed is Map<String, dynamic>) {
137      if (parsed.containsKey('results') && parsed['results'] is Map) {
138        return parsed['results'];
139      }
140      return parsed;
141    }
142    return {};
143  }
144
145  // ---------------------------------------------------------------------------
146  // EXTRACTION LOGIC
147  // ---------------------------------------------------------------------------
148
149  // Extracts font names and resolves them to valid Google Font strings.
150  List<String> _extractFonts(Map<String, dynamic> data) {
151    dynamic fontData = _findValue(data, [
152      'Typography',
153      'fonts',
154      'typography',
155      'Fonts',
156    ]);
157    if (fontData == null) return [];
158
159    List<String> rawNames = [];
160    if (fontData is List) {
161      for (var item in fontData) {
162        if (item is Map && item.containsKey('label')) {
163          rawNames.add(item['label'].toString().trim());
164        } else if (item is String) {
165          rawNames.add(item.trim());
166        }
167      }
168    } else if (fontData is Map && fontData.containsKey('label')) {
169      rawNames.add(fontData['label'].toString().trim());
170    } else if (fontData is String) {
171      rawNames.add(fontData.trim());
172    }
173
174    return rawNames.map((name) => _resolveGoogleFontName(name)).toList();
175  }
176
177  // Extracts colors from Hex codes or semantic labels
178  List<Color> _extractColors(Map<String, dynamic> data) {
179    dynamic colorData = _findValue(data, [
180      'Colour Palette',
181      'Color Palette',
182      'colors',
183      'Colors',
184    ]);
185    if (colorData == null) return [];
186
187    List<Color> resolvedColors = [];
188
189    if (colorData is List) {
190      for (var item in colorData) {
191        String? label;
192        if (item is Map) {
193          label = item['label']?.toString();
194        } else if (item is String) {
195          label = item;
196        }
197
198        if (label != null) {
199          resolvedColors.add(_parseColor(label));
200        }
201      }
202    }
203    return resolvedColors;
204  }
205
206  // Generic helper to extract a list of strings from various keys
207  // Supports extraction from [{ "label": "val" }] or ["val"] or "val"
208  List<String> _extractStrings(
209    Map<String, dynamic> data,
210    List<String> keys, {
211    String valueKey = 'label',
212  }) {
213    dynamic rawData = _findValue(data, keys);
214    if (rawData == null) return [];
215
216    List<String> results = [];
217    if (rawData is List) {
218      for (var item in rawData) {
219        if (item is Map && item.containsKey(valueKey)) {
220          results.add(item[valueKey].toString());
221        } else if (item is String) {
222          results.add(item);
223        }
224      }
225    } else if (rawData is Map && rawData.containsKey(valueKey)) {
226      results.add(rawData[valueKey].toString());
227    } else if (rawData is String) {
228      results.add(rawData);
229    }
230    return results;
231  }
232
233  // ---------------------------------------------------------------------------
234  // HELPERS
235  // ---------------------------------------------------------------------------
236
237  dynamic _findValue(Map<String, dynamic> map, List<String> keys) {
238    for (var k in keys) {
239      if (map.containsKey(k)) return map[k];
240      // Case-insensitive check
241      for (var mapKey in map.keys) {
242        if (mapKey.toLowerCase() == k.toLowerCase()) return map[mapKey];
243      }
244    }
245    return null;
246  }
247
248  String _resolveGoogleFontName(String dirtyName) {
249    // 1. Exact match check (fastest)
250    try {
251      GoogleFonts.getFont(dirtyName);
252      return dirtyName;
253    } catch (_) {}
254
255    // 2. Fuzzy match
256    String cleanInput = dirtyName
257        .toLowerCase()
258        .replaceAll(RegExp(r'[-_]regular$'), '')
259        .replaceAll(RegExp(r'[^a-z0-9]'), '');
260
261    final allFonts = GoogleFonts.asMap().keys;
262    for (String officialName in allFonts) {
263      String cleanOfficial = officialName.toLowerCase().replaceAll(
264        RegExp(r'[^a-z0-9]'),
265        '',
266      );
267      if (cleanOfficial == cleanInput) {
268        return officialName;
269      }
270    }
271    return dirtyName; // Fallback
272  }
273
274  Color _parseColor(String input) {
275    if (input.startsWith('#') || input.length == 6) {
276      try {
277        String hex = input.replaceAll('#', '');
278        if (hex.length == 6) {
279          return Color(int.parse('0xFF$hex'));
280        }
281      } catch (_) {}
282    }
283    return Colors.grey.shade400; // Default fallback
284  }
285}