creek

The AI Image Editor of 2030
commit 960b59c9bf8a2dd1c1b77e004492e7cbf0e295d7
parent 8c99ca2761b4b7abd7b84f826e242da9acd74ace
Author: Abhinav Rai <69450646+AbhinavRai01@users.noreply.github.com>
Date:   Sun, 30 Nov 2025 16:22:16 +0530

Stylesheet page implemented

Stylesheet design implemented
Diffstat:
Alib/services/flask_service.dart | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/ui/pages/stylesheet_page.dart | 673+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Mpubspec.yaml | 1+
3 files changed, 593 insertions(+), 153 deletions(-)

diff --git a/lib/services/flask_service.dart b/lib/services/flask_service.dart @@ -0,0 +1,71 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart' as http; +import 'package:path_provider/path_provider.dart'; + +class GenerateImageService { + // Replace with your actual server URL. + // Android Emulator: 'http://10.0.2.2:5000/generate' + // iOS Simulator: 'http://127.0.0.1:5000/generate' + // Physical Device: 'http://YOUR_PC_IP:5000/generate' + static const String _serverUrl = 'http://10.0.2.2:5000/generate'; + + /// Sends a prompt to the Flask server, decodes the Base64 image, + /// saves it as a PNG, and returns the file path. + Future<String?> generateAndSaveImage(String prompt) async { + try { + debugPrint("🎨 Sending prompt to server: '$prompt'..."); + + // 1. Send POST request to Flask API + final response = await http.post( + Uri.parse(_serverUrl), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'prompt': prompt}), + ); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + + if (data['image'] != null) { + final String base64String = data['image']; + + // 2. Decode Base64 string to bytes + final Uint8List imageBytes = base64Decode(base64String); + + // 3. Get directory to save the file + final directory = await getApplicationDocumentsDirectory(); + final imagesDir = Directory('${directory.path}/generated_images'); + if (!await imagesDir.exists()) { + await imagesDir.create(recursive: true); + } + + // 4. Generate unique filename + final timestamp = DateTime.now().millisecondsSinceEpoch; + // Sanitize prompt for filename (remove special chars) + final safePrompt = prompt.replaceAll(RegExp(r'[^\w\s]'), '').trim().replaceAll(' ', '_'); + // Limit filename length + final shortPrompt = safePrompt.length > 20 ? safePrompt.substring(0, 20) : safePrompt; + + final String filePath = '${imagesDir.path}/${shortPrompt}_$timestamp.png'; + final File file = File(filePath); + + // 5. Save bytes to file + await file.writeAsBytes(imageBytes); + + debugPrint("✅ Image saved successfully at: $filePath"); + return filePath; + } else { + debugPrint("❌ Server response missing 'image' field."); + return null; + } + } else { + debugPrint("❌ Server Error: ${response.statusCode} - ${response.body}"); + return null; + } + } catch (e) { + debugPrint("❌ Error generating image: $e"); + return null; + } + } +} +\ No newline at end of file diff --git a/lib/ui/pages/stylesheet_page.dart b/lib/ui/pages/stylesheet_page.dart @@ -1,6 +1,6 @@ import 'dart:convert'; import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; +import 'package:google_fonts/google_fonts.dart'; import 'package:adobe/ui/styles/variables.dart'; import 'package:adobe/ui/widgets/bottom_bar.dart'; import 'package:adobe/ui/widgets/top_bar.dart'; @@ -11,7 +11,10 @@ import 'package:adobe/services/python_service.dart'; class StylesheetPage extends StatefulWidget { final int projectId; - const StylesheetPage({super.key, required this.projectId}); + const StylesheetPage({ + super.key, + required this.projectId, + }); @override State<StylesheetPage> createState() => _StylesheetPageState(); @@ -20,7 +23,12 @@ class StylesheetPage extends StatefulWidget { class _StylesheetPageState extends State<StylesheetPage> { late int _currentProjectId; bool _isLoading = false; - String? _resultJson; + + Map<String, dynamic>? _stylesheetMap; + String? _rawJsonString; + + // Cache for font name lookups + final Map<String, String> _fontNameCache = {}; @override void initState() { @@ -29,205 +37,563 @@ class _StylesheetPageState extends State<StylesheetPage> { _loadSavedStylesheet(); } + // --- PARSING LOGIC --- + String _cleanJsonString(String raw) { + String cleaned = raw; + cleaned = cleaned.replaceAllMapped( + RegExp(r'([{,]\s*)([a-zA-Z0-9_\s/]+)(\s*:)'), + (match) => '${match[1]}"${match[2]?.trim()}"${match[3]}' + ); + cleaned = cleaned.replaceAllMapped( + RegExp(r'(:\s*)([a-zA-Z0-9_\-\.\/\s]+)(?=\s*[,}])'), + (match) { + String val = match[2]!.trim(); + if (val == 'true' || val == 'false' || val == 'null' || double.tryParse(val) != null) { + return match[0]!; + } + return '${match[1]}"$val"'; + } + ); + return cleaned; + } + + String _resolveGoogleFontName(String dirtyName) { + if (_fontNameCache.containsKey(dirtyName)) { + return _fontNameCache[dirtyName]!; + } + + String cleanInput = dirtyName.toLowerCase() + .replaceAll(RegExp(r'[-_]regular$'), '') + .replaceAll(RegExp(r'[^a-z0-9]'), ''); + + final allFonts = GoogleFonts.asMap().keys; + + for (String officialName in allFonts) { + String cleanOfficial = officialName.toLowerCase().replaceAll(RegExp(r'[^a-z0-9]'), ''); + if (cleanOfficial == cleanInput) { + _fontNameCache[dirtyName] = officialName; + return officialName; + } + } + return dirtyName; + } + Future<void> _loadSavedStylesheet() async { final project = await ProjectRepo().getProjectById(_currentProjectId); - if (project?.globalStylesheet != null && - project!.globalStylesheet!.isNotEmpty) { + if (project?.globalStylesheet != null && project!.globalStylesheet!.isNotEmpty) { + String raw = project.globalStylesheet!; + dynamic parsed; + try { - final parsed = jsonDecode(project.globalStylesheet!); - const encoder = JsonEncoder.withIndent(' '); + parsed = jsonDecode(raw); + } catch (e) { + try { + parsed = jsonDecode(_cleanJsonString(raw)); + } catch (_) {} + } + + if (parsed is String) { + try { parsed = jsonDecode(parsed); } catch (_) {} + } + + if (mounted) { setState(() { - _resultJson = encoder.convert(parsed); + _rawJsonString = raw; + if (parsed is Map<String, dynamic>) { + if (parsed.containsKey('results') && parsed['results'] is Map) { + _stylesheetMap = parsed['results']; + } else { + _stylesheetMap = parsed; + } + } else { + _stylesheetMap = null; + } }); - } catch (_) {} + } } } Future<void> _generateStylesheet() async { setState(() { _isLoading = true; - _resultJson = null; + _stylesheetMap = null; + _rawJsonString = null; }); try { - // 1. Fetch images for this project final images = await ImageRepo().getImages(_currentProjectId); + final analysisData = images + .map((img) => img.analysisData) + .where((data) => data != null && data.isNotEmpty) + .cast<String>() + .toList(); - // 2. Extract valid analysis data - final List<String> analysisDataList = - images - .map((img) => img.analysisData) - .where((data) => data != null && data.isNotEmpty) - .cast<String>() - .toList(); - - if (analysisDataList.isEmpty) { + if (analysisData.isEmpty) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("No analyzed images found for this project."), - ), + const SnackBar(content: Text("No analyzed images found.")), ); } - setState(() { - _isLoading = false; - }); return; } - // 3. Call Python Service - final result = await PythonService().generateStylesheet(analysisDataList); - if (mounted) { - if (result != null) { - const encoder = JsonEncoder.withIndent(' '); - final jsonString = encoder.convert(result); - - // 4. Save to Database - await ProjectRepo().updateStylesheet(_currentProjectId, jsonString); - setState(() { - _resultJson = jsonString; - }); - } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("Failed to generate stylesheet.")), - ); - } + final result = await PythonService().generateStylesheet(analysisData); + + if (mounted && result != null) { + final jsonString = jsonEncode(result); + await ProjectRepo().updateStylesheet(_currentProjectId, jsonString); + + setState(() { + if (result.containsKey('results') && result['results'] is Map) { + _stylesheetMap = result['results']; + } else { + _stylesheetMap = result; + } + _rawJsonString = jsonString; + }); } } catch (e) { - debugPrint("Error generating stylesheet: $e"); - if (mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text("Error: $e"))); - } + debugPrint("Gen Error: $e"); } finally { - if (mounted) { - setState(() { - _isLoading = false; - }); + if (mounted) setState(() => _isLoading = false); + } + } + + dynamic _getData(List<String> keys) { + if (_stylesheetMap == null) return null; + for (var k in keys) { + if (_stylesheetMap!.containsKey(k)) return _stylesheetMap![k]; + for (var mapKey in _stylesheetMap!.keys) { + if (mapKey.toLowerCase() == k.toLowerCase()) return _stylesheetMap![mapKey]; } } + return null; } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Variables.background, - appBar: TopBar( currentProjectId: _currentProjectId, onBack: () => Navigator.of(context).pop(), - onProjectChanged: (newProject) { - setState(() { - _currentProjectId = newProject.id!; - _resultJson = null; - _loadSavedStylesheet(); - }); - }, + onProjectChanged: (p) => setState(() { + _currentProjectId = p.id!; + _stylesheetMap = null; + _loadSavedStylesheet(); + }), + ), + body: _isLoading + ? const Center(child: CircularProgressIndicator(color: Variables.textPrimary)) + : (_stylesheetMap == null && _rawJsonString == null) + ? _buildEmptyState() + : _buildContent(), + bottomNavigationBar: BottomBar( + currentTab: BottomBarItem.stylesheet, + projectId: _currentProjectId, ), + ); + } - body: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics(), - child: ConstrainedBox( - constraints: BoxConstraints(minHeight: constraints.maxHeight), - child: Center( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 24.0, - vertical: 20, + Widget _buildEmptyState() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text("No stylesheet data.", style: Variables.headerStyle.copyWith(fontSize: 18)), + const SizedBox(height: 24), + _buildGenerateButton("Generate Stylesheet"), + ], + ), + ); + } + + Widget _buildContent() { + final style = _getData(['Style', 'style']); + final lighting = _getData(['Lighting', 'lighting']); + final colors = _getData(['Colour Palette', 'Color Palette', 'colors']); + final emotions = _getData(['Emotions', 'emotions']); + final era = _getData(['Era/Cultural Reference', 'era']); + final typography = _getData(['Typography', 'fonts']); + + final foundAny = (style != null || lighting != null || colors != null || emotions != null || era != null || typography != null); + + return RefreshIndicator( + onRefresh: _generateStylesheet, + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(vertical: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + "Visual Identity", + style: TextStyle(fontFamily: 'GeneralSans', fontSize: 24, fontWeight: FontWeight.w600), ), + IconButton( + icon: const Icon(Icons.refresh), + onPressed: _generateStylesheet, + ), + ], + ), + ), + const SizedBox(height: 32), + + if (foundAny) ...[ + // 1. Typography (Now a Slider) + if (typography != null) + _buildTypographySection(typography), + + // 2. Color Palette + if (colors != null) ...[ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), child: Column( - mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - // No saved result - if (_resultJson == null && !_isLoading) ...[ - Text( - "Are you ready to start\nbuilding the visual identity?", - textAlign: TextAlign.center, - style: Variables.headerStyle, - ), - const SizedBox(height: 32), - ], - - // Loading - if (_isLoading) - const CircularProgressIndicator( - color: Variables.textPrimary, - ), - - // Result JSON - if (_resultJson != null) - Container( - width: double.infinity, - padding: const EdgeInsets.all(16), - margin: const EdgeInsets.only(bottom: 24), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.grey.shade300), - ), - child: Text( - _resultJson!, - style: const TextStyle( - fontFamily: 'monospace', - fontSize: 12, - ), - ), - ), - - // Button - if (!_isLoading) ...[ - if (_resultJson != null) const SizedBox(height: 24), - GestureDetector( - onTap: _generateStylesheet, - child: Container( - width: 274, - height: 44, - decoration: BoxDecoration( - color: Variables.textPrimary, - borderRadius: BorderRadius.circular(112), - ), - alignment: Alignment.center, - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - _resultJson == null - ? "Generate Stylesheet" - : "Regenerate Stylesheet", - style: Variables.buttonTextStyle, - ), - const SizedBox(width: 8), - SvgPicture.asset( - "assets/icons/generate_icon.svg", - width: 18, - height: 18, - colorFilter: const ColorFilter.mode( - Variables.textWhite, - BlendMode.srcIn, - ), - ), - ], - ), - ), - ), - ], + _buildSectionHeader("Color Palette"), + _buildColorSection(colors), ], ), ), + const SizedBox(height: 32), + ], + + // 3. Slider Sections + 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), + + ] 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"), + ), + ), + ], + + const SizedBox(height: 40), + Center(child: _buildGenerateButton("Regenerate")), + const SizedBox(height: 40), + ], + ), + ), + ); + } + + Widget _buildGenerateButton(String label) { + return GestureDetector( + onTap: _generateStylesheet, + child: Container( + width: 200, height: 44, + decoration: BoxDecoration( + color: Variables.textPrimary, + borderRadius: BorderRadius.circular(112), + ), + alignment: Alignment.center, + child: Text(label, style: Variables.buttonTextStyle), + ), + ); + } + + Widget _buildSectionHeader(String title) { + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text( + title.toUpperCase(), + style: const TextStyle( + fontFamily: 'GeneralSans', fontSize: 12, fontWeight: FontWeight.bold, + letterSpacing: 1.2, color: Variables.textSecondary, + ), + ), + ); + } + + // --- TYPOGRAPHY SECTION (UPDATED TO SLIDER) --- + Widget _buildTypographySection(dynamic data) { + List<String> fontNames = []; + + if (data is List) { + for (var item in data) { + if (item is Map && item.containsKey('label')) { + fontNames.add(item['label'].toString().trim()); + } else if (item is String) { + fontNames.add(item.trim()); + } + } + } else if (data is Map && data.containsKey('label')) { + fontNames.add(data['label'].toString().trim()); + } else if (data is String) { + fontNames.add(data.trim()); + } + + if (fontNames.isEmpty) return const SizedBox(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: _buildSectionHeader("Typography"), + ), + SizedBox( + height: 150, // Height for the cards + 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]), + ), + ), + const SizedBox(height: 32), + ], + ); + } + + Widget _buildTypographyCard(String rawFontName) { + final String correctFontName = _resolveGoogleFontName(rawFontName); + TextStyle sampleStyle; + + try { + sampleStyle = GoogleFonts.getFont(correctFontName); + } catch (_) { + sampleStyle = const TextStyle(fontFamily: 'GeneralSans'); + } + + return Container( + width: 160, // Fixed Width for Horizontal List + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Variables.borderSubtle), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.03), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 1. Big "Aa" Preview + Expanded( + child: Text( + "Aa", + style: sampleStyle.copyWith( + fontSize: 56, + height: 1, + fontWeight: FontWeight.w400, + color: Colors.black ), ), - ); - }, + ), + // 2. Font Name + Text( + correctFontName, + style: const TextStyle( + fontFamily: 'GeneralSans', + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.black, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + // 3. Label + const Text( + "Primary Typeface", + style: TextStyle( + fontFamily: 'GeneralSans', + fontSize: 11, + color: Variables.textSecondary, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } + + Widget _buildColorSection(dynamic data) { + List<Map<String, dynamic>> palette = []; + if (data is List) { + for (var item in data) { + if (item is Map) { + palette.add({ + 'label': item['label']?.toString() ?? '', + 'score': item['score'] ?? 0, + }); + } + } + } + + 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) { + return _buildColorCard(palette[index]['label']); + }, + ); + } - bottomNavigationBar: BottomBar( - currentTab: BottomBarItem.stylesheet, - projectId: _currentProjectId, + Widget _buildColorCard(String label) { + Color color = _getColorFromLabel(label); + String hexCode = "#${color.value.toRadixString(16).substring(2).toUpperCase()}"; + + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Variables.borderSubtle), + ), + clipBehavior: Clip.antiAlias, + child: 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), + Text( + label.toUpperCase(), + style: const TextStyle( + fontFamily: 'GeneralSans', + fontSize: 10, + color: Variables.textSecondary, + overflow: TextOverflow.ellipsis, + ), + maxLines: 1, + ), + ], + ), + ), + ), + ], ), ); } -} + + 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); + } + items.sort((a, b) => (b['score'] ?? 0).compareTo(a['score'] ?? 0)); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: _buildSectionHeader(title), + ), + SizedBox( + height: 120, + 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]; + final label = item['label']?.toString() ?? ''; + + return Container( + width: 120, + height: 120, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Variables.borderSubtle), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.03), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Center( + child: Text( + label.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, + ), + ), + ); + }, + ), + ), + const SizedBox(height: 32), + ], + ); + } + + Color _getColorFromLabel(String label) { + label = label.toLowerCase(); + if (label.contains('neon')) return const Color(0xFF39FF14); + if (label.contains('earth')) return const Color(0xFF8D6E63); + if (label.contains('pastel')) return const Color(0xFFFFB7B2); + if (label.contains('neutral')) return const Color(0xFFE0E0E0); + if (label.contains('vintage')) return const Color(0xFFD2B48C); + if (label.contains('modern')) return const Color(0xFF212121); + if (label.contains('warm')) return const Color(0xFFFF9800); + if (label.contains('cool')) return const Color(0xFF00BCD4); + if (label.contains('dark')) return const Color(0xFF1a1a1a); + return Colors.grey.shade400; + } +} +\ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml @@ -25,6 +25,7 @@ dependencies: intl: ^0.20.2 google_mlkit_text_recognition: ^0.15.0 flutter_svg: ^2.2.3 + google_fonts: ^6.1.0 dev_dependencies: flutter_test: