creek
The AI Image Editor of 2030git clone https://github.com/maydayv7/creek
lib/ui/pages/stylesheet_page.dart (22099B)
1import 'dart:convert';
2import 'dart:io';
3import 'package:flutter/material.dart';
4import 'package:flutter_svg/flutter_svg.dart';
5import 'package:google_fonts/google_fonts.dart';
6import 'package:image_picker/image_picker.dart';
7import 'package:path/path.dart' as p;
8import 'package:path_provider/path_provider.dart';
9import 'package:creekui/data/repos/project_repo.dart';
10import 'package:creekui/data/repos/image_repo.dart';
11import 'package:creekui/data/repos/note_repo.dart';
12import 'package:creekui/services/stylesheet_service.dart';
13import 'package:creekui/services/python_service.dart';
14import 'package:creekui/ui/widgets/bottom_bar.dart';
15import 'package:creekui/ui/widgets/top_bar.dart';
16import 'package:creekui/ui/styles/variables.dart';
17import 'package:creekui/ui/widgets/section_header.dart';
18import 'package:creekui/ui/widgets/primary_button.dart';
19
20class StylesheetPage extends StatefulWidget {
21 final int projectId;
22 final bool autoGenerate;
23
24 const StylesheetPage({
25 super.key,
26 required this.projectId,
27 this.autoGenerate = false,
28 });
29
30 @override
31 State<StylesheetPage> createState() => _StylesheetPageState();
32}
33
34class _StylesheetPageState extends State<StylesheetPage> {
35 late int _currentProjectId;
36 bool _isLoading = false;
37 bool _showHexCodes = false;
38
39 StylesheetData? _stylesheetData;
40 String? _rawJsonString;
41
42 List<String> _projectAssets = [];
43 List<String> _logoPaths = [];
44 final ImagePicker _picker = ImagePicker();
45 final StylesheetService _stylesheetService = StylesheetService();
46
47 // Asset mapping
48 final Map<String, String> _lightingAssets = {
49 'backlit': 'assets/stylesheet/lighting/backlit.png',
50 'diffused': 'assets/stylesheet/lighting/diffused.png',
51 'dramaticcontrast': 'assets/stylesheet/lighting/dramatic-contrast.png',
52 'flatlighting': 'assets/stylesheet/lighting/flat-lighting.png',
53 'softlight': 'assets/stylesheet/lighting/soft-light.png',
54 'specularhighlights': 'assets/stylesheet/lighting/specular-highlights.png',
55 'studiolighting': 'assets/stylesheet/lighting/studio-lighting.png',
56 };
57
58 final Map<String, String> _materialAssets = {
59 'glossy': 'assets/stylesheet/material-look/glossy.png',
60 'laminated': 'assets/stylesheet/material-look/laminated.png',
61 'matte': 'assets/stylesheet/material-look/matte.png',
62 'mettalic': 'assets/stylesheet/material-look/metallic.png',
63 'organic': 'assets/stylesheet/material-look/organic.png',
64 'porcelain': 'assets/stylesheet/material-look/porcelain.png',
65 'wetlook': 'assets/stylesheet/material-look/wet-look.png',
66 'wood': 'assets/stylesheet/material-look/wooden.png',
67 };
68
69 final Map<String, String> _textureAssets = {
70 'bokehbackground': 'assets/stylesheet/texture/bokeh-background.png',
71 'brick': 'assets/stylesheet/texture/brick.png',
72 'concrete': 'assets/stylesheet/texture/concrete.png',
73 'denim': 'assets/stylesheet/texture/denim.png',
74 'fabric': 'assets/stylesheet/texture/fabric.png',
75 'grid': 'assets/stylesheet/texture/subtle-grid.png',
76 'ground': 'assets/stylesheet/texture/ground.png',
77 'motif': 'assets/stylesheet/texture/motifs.png',
78 'newspaper': 'assets/stylesheet/texture/newspaper-texture.png',
79 'paper': 'assets/stylesheet/texture/paper-grain.png',
80 'pattern': 'assets/stylesheet/texture/printed-pattern.png',
81 'stone': 'assets/stylesheet/texture/stone.png',
82 'studiobackdrop': 'assets/stylesheet/texture/studio-backdrop.png',
83 };
84
85 @override
86 void initState() {
87 super.initState();
88 _currentProjectId = widget.projectId;
89 if (widget.autoGenerate) {
90 WidgetsBinding.instance.addPostFrameCallback((_) {
91 _generateStylesheet();
92 });
93 } else {
94 _loadSavedStylesheet();
95 }
96 }
97
98 // Utils
99 String _formatLabel(String label) {
100 String clean = label.replaceAll(RegExp(r'[-_]'), ' ');
101 List<String> words = clean.split(' ');
102 return words
103 .map((w) {
104 if (w.isEmpty) return '';
105 return w[0].toUpperCase() + w.substring(1).toLowerCase();
106 })
107 .join(' ');
108 }
109
110 String? _findAssetPath(Map<String, String> assetMap, String label) {
111 String normalized = label.toLowerCase().replaceAll(
112 RegExp(r'[^a-z0-9]'),
113 '',
114 );
115 return assetMap[normalized];
116 }
117
118 Future<File?> _resolveFile(String path) async {
119 final file = File(path);
120 if (await file.exists()) return file;
121 try {
122 final filename = p.basename(path);
123 final dir = await getApplicationDocumentsDirectory();
124 final fixedPath = p.join(dir.path, 'generated_images', filename);
125 final fixedFile = File(fixedPath);
126 if (await fixedFile.exists()) return fixedFile;
127 } catch (e) {
128 debugPrint("Error resolving file path: $e");
129 }
130 return null;
131 }
132
133 Future<void> _loadSavedStylesheet() async {
134 final project = await ProjectRepo().getProjectById(_currentProjectId);
135 if (project == null) return;
136
137 List<String> currentAssets = project.assetsPath;
138 String? rawJson = project.globalStylesheet;
139 StylesheetData? data;
140
141 if (rawJson != null && rawJson.isNotEmpty) {
142 data = _stylesheetService.parse(rawJson);
143 }
144
145 if (mounted) {
146 setState(() {
147 _projectAssets = currentAssets;
148 _rawJsonString = rawJson;
149 _stylesheetData = data;
150 });
151 }
152 }
153
154 Future<void> _generateStylesheet() async {
155 setState(() {
156 _isLoading = true;
157 _stylesheetData = null;
158 _rawJsonString = null;
159 });
160
161 try {
162 final images = await ImageRepo().getImages(_currentProjectId);
163 final List<String> analysisData =
164 images
165 .map((img) => img.analysisData)
166 .where((data) => data != null && data.isNotEmpty)
167 .cast<String>()
168 .toList();
169 final notes = await NoteRepo().getNotesByProjectId(_currentProjectId);
170 final List<String> noteAnalysisData =
171 notes
172 .map((n) => n.analysisData)
173 .where((data) => data != null && data.isNotEmpty)
174 .cast<String>()
175 .toList();
176
177 analysisData.addAll(noteAnalysisData);
178
179 if (analysisData.isEmpty) {
180 if (mounted) {
181 ScaffoldMessenger.of(context).showSnackBar(
182 const SnackBar(content: Text("No analyzed images or notes found")),
183 );
184 }
185 return;
186 }
187
188 final result = await PythonService().generateStylesheet(analysisData);
189
190 if (mounted && result != null) {
191 final jsonString = jsonEncode(result);
192 await ProjectRepo().updateStylesheet(_currentProjectId, jsonString);
193 await _loadSavedStylesheet();
194 }
195 } catch (e) {
196 debugPrint("Gen Error: $e");
197 } finally {
198 if (mounted) setState(() => _isLoading = false);
199 }
200 }
201
202 @override
203 Widget build(BuildContext context) {
204 return Scaffold(
205 backgroundColor: Variables.background,
206 appBar: TopBar(
207 currentProjectId: _currentProjectId,
208 onBack: () => Navigator.of(context).pop(),
209 onProjectChanged:
210 (p) => setState(() {
211 _currentProjectId = p.id!;
212 _stylesheetData = null;
213 _projectAssets = [];
214 _logoPaths = [];
215 _loadSavedStylesheet();
216 }),
217 ),
218 body:
219 _isLoading
220 ? const Center(
221 child: CircularProgressIndicator(color: Variables.textPrimary),
222 )
223 : (_stylesheetData == null && _rawJsonString == null)
224 ? _buildEmptyState()
225 : _buildContent(),
226 bottomNavigationBar: BottomBar(
227 currentTab: BottomBarItem.stylesheet,
228 projectId: _currentProjectId,
229 ),
230 );
231 }
232
233 Widget _buildEmptyState() {
234 return Center(
235 child: Padding(
236 padding: const EdgeInsets.symmetric(horizontal: 40),
237 child: Column(
238 mainAxisAlignment: MainAxisAlignment.center,
239 children: [
240 Text(
241 "Are you ready to start building\nyour visual identity",
242 style: Variables.headerStyle.copyWith(fontSize: 18),
243 textAlign: TextAlign.center,
244 ),
245 const SizedBox(height: 24),
246 PrimaryButton(
247 text: "Generate Stylesheet",
248 iconPath: 'assets/icons/generate_icon.svg',
249 onPressed: _generateStylesheet,
250 ),
251 ],
252 ),
253 ),
254 );
255 }
256
257 Widget _buildContent() {
258 final data = _stylesheetData;
259 if (data == null) return const SizedBox();
260
261 return RefreshIndicator(
262 onRefresh: _generateStylesheet,
263 child: SingleChildScrollView(
264 padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
265 child: Column(
266 crossAxisAlignment: CrossAxisAlignment.start,
267 children: [
268 Center(
269 child: PrimaryButton(
270 text: "Regenerate Stylesheet",
271 iconPath: 'assets/icons/generate_icon.svg',
272 onPressed: _generateStylesheet,
273 ),
274 ),
275 const SizedBox(height: 24),
276
277 _buildLogosSection(null),
278 if (data.graphics.isNotEmpty || _projectAssets.isNotEmpty)
279 _buildGraphicsSection(data.graphics),
280 if (data.colors.isNotEmpty) _buildColorsSection(data.colors),
281 if (data.fonts.isNotEmpty) _buildFontsSection(data.fonts),
282 if (data.compositions.isNotEmpty)
283 _buildCompositionsSection(data.compositions),
284 if (data.materialLook.isNotEmpty)
285 _buildMaterialLookSection(data.materialLook),
286 if (data.textures.isNotEmpty) _buildTexturesSection(data.textures),
287 if (data.lighting.isNotEmpty) _buildLightingSection(data.lighting),
288 if (data.style.isNotEmpty) _buildStyleSection(data.style),
289 if (data.era.isNotEmpty) _buildEraSection(data.era),
290 if (data.emotions.isNotEmpty) _buildEmotionsSection(data.emotions),
291
292 const SizedBox(height: 24),
293 ],
294 ),
295 ),
296 );
297 }
298
299 // Logos
300 Widget _buildLogosSection(dynamic data) {
301 List<String> logoPaths = List.from(_logoPaths);
302
303 if (data is List) {
304 for (var item in data) {
305 if (item is Map && item.containsKey('path'))
306 logoPaths.add(item['path'].toString());
307 else if (item is String)
308 logoPaths.add(item);
309 }
310 } else if (data is String)
311 logoPaths.add(data);
312
313 return Column(
314 crossAxisAlignment: CrossAxisAlignment.start,
315 children: [
316 const Padding(
317 padding: EdgeInsets.only(bottom: 12),
318 child: SectionHeader(title: "Logos"),
319 ),
320 SizedBox(
321 height: 76,
322 child: ListView.separated(
323 scrollDirection: Axis.horizontal,
324 itemCount: logoPaths.length + 1,
325 separatorBuilder: (_, __) => const SizedBox(width: 8),
326 itemBuilder: (context, index) {
327 if (index == logoPaths.length) {
328 return GestureDetector(
329 onTap: _pickLogo,
330 child: Container(
331 width: 76,
332 height: 76,
333 decoration: BoxDecoration(
334 border: Border.all(color: Variables.borderSubtle),
335 borderRadius: BorderRadius.circular(8),
336 ),
337 child: Center(
338 child: SvgPicture.asset(
339 'assets/icons/add-line.svg',
340 width: 20,
341 height: 20,
342 colorFilter: const ColorFilter.mode(
343 Variables.textSecondary,
344 BlendMode.srcIn,
345 ),
346 ),
347 ),
348 ),
349 );
350 }
351 return _buildGraphicCard(logoPaths[index], size: 76);
352 },
353 ),
354 ),
355 const SizedBox(height: 16),
356 ],
357 );
358 }
359
360 Future<void> _pickLogo() async {
361 try {
362 final XFile? pickedFile = await _picker.pickImage(
363 source: ImageSource.gallery,
364 );
365 if (pickedFile != null) {
366 setState(() {
367 _logoPaths.add(pickedFile.path);
368 });
369 }
370 } catch (e) {
371 debugPrint("Error picking logo: $e");
372 }
373 }
374
375 // Graphics
376 Widget _buildGraphicsSection(List<String> extractedGraphics) {
377 List<String> graphicPaths = List.from(_projectAssets);
378 graphicPaths.addAll(extractedGraphics);
379
380 if (graphicPaths.isEmpty) return const SizedBox();
381
382 return Column(
383 crossAxisAlignment: CrossAxisAlignment.start,
384 children: [
385 const Padding(
386 padding: EdgeInsets.only(bottom: 12),
387 child: SectionHeader(title: "Graphics"),
388 ),
389 SizedBox(
390 height: 106,
391 child: ListView.separated(
392 scrollDirection: Axis.horizontal,
393 itemCount: graphicPaths.length,
394 separatorBuilder: (_, __) => const SizedBox(width: 8),
395 itemBuilder:
396 (context, index) => _buildGraphicCard(graphicPaths[index]),
397 ),
398 ),
399 const SizedBox(height: 16),
400 ],
401 );
402 }
403
404 Widget _buildGraphicCard(String savedPath, {double size = 104}) {
405 return FutureBuilder<File?>(
406 future: _resolveFile(savedPath),
407 builder: (context, snapshot) {
408 final File? file = snapshot.data;
409 return Container(
410 width: size,
411 height: size,
412 decoration: BoxDecoration(
413 borderRadius: BorderRadius.circular(8),
414 color: Variables.surfaceSubtle,
415 ),
416 clipBehavior: Clip.antiAlias,
417 child:
418 file != null
419 ? Image.file(file, fit: BoxFit.cover)
420 : Container(
421 color: Variables.surfaceSubtle,
422 child: const Center(
423 child: Icon(
424 Icons.broken_image,
425 color: Colors.grey,
426 size: 20,
427 ),
428 ),
429 ),
430 );
431 },
432 );
433 }
434
435 // Fonts
436 Widget _buildFontsSection(List<String> fontNames) {
437 if (fontNames.isEmpty) return const SizedBox();
438
439 return Column(
440 crossAxisAlignment: CrossAxisAlignment.start,
441 children: [
442 const Padding(
443 padding: EdgeInsets.only(bottom: 12),
444 child: SectionHeader(title: "Fonts"),
445 ),
446 SizedBox(
447 height: 120,
448 child: ListView.separated(
449 scrollDirection: Axis.horizontal,
450 itemCount: fontNames.length,
451 separatorBuilder: (_, __) => const SizedBox(width: 8),
452 itemBuilder: (context, index) => _buildFontCard(fontNames[index]),
453 ),
454 ),
455 const SizedBox(height: 16),
456 ],
457 );
458 }
459
460 Widget _buildFontCard(String resolvedFontName) {
461 final String displayFontName = _formatLabel(resolvedFontName);
462 TextStyle sampleStyle;
463 try {
464 sampleStyle = GoogleFonts.getFont(resolvedFontName);
465 } catch (_) {
466 sampleStyle = const TextStyle(fontFamily: 'GeneralSans');
467 }
468
469 return Container(
470 width: (MediaQuery.of(context).size.width - 32 - 16) / 3,
471 height: 120,
472 padding: const EdgeInsets.all(9),
473 decoration: BoxDecoration(
474 border: Border.all(color: Variables.borderSubtle),
475 borderRadius: BorderRadius.circular(8),
476 ),
477 child: Column(
478 crossAxisAlignment: CrossAxisAlignment.start,
479 mainAxisAlignment: MainAxisAlignment.spaceBetween,
480 children: [
481 Text(
482 "Aa",
483 style: sampleStyle.copyWith(
484 fontSize: 28,
485 height: 1.2,
486 fontWeight: FontWeight.w400,
487 color: Variables.textPrimary,
488 ),
489 ),
490 Text(
491 displayFontName,
492 maxLines: 2,
493 overflow: TextOverflow.ellipsis,
494 style: Variables.captionStyle,
495 ),
496 ],
497 ),
498 );
499 }
500
501 // Colors
502 Widget _buildColorsSection(List<Color> colors) {
503 if (colors.isEmpty) return const SizedBox();
504
505 return Column(
506 crossAxisAlignment: CrossAxisAlignment.start,
507 children: [
508 Padding(
509 padding: const EdgeInsets.only(bottom: 12),
510 child: SectionHeader(
511 title: "Colors",
512 trailing: GestureDetector(
513 onTap: () => setState(() => _showHexCodes = !_showHexCodes),
514 child: Icon(
515 _showHexCodes ? Icons.visibility : Icons.visibility_outlined,
516 color: Variables.textPrimary,
517 size: 20,
518 ),
519 ),
520 ),
521 ),
522 Wrap(
523 spacing: 8,
524 runSpacing: 8,
525 children:
526 colors.take(5).map((color) => _buildColorSwatch(color)).toList(),
527 ),
528 const SizedBox(height: 16),
529 ],
530 );
531 }
532
533 Widget _buildColorSwatch(Color color) {
534 final String hexCode =
535 '#${color.value.toRadixString(16).padLeft(8, '0').substring(2).toUpperCase()}';
536 final Color textColor =
537 color.computeLuminance() > 0.5 ? Colors.black : Colors.white;
538
539 return Container(
540 width: 104,
541 height: 52,
542 decoration: BoxDecoration(
543 color: color,
544 borderRadius: BorderRadius.circular(8),
545 ),
546 alignment: Alignment.center,
547 child:
548 _showHexCodes
549 ? Text(
550 hexCode,
551 style: TextStyle(
552 fontFamily: 'GeneralSans',
553 fontSize: 13,
554 fontWeight: FontWeight.w500,
555 color: textColor,
556 ),
557 )
558 : null,
559 );
560 }
561
562 Widget _buildUnifiedCard(String label, String? assetPath) {
563 final formattedLabel = _formatLabel(label);
564
565 if (assetPath != null) {
566 return Container(
567 width: 106,
568 height: 106,
569 decoration: BoxDecoration(
570 borderRadius: BorderRadius.circular(8),
571 color: Colors.white,
572 border: Border.all(color: Variables.borderSubtle.withOpacity(0.5)),
573 ),
574 clipBehavior: Clip.antiAlias,
575 child: Stack(
576 children: [
577 Positioned.fill(
578 child: Image.asset(
579 assetPath,
580 fit: BoxFit.cover,
581 errorBuilder:
582 (c, o, s) => Container(color: Variables.surfaceSubtle),
583 ),
584 ),
585 Positioned(
586 left: 0,
587 right: 0,
588 bottom: 0,
589 height: 40,
590 child: Container(
591 decoration: BoxDecoration(
592 gradient: LinearGradient(
593 begin: Alignment.topCenter,
594 end: Alignment.bottomCenter,
595 colors: [Colors.transparent, Colors.black.withOpacity(0.7)],
596 ),
597 ),
598 ),
599 ),
600 Positioned(
601 left: 4,
602 right: 4,
603 bottom: 4,
604 child: Text(
605 formattedLabel,
606 style: const TextStyle(
607 fontFamily: 'GeneralSans',
608 fontSize: 12,
609 fontWeight: FontWeight.w500,
610 color: Colors.white,
611 ),
612 textAlign: TextAlign.center,
613 maxLines: 2,
614 overflow: TextOverflow.ellipsis,
615 ),
616 ),
617 ],
618 ),
619 );
620 } else {
621 return Container(
622 width: 120,
623 height: 48,
624 padding: const EdgeInsets.symmetric(horizontal: 8),
625 decoration: BoxDecoration(
626 color: Variables.surfaceSubtle,
627 borderRadius: BorderRadius.circular(8),
628 ),
629 alignment: Alignment.center,
630 child: Text(
631 formattedLabel,
632 style: const TextStyle(
633 fontFamily: 'GeneralSans',
634 fontSize: 14,
635 fontWeight: FontWeight.w500,
636 color: Variables.textPrimary,
637 ),
638 textAlign: TextAlign.center,
639 ),
640 );
641 }
642 }
643
644 Widget _buildAttributeSection(
645 String title,
646 List<String> items,
647 Map<String, String>? assetMap,
648 ) {
649 if (items.isEmpty) return const SizedBox();
650
651 return Column(
652 crossAxisAlignment: CrossAxisAlignment.start,
653 children: [
654 Padding(
655 padding: const EdgeInsets.only(bottom: 12),
656 child: SectionHeader(title: title),
657 ),
658 SizedBox(
659 height: (assetMap != null) ? 106 : 54,
660 child: ListView.separated(
661 scrollDirection: Axis.horizontal,
662 itemCount: items.length,
663 separatorBuilder: (_, __) => const SizedBox(width: 8),
664 itemBuilder: (context, index) {
665 String label = items[index];
666 String? assetPath =
667 assetMap != null ? _findAssetPath(assetMap, label) : null;
668 return _buildUnifiedCard(label, assetPath);
669 },
670 ),
671 ),
672 const SizedBox(height: 24),
673 ],
674 );
675 }
676
677 Widget _buildCompositionsSection(List<String> data) =>
678 _buildAttributeSection("Compositions", data, null);
679 Widget _buildMaterialLookSection(List<String> data) =>
680 _buildAttributeSection("Material Look", data, _materialAssets);
681 Widget _buildTexturesSection(List<String> data) =>
682 _buildAttributeSection("Textures", data, _textureAssets);
683 Widget _buildLightingSection(List<String> data) =>
684 _buildAttributeSection("Lighting", data, _lightingAssets);
685 Widget _buildStyleSection(List<String> data) =>
686 _buildAttributeSection("Style", data, null);
687 Widget _buildEraSection(List<String> data) =>
688 _buildAttributeSection("Era", data, null);
689 Widget _buildEmotionsSection(List<String> data) =>
690 _buildAttributeSection("Emotions", data, null);
691}