creek

The AI Image Editor of 2030
commit 0f84a7a4d721abe28d4106fcda6e7b188ca0aa58
parent 87dbf78d98e6393d2b030bce7bf50afbe349d28c
Author: aditya-samal <samaladitya2004@gmail.com>
Date:   Tue,  2 Dec 2025 05:28:07 +0530

Fixed Stylesheet

Diffstat:
Mlib/ui/pages/stylesheet_page.dart | 819+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Mlib/ui/widgets/top_bar.dart | 10++++++----
2 files changed, 665 insertions(+), 164 deletions(-)

diff --git a/lib/ui/pages/stylesheet_page.dart b/lib/ui/pages/stylesheet_page.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:image_picker/image_picker.dart'; import 'package:adobe/ui/styles/variables.dart'; import 'package:adobe/ui/widgets/bottom_bar.dart'; import 'package:adobe/ui/widgets/top_bar.dart'; @@ -31,7 +32,9 @@ class _StylesheetPageState extends State<StylesheetPage> { String? _rawJsonString; List<String> _projectAssets = []; + List<String> _logoPaths = []; final Map<String, String> _fontNameCache = {}; + final ImagePicker _picker = ImagePicker(); @override void initState() { @@ -195,14 +198,20 @@ class _StylesheetPageState extends State<StylesheetPage> { _currentProjectId = p.id!; _stylesheetMap = null; _projectAssets = []; + _logoPaths = []; _loadSavedStylesheet(); }), + onSettingsPressed: () { + // Handle settings action + }, ), body: _isLoading ? const Center(child: CircularProgressIndicator(color: Variables.textPrimary)) - : (_stylesheetMap == null && _rawJsonString == null && _projectAssets.isEmpty) + : (_stylesheetMap == null && _rawJsonString == null && _projectAssets.isEmpty && _logoPaths.isEmpty) ? _buildEmptyState() : _buildContent(), + floatingActionButton: _buildFAB(), + floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, bottomNavigationBar: BottomBar(currentTab: BottomBarItem.stylesheet, projectId: _currentProjectId), ); } @@ -225,86 +234,90 @@ class _StylesheetPageState extends State<StylesheetPage> { } Widget _buildContent() { - // Added 'Composition' to style lookup to match your screenshot - final style = _getData(['Style', 'style', 'Composition', 'composition']); + // Extract data in order matching Figma design + final graphics = _getData(['Graphics', 'graphics']); + final colors = _getData(['Colour Palette', 'Color Palette', 'colors', 'Colors']); + final typography = _getData(['Typography', 'fonts', 'Fonts']); + final compositions = _getData(['Compositions', 'compositions']); + final materialLook = _getData(['Material look', 'Material Look', 'material_look']); + final textures = _getData(['Textures', 'textures']); final lighting = _getData(['Lighting', 'lighting']); - final colors = _getData(['Colour Palette', 'Color Palette', 'colors']); + final style = _getData(['Style', 'style']); + final era = _getData(['Era/Cultural Reference', 'era', 'Era']); final emotions = _getData(['Emotions', 'emotions']); - final era = _getData(['Era/Cultural Reference', 'era']); - final typography = _getData(['Typography', 'fonts']); - - final hasAssets = _projectAssets.isNotEmpty; - - // --- DYNAMIC SECTION GENERATOR --- - // If the map contains keys we haven't hardcoded above, generate sliders for them. - // This prevents the "Parsing Error" when valid data exists but the key is new. - List<Widget> dynamicSections = []; - List<String> handledKeys = [ - 'Style', 'style', 'Composition', 'composition', - 'Lighting', 'lighting', - 'Colour Palette', 'Color Palette', 'colors', - 'Emotions', 'emotions', - 'Era/Cultural Reference', 'era', - 'Typography', 'fonts' - ]; - - if (_stylesheetMap != null) { - for (var key in _stylesheetMap!.keys) { - // If we haven't handled this key yet and it looks like a list (slider data) - if (!handledKeys.any((k) => k.toLowerCase() == key.toLowerCase()) && _stylesheetMap![key] is List) { - dynamicSections.add(_buildSliderSection(key, _stylesheetMap![key])); - } - } - } - - final foundAny = (style != null || lighting != null || colors != null || emotions != null || era != null || typography != null || hasAssets || dynamicSections.isNotEmpty); return RefreshIndicator( onRefresh: _generateStylesheet, child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(vertical: 24), + padding: const EdgeInsets.only(left: 16, right: 16, top: 24, bottom: 104), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - - if (foundAny) ...[ - if (hasAssets) _buildAssetsSection(_projectAssets), - if (typography != null) _buildTypographySection(typography), - if (colors != null) ...[ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildSectionHeader("Color Palette"), - _buildColorSection(colors), + // Logos Section - always show for manual upload + _buildLogosSection(null), + + // Graphics Section - includes project assets + if (graphics != null || _projectAssets.isNotEmpty) _buildGraphicsSection(graphics), + + // Colors Section + if (colors != null) _buildColorsSection(colors), + + // Fonts Section + if (typography != null) _buildFontsSection(typography), + + // Compositions Section + if (compositions != null) _buildCompositionsSection(compositions), + + // Material look Section + if (materialLook != null) _buildMaterialLookSection(materialLook), + + // Textures Section + if (textures != null) _buildTexturesSection(textures), + + // Lighting Section + if (lighting != null) _buildLightingSection(lighting), + + // Style Section + if (style != null) _buildStyleSection(style), + + // Era Section + if (era != null) _buildEraSection(era), + + // Emotions Section + if (emotions != null) _buildEmotionsSection(emotions), + + const SizedBox(height: 24), ], ), ), - const SizedBox(height: 32), - ], - if (style != null) _buildSliderSection("Style & Aesthetic", style), - if (emotions != null) _buildSliderSection("Mood & Emotions", emotions), - if (lighting != null) _buildSliderSection("Lighting", lighting), - if (era != null) _buildSliderSection("Era & Culture", era), - - // Render any extra data found in the JSON - ...dynamicSections, - - ] else ...[ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration(color: Colors.red.shade50, borderRadius: BorderRadius.circular(12)), - child: Text("Parsing Error. Raw Data:\n\n$_rawJsonString"), - ), + ); + } + + Widget _buildFAB() { + return Container( + margin: const EdgeInsets.only(bottom: 104), + child: FloatingActionButton.extended( + onPressed: () { + // Handle visualize action + }, + backgroundColor: Variables.textPrimary, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(1000), + ), + label: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.visibility, size: 20, color: Variables.textWhite), + const SizedBox(width: 8), + Text( + 'Visualize', + style: Variables.buttonTextStyle, ), ], - const SizedBox(height: 40), - Center(child: _buildGenerateButton("Regenerate")), - const SizedBox(height: 40), - ], + ), ), ), ); @@ -334,59 +347,217 @@ class _StylesheetPageState extends State<StylesheetPage> { ); } - Widget _buildSectionHeader(String title) { + Widget _buildSectionHeader(String title, {bool showArrow = true}) { return Padding( - padding: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.only(bottom: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( child: Text( - title.toUpperCase(), - style: const TextStyle(fontFamily: 'GeneralSans', fontSize: 12, fontWeight: FontWeight.bold, letterSpacing: 1.2, color: Variables.textSecondary), + title, + style: const TextStyle( + fontFamily: 'GeneralSans', + fontSize: 16, + fontWeight: FontWeight.w500, + height: 24 / 16, + color: Variables.textPrimary, + ), + ), + ), + if (showArrow) + Transform.rotate( + angle: 3.14159, // 180 degrees + child: SvgPicture.asset( + 'assets/icons/arrow-left-s-line.svg', + width: 24, + height: 24, + colorFilter: const ColorFilter.mode( + Variables.textPrimary, + BlendMode.srcIn, + ), + ), + ), + ], ), ); } - Widget _buildAssetsSection(List<String> imagePaths) { + Future<void> _pickLogo() async { + try { + final XFile? pickedFile = await _picker.pickImage(source: ImageSource.gallery); + if (pickedFile != null) { + setState(() { + _logoPaths.add(pickedFile.path); + }); + } + } catch (e) { + debugPrint("Error picking logo: $e"); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error picking logo: $e')), + ); + } + } + } + + Widget _buildLogosSection(dynamic data) { + // Use manually uploaded logos + List<String> logoPaths = List.from(_logoPaths); + + // Also add any logos from data if present + if (data is List) { + for (var item in data) { + if (item is Map && item.containsKey('path')) { + logoPaths.add(item['path'].toString()); + } else if (item is String) { + logoPaths.add(item); + } + } + } else if (data is String) { + logoPaths.add(data); + } + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: _buildSectionHeader("Subjects & Assets"), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: GridView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, crossAxisSpacing: 12, mainAxisSpacing: 12, childAspectRatio: 0.8), - itemCount: imagePaths.length, - itemBuilder: (context, index) => _buildAssetCard(imagePaths[index]), + _buildSectionHeader("Logos"), + const SizedBox(height: 12), + SizedBox( + height: 76, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: logoPaths.length + 1, // +1 for add button + separatorBuilder: (_, __) => const SizedBox(width: 8), + itemBuilder: (context, index) { + if (index == logoPaths.length) { + // Add button + return GestureDetector( + onTap: _pickLogo, + child: Container( + width: 76, + height: 76, + decoration: BoxDecoration( + border: Border.all(color: Variables.borderSubtle), + borderRadius: BorderRadius.circular(8), + ), + child: Center( + child: SvgPicture.asset( + 'assets/icons/add-line.svg', + width: 20, + height: 20, + colorFilter: const ColorFilter.mode( + Variables.textSecondary, + BlendMode.srcIn, + ), + ), + ), + ), + ); + } + return _buildLogoCard(logoPaths[index]); + }, ), ), - const SizedBox(height: 32), + const SizedBox(height: 16), ], ); } - Widget _buildAssetCard(String savedPath) { + Widget _buildLogoCard(String savedPath) { return FutureBuilder<File?>( future: _resolveFile(savedPath), builder: (context, snapshot) { final File? file = snapshot.data; return Container( + width: 76, + height: 76, decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Variables.borderSubtle), - boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2))], ), clipBehavior: Clip.antiAlias, - child: file != null ? Image.file(file, fit: BoxFit.cover) : Container(color: Colors.grey.shade100, child: const Center(child: Icon(Icons.broken_image, color: Colors.grey, size: 20))), + child: file != null + ? Image.file(file, fit: BoxFit.cover) + : Container( + color: Variables.surfaceSubtle, + child: const Center( + child: Icon(Icons.broken_image, color: Colors.grey, size: 20), + ), + ), ); }, ); } - Widget _buildTypographyCard(String rawFontName) { + Widget _buildGraphicsSection(dynamic data) { + List<String> graphicPaths = []; + + // Add project assets (subjects and assets) to graphics + graphicPaths.addAll(_projectAssets); + + // Add graphics from data if present + if (data is List) { + for (var item in data) { + if (item is Map && item.containsKey('path')) { + graphicPaths.add(item['path'].toString()); + } else if (item is String) { + graphicPaths.add(item); + } + } + } else if (data is String) { + graphicPaths.add(data); + } + + if (graphicPaths.isEmpty) return const SizedBox(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSectionHeader("Graphics"), + const SizedBox(height: 12), + SizedBox( + height: 106, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: graphicPaths.length, + separatorBuilder: (_, __) => const SizedBox(width: 8), + itemBuilder: (context, index) => _buildGraphicCard(graphicPaths[index]), + ), + ), + const SizedBox(height: 16), + ], + ); + } + + Widget _buildGraphicCard(String savedPath) { + return FutureBuilder<File?>( + future: _resolveFile(savedPath), + builder: (context, snapshot) { + final File? file = snapshot.data; + return Container( + width: 104, + height: 106, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Variables.surfaceSubtle, + ), + clipBehavior: Clip.antiAlias, + child: file != null + ? Image.file(file, fit: BoxFit.cover) + : Container( + color: Variables.surfaceSubtle, + child: const Center( + child: Icon(Icons.broken_image, color: Colors.grey, size: 20), + ), + ), + ); + }, + ); + } + + + Widget _buildFontCard(String rawFontName) { final String correctFontName = _resolveGoogleFontName(rawFontName); TextStyle sampleStyle; try { @@ -395,26 +566,48 @@ class _StylesheetPageState extends State<StylesheetPage> { sampleStyle = const TextStyle(fontFamily: 'GeneralSans'); } - return Container( - width: 160, padding: const EdgeInsets.all(20), + // Get font info (styles count) - simplified for now + String fontInfo = "16 styles + variable cut"; + + return Builder( + builder: (context) => Container( + width: (MediaQuery.of(context).size.width - 32 - 16) / 3, // 3 cards with spacing + height: 120, + padding: const EdgeInsets.all(9), decoration: BoxDecoration( - color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: Variables.borderSubtle), - boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2))], + borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded(child: Text("Aa", style: sampleStyle.copyWith(fontSize: 56, height: 1, fontWeight: FontWeight.w400, color: Colors.black))), - Text(correctFontName, style: const TextStyle(fontFamily: 'GeneralSans', fontSize: 16, fontWeight: FontWeight.w600, color: Colors.black), maxLines: 1, overflow: TextOverflow.ellipsis), - const SizedBox(height: 4), - const Text("Primary Typeface", style: TextStyle(fontFamily: 'GeneralSans', fontSize: 11, color: Variables.textSecondary, fontWeight: FontWeight.w500)), - ], + Text( + "Aa", + style: sampleStyle.copyWith( + fontSize: 28, + height: 1.2, + fontWeight: FontWeight.w400, + color: Variables.textPrimary, + ), + ), + Text( + fontInfo, + style: const TextStyle( + fontFamily: 'GeneralSans', + fontSize: 12, + height: 16 / 12, + color: Variables.textSecondary, + fontWeight: FontWeight.w400, + ), + ), + ], + ), ), ); } - Widget _buildTypographySection(dynamic data) { + Widget _buildFontsSection(dynamic data) { List<String> fontNames = []; if (data is List) { for (var item in data) { @@ -431,110 +624,416 @@ class _StylesheetPageState extends State<StylesheetPage> { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding(padding: const EdgeInsets.symmetric(horizontal: 20), child: _buildSectionHeader("Typography")), + _buildSectionHeader("Fonts"), + const SizedBox(height: 12), SizedBox( - height: 150, + height: 120, child: ListView.separated( - padding: const EdgeInsets.symmetric(horizontal: 20), scrollDirection: Axis.horizontal, itemCount: fontNames.length, - separatorBuilder: (_, __) => const SizedBox(width: 12), - itemBuilder: (context, index) => _buildTypographyCard(fontNames[index]), + separatorBuilder: (_, __) => const SizedBox(width: 8), + itemBuilder: (context, index) => _buildFontCard(fontNames[index]), ), ), - const SizedBox(height: 32), + const SizedBox(height: 16), ], ); } - Widget _buildColorSection(dynamic data) { - List<Map<String, dynamic>> palette = []; + Widget _buildColorsSection(dynamic data) { + List<String> colorList = []; if (data is List) { for (var item in data) { - if (item is Map) palette.add({'label': item['label']?.toString() ?? '', 'score': item['score'] ?? 0}); + if (item is Map && item.containsKey('label')) { + colorList.add(item['label'].toString()); + } else if (item is String) { + colorList.add(item); + } } + } else if (data is String) { + colorList.add(data); } - if (palette.isEmpty) return const SizedBox(); - return GridView.builder( - shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, crossAxisSpacing: 12, mainAxisSpacing: 12, childAspectRatio: 0.8), - itemCount: palette.length, - itemBuilder: (context, index) => _buildColorCard(palette[index]['label']), + if (colorList.isEmpty) return const SizedBox(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSectionHeader("Colors", showArrow: false), + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: colorList.take(5).map((color) => _buildColorSwatch(color)).toList(), + ), + const SizedBox(height: 16), + ], ); } - Widget _buildColorCard(String label) { + Widget _buildColorSwatch(String label) { Color color = _getColorFromLabel(label); - String hexCode = label.toUpperCase(); - return Container( - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all(color: Variables.borderSubtle)), - clipBehavior: Clip.antiAlias, - child: Column( + width: 104, + height: 52, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(8), + ), + ); + } + + Widget _buildCompositionsSection(dynamic data) { + List<String> compositionPaths = []; + if (data is List) { + for (var item in data) { + if (item is Map && item.containsKey('path')) { + compositionPaths.add(item['path'].toString()); + } else if (item is String) { + compositionPaths.add(item); + } + } + } else if (data is String) { + compositionPaths.add(data); + } + + if (compositionPaths.isEmpty) return const SizedBox(); + + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded(flex: 3, child: Container(color: color)), - Expanded( - flex: 2, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(hexCode, style: const TextStyle(fontFamily: 'GeneralSans', fontSize: 12, fontWeight: FontWeight.bold, color: Variables.textPrimary)), - const SizedBox(height: 2), - const Text("HEX", style: TextStyle(fontFamily: 'GeneralSans', fontSize: 10, color: Variables.textSecondary, overflow: TextOverflow.ellipsis), maxLines: 1), - ], - ), - ), + _buildSectionHeader("Compositions"), + const SizedBox(height: 12), + SizedBox( + height: 106, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: compositionPaths.length, + separatorBuilder: (_, __) => const SizedBox(width: 8), + itemBuilder: (context, index) => _buildCompositionCard(compositionPaths[index]), ), - ], - ), + ), + const SizedBox(height: 16), + ], ); } - Widget _buildSliderSection(String title, dynamic data) { - if (data is! List) return const SizedBox(); - List<Map> items = []; - for (var i in data) { - if (i is Map) items.add(i); + Widget _buildCompositionCard(String savedPath) { + return FutureBuilder<File?>( + future: _resolveFile(savedPath), + builder: (context, snapshot) { + final File? file = snapshot.data; + return Container( + width: 104, + height: 106, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Variables.surfaceSubtle, + ), + clipBehavior: Clip.antiAlias, + child: file != null + ? Image.file(file, fit: BoxFit.cover) + : Container( + color: Variables.surfaceSubtle, + child: const Center( + child: Icon(Icons.broken_image, color: Colors.grey, size: 20), + ), + ), + ); + }, + ); + } + + Widget _buildMaterialLookSection(dynamic data) { + List<String> materialPaths = []; + if (data is List) { + for (var item in data) { + if (item is Map && item.containsKey('path')) { + materialPaths.add(item['path'].toString()); + } else if (item is String) { + materialPaths.add(item); + } + } + } else if (data is String) { + materialPaths.add(data); + } + + if (materialPaths.isEmpty) return const SizedBox(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSectionHeader("Material look"), + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: materialPaths.take(2).map((path) => _buildMaterialCard(path)).toList(), + ), + const SizedBox(height: 16), + ], + ); + } + + Widget _buildMaterialCard(String savedPath) { + return FutureBuilder<File?>( + future: _resolveFile(savedPath), + builder: (context, snapshot) { + final File? file = snapshot.data; + return Container( + width: 106, + height: 106, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Variables.surfaceSubtle, + ), + clipBehavior: Clip.antiAlias, + child: file != null + ? Image.file(file, fit: BoxFit.cover) + : Container( + color: Variables.surfaceSubtle, + child: const Center( + child: Icon(Icons.broken_image, color: Colors.grey, size: 20), + ), + ), + ); + }, + ); + } + + Widget _buildTexturesSection(dynamic data) { + List<String> texturePaths = []; + if (data is List) { + for (var item in data) { + if (item is Map && item.containsKey('path')) { + texturePaths.add(item['path'].toString()); + } else if (item is String) { + texturePaths.add(item); + } + } + } else if (data is String) { + texturePaths.add(data); } - items.sort((a, b) => (b['score'] ?? 0).compareTo(a['score'] ?? 0)); + + if (texturePaths.isEmpty) return const SizedBox(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding(padding: const EdgeInsets.symmetric(horizontal: 20), child: _buildSectionHeader(title)), + _buildSectionHeader("Textures"), + const SizedBox(height: 12), SizedBox( - height: 120, + height: 106, child: ListView.separated( - padding: const EdgeInsets.symmetric(horizontal: 20), scrollDirection: Axis.horizontal, - itemCount: items.length, - separatorBuilder: (_, __) => const SizedBox(width: 12), - itemBuilder: (context, index) { - final item = items[index]; + itemCount: texturePaths.length, + separatorBuilder: (_, __) => const SizedBox(width: 8), + itemBuilder: (context, index) => _buildTextureCard(texturePaths[index]), + ), + ), + const SizedBox(height: 16), + ], + ); + } + + Widget _buildTextureCard(String savedPath) { + return FutureBuilder<File?>( + future: _resolveFile(savedPath), + builder: (context, snapshot) { + final File? file = snapshot.data; return Container( - width: 120, height: 120, padding: const EdgeInsets.all(12), + width: 104, + height: 106, decoration: BoxDecoration( - color: Colors.white, borderRadius: BorderRadius.circular(16), - border: Border.all(color: Variables.borderSubtle), - boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2))], + borderRadius: BorderRadius.circular(8), + color: Variables.surfaceSubtle, + ), + clipBehavior: Clip.antiAlias, + child: file != null + ? Image.file(file, fit: BoxFit.cover) + : Container( + color: Variables.surfaceSubtle, + child: const Center( + child: Icon(Icons.broken_image, color: Colors.grey, size: 20), + ), ), - child: Center( - child: Text(item['label']?.toString().toUpperCase() ?? '', textAlign: TextAlign.center, style: const TextStyle(fontFamily: 'GeneralSans', fontSize: 13, fontWeight: FontWeight.w600, color: Variables.textPrimary, height: 1.2), maxLines: 3, overflow: TextOverflow.ellipsis), + ); + }, + ); + } + + Widget _buildLightingSection(dynamic data) { + List<String> lightingPaths = []; + if (data is List) { + for (var item in data) { + if (item is Map && item.containsKey('path')) { + lightingPaths.add(item['path'].toString()); + } else if (item is String) { + lightingPaths.add(item); + } + } + } else if (data is String) { + lightingPaths.add(data); + } + + if (lightingPaths.isEmpty) return const SizedBox(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSectionHeader("Lighting"), + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: lightingPaths.take(3).map((path) => _buildLightingCard(path)).toList(), + ), + const SizedBox(height: 16), + ], + ); + } + + Widget _buildLightingCard(String savedPath) { + return FutureBuilder<File?>( + future: _resolveFile(savedPath), + builder: (context, snapshot) { + final File? file = snapshot.data; + return Container( + width: 106, + height: 106, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Variables.surfaceSubtle, + ), + clipBehavior: Clip.antiAlias, + child: file != null + ? Image.file(file, fit: BoxFit.cover) + : Container( + color: Variables.surfaceSubtle, + child: const Center( + child: Icon(Icons.broken_image, color: Colors.grey, size: 20), + ), ), ); }, - ), + ); + } + + Widget _buildStyleSection(dynamic data) { + List<String> styleTags = []; + if (data is List) { + for (var item in data) { + if (item is Map && item.containsKey('label')) { + styleTags.add(item['label'].toString()); + } else if (item is String) { + styleTags.add(item); + } + } + } else if (data is String) { + styleTags.add(data); + } + + if (styleTags.isEmpty) return const SizedBox(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSectionHeader("Style"), + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: styleTags.take(4).map((tag) => _buildTagCard(tag)).toList(), + ), + const SizedBox(height: 16), + ], + ); + } + + Widget _buildEraSection(dynamic data) { + List<String> eraTags = []; + if (data is List) { + for (var item in data) { + if (item is Map && item.containsKey('label')) { + eraTags.add(item['label'].toString()); + } else if (item is String) { + eraTags.add(item); + } + } + } else if (data is String) { + eraTags.add(data); + } + + if (eraTags.isEmpty) return const SizedBox(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSectionHeader("Era"), + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: eraTags.take(4).map((tag) => _buildTagCard(tag)).toList(), + ), + const SizedBox(height: 16), + ], + ); + } + + Widget _buildEmotionsSection(dynamic data) { + List<String> emotionTags = []; + if (data is List) { + for (var item in data) { + if (item is Map && item.containsKey('label')) { + emotionTags.add(item['label'].toString()); + } else if (item is String) { + emotionTags.add(item); + } + } + } else if (data is String) { + emotionTags.add(data); + } + + if (emotionTags.isEmpty) return const SizedBox(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSectionHeader("Emotions"), + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: emotionTags.take(4).map((tag) => _buildTagCard(tag)).toList(), ), - const SizedBox(height: 32), + const SizedBox(height: 16), ], ); } + Widget _buildTagCard(String label) { + return Container( + width: 160, + height: 54, + decoration: BoxDecoration( + color: Variables.surfaceSubtle, + borderRadius: BorderRadius.circular(8), + ), + alignment: Alignment.center, + child: Text( + label, + style: const TextStyle( + fontFamily: 'GeneralSans', + fontSize: 16, + fontWeight: FontWeight.w500, + height: 24 / 16, + color: Variables.textPrimary, + ), + ), + ); + } + Color _getColorFromLabel(String label) { if (label.startsWith('#') || label.length == 6) { try { diff --git a/lib/ui/widgets/top_bar.dart b/lib/ui/widgets/top_bar.dart @@ -259,7 +259,7 @@ class _TopBarState extends State<TopBar> { }).toList(); }, ), - // Layout Icon Button (Toggle between All Images/Categorized) + // Layout Icon Button (Toggle between All Images/Categorized) or Edit Icon if (widget.onLayoutToggle != null || widget.onLayoutPressed != null) ...[ const SizedBox(width: 8), GestureDetector( @@ -274,9 +274,11 @@ class _TopBarState extends State<TopBar> { borderRadius: BorderRadius.circular(1000), ), child: Icon( - widget.isAlternateView == true - ? Icons.dashboard - : Icons.view_agenda_outlined, + widget.onLayoutToggle != null + ? (widget.isAlternateView == true + ? Icons.dashboard + : Icons.view_agenda_outlined) + : Icons.edit, // Use edit icon when only onLayoutPressed is provided size: 20, color: Variables.textPrimary, ),