creek

The AI Image Editor of 2030

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}