creek
The AI Image Editor of 2030git clone https://github.com/maydayv7/creek
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}