creek

The AI Image Editor of 2030
commit ba31c4b30ed8ad2b837a1e7a8d8ae60c82949c8a
parent 6ce8861827f4b98a4a65274fecc1421a1415adb9
Author: maydayv7 <maydayv7@gmail.com>
Date:   Tue,  2 Dec 2025 01:53:05 +0530

Update color analysis model

Diffstat:
Mandroid/app/src/main/python/color_style_infer.py | 280++++++++++++++++++++++++++++---------------------------------------------------
Dandroid/app/src/main/python/color_style_model.joblib | 0
Mandroid/app/src/main/python/stylesheet_generator.py | 54+++++++++++++++++++++++++++++++++---------------------
Mlib/services/analysis_queue_manager.dart | 50++++++++++++++++++++++++++++++--------------------
Mlib/services/analyze/image_analyzer.dart | 5++++-
Mlib/services/stylesheet_service.dart | 16----------------
Mlib/ui/pages/stylesheet_page.dart | 107+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
7 files changed, 226 insertions(+), 286 deletions(-)

diff --git a/android/app/src/main/python/color_style_infer.py b/android/app/src/main/python/color_style_infer.py @@ -1,198 +1,115 @@ -import os import sys import json import traceback import numpy as np import cv2 -import joblib - -# --- Global Cache --- -_MODEL_DATA = None - -class NumpyEncoder(json.JSONEncoder): - def default(self, obj): - if isinstance(obj, (np.integer, int)): - return int(obj) - elif isinstance(obj, (np.floating, float)): - return float(obj) - elif isinstance(obj, np.ndarray): - return obj.tolist() - return super(NumpyEncoder, self).default(obj) - -def _load_model_if_needed(): - global _MODEL_DATA - if _MODEL_DATA is not None: - return _MODEL_DATA - - try: - base_dir = os.path.dirname(__file__) - model_path = os.path.join(base_dir, "color_style_model.joblib") - if os.path.exists(model_path): - _MODEL_DATA = joblib.load(model_path) - except Exception as e: - print(f"Error loading model: {e}") - return _MODEL_DATA - -# --- Feature Extraction Helpers --- -def compute_color_features(bgr): - hsv = cv2.cvtColor(bgr, cv2.COLOR_BGR2HSV) - lab = cv2.cvtColor(bgr, cv2.COLOR_BGR2LAB) - H, S, V = cv2.split(hsv) - L, A, B = cv2.split(lab) - - def stats(x): - x = x.astype(np.float32) / 255.0 - return float(x.mean()), float(x.std()), float(np.percentile(x, 1)), float(np.percentile(x, 99)) - - color = {} - - # Lightness / brightness - mean_L, std_L, p1_L, p99_L = stats(L) - color.update({"mean_L": mean_L, "std_L": std_L, "p1_L": p1_L, "p99_L": p99_L}) - - # Saturation - mean_S, std_S, p1_S, p99_S = stats(S) - color.update({"mean_S": mean_S, "std_S": std_S, "p1_S": p1_S, "p99_S": p99_S}) - - # Value / luminance - mean_V, std_V, p1_V, p99_V = stats(V) - color.update({"mean_V": mean_V, "std_V": std_V, "p1_V": p1_V, "p99_V": p99_V}) - - # Hue stats - Hf = H.astype(np.float32) * 2.0 - rad = np.deg2rad(Hf) - sin_mean, cos_mean = np.sin(rad).mean(), np.cos(rad).mean() - hue_mean_deg = np.rad2deg(np.arctan2(sin_mean, cos_mean)) % 360 - R = np.sqrt(sin_mean**2 + cos_mean**2) - hue_dispersion = float(1 - R) - color.update({"hue_mean_deg": float(hue_mean_deg), "hue_dispersion": hue_dispersion}) - - # Colorfulness - rg = (bgr[:, :, 2].astype(np.float32) - bgr[:, :, 1].astype(np.float32)) - yb = 0.5 * (bgr[:, :, 2].astype(np.float32) + bgr[:, :, 1].astype(np.float32)) - bgr[:, :, 0].astype(np.float32) - sigma_rg, sigma_yb = rg.std(), yb.std() - mean_rg, mean_yb = rg.mean(), yb.mean() - colorfulness = np.sqrt(sigma_rg**2 + sigma_yb**2) + 0.3 * np.sqrt(mean_rg**2 + mean_yb**2) - color["colorfulness"] = float(colorfulness) - - # Palette - pixels = bgr.reshape(-1, 3).astype(np.float32) - K = 5 - criteria = (cv2.TermCriteria_EPS + cv2.TermCriteria_MAX_ITER, 20, 1.0) - _, _, centers = cv2.kmeans(pixels, K, None, criteria, 1, cv2.KMEANS_PP_CENTERS) - color["palette_bgr"] = centers.astype(int).tolist() - - return color - - -def compute_editing_features(bgr): - hsv = cv2.cvtColor(bgr, cv2.COLOR_BGR2HSV) - lab = cv2.cvtColor(bgr, cv2.COLOR_BGR2LAB) - H, S, V = cv2.split(hsv) - L, A, B = cv2.split(lab) - - feats = {} - - def stats(x): - x = x.astype(np.float32) / 255.0 - return float(x.mean()), float(x.std()), float(np.percentile(x, 1)), float(np.percentile(x, 99)) - - mean_V, std_V, p1_V, p99_V = stats(V) - mean_S, std_S, p1_S, p99_S = stats(S) - - feats["brightness_mean"] = mean_V - feats["brightness_range"] = p99_V - p1_V - - gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY).astype(np.float32) / 255.0 - feats["contrast_rms"] = float(gray.std()) - feats["saturation_mean"] = mean_S - feats["saturation_range"] = p99_S - p1_S - feats["tint_a_mean"] = float(A.mean()) - feats["tint_b_mean"] = float(B.mean()) - - Hf = H.astype(np.float32) * 2.0 - rad = np.deg2rad(Hf) - sin_mean, cos_mean = np.sin(rad).mean(), np.cos(rad).mean() - hue_mean_deg = np.rad2deg(np.arctan2(sin_mean, cos_mean)) % 360 - R = np.sqrt(sin_mean**2 + cos_mean**2) - feats["hue_mean_deg"] = float(hue_mean_deg) - feats["hue_dispersion"] = float(1 - R) - - patch_std = [] - step, k = 16, 16 - for y in range(0, gray.shape[0] - k + 1, step): - for x in range(0, gray.shape[1] - k + 1, step): - patch = gray[y:y + k, x:x + k] - patch_std.append(patch.std()) - if len(patch_std) > 0: - patch_std = np.array(patch_std) - feats["local_contrast_mean"] = float(patch_std.mean()) - feats["local_contrast_std"] = float(patch_std.std()) - else: - feats["local_contrast_mean"] = 0.0 - feats["local_contrast_std"] = 0.0 - - return feats - - -def flatten_features(features_dict): - flat_values = [] - # 1. Color Features - color_feats = features_dict.get('color', {}) - for key, val in color_feats.items(): - if key == 'palette_bgr': - flat_values.extend(np.array(val).flatten()) - else: - flat_values.append(val) - # 2. Editing Features - edit_feats = features_dict.get('editing', {}) - for key, val in edit_feats.items(): - flat_values.append(val) - return np.array(flat_values) - - -# --- Public API --- +from sklearn.cluster import KMeans + +# ========================================== +# HELPER FUNCTIONS +# ========================================== + +def get_dominant_colors(img_rgb, k=5): + """ + Extracts dominant colors using KMeans. + Expects a Numpy array (RGB). + """ + # Resize to speed up processing + img_small = cv2.resize(img_rgb, (150, 150), interpolation=cv2.INTER_AREA) + + # Reshape to a list of pixels + pixels = img_small.reshape((-1, 3)) + + # KMeans Clustering + # FIX: n_init='auto' crashes on older sklearn versions found in Chaquopy. + # We use n_init=10 which is the standard default for older versions. + kmeans = KMeans(n_clusters=k, n_init=10, random_state=42) + kmeans.fit(pixels) + + colors = kmeans.cluster_centers_.astype(int) + + # Sort by brightness (Sum of RGB channels) + return sorted(colors.tolist(), key=lambda x: sum(x)) + +def classify_mood(rgb_colors): + """ + Classifies mood based on HSV values. + Adapted to use OpenCV instead of Matplotlib to reduce APK size and dependencies. + """ + # Normalize RGB values to 0-1 range (Float32 required for CV2 conversion) + norm_colors = np.array(rgb_colors, dtype=np.float32) / 255.0 + + # Reshape to (1, N, 3) image format for cv2.cvtColor + img_reshaped = norm_colors.reshape(1, -1, 3) + + # Convert RGB to HSV + # OpenCV with float32 input returns: H[0-360], S[0-1], V[0-1] + hsv_img = cv2.cvtColor(img_reshaped, cv2.COLOR_RGB2HSV) + hsv_stats = hsv_img[0] # Shape (N, 3) + + # Normalize Hue to 0-1 range to match original logic (Matplotlib uses 0-1) + hsv_stats[:, 0] /= 360.0 + + # Extract averages + # hsv_stats structure is [Hue, Saturation, Value] + avg_sat = np.mean(hsv_stats[:, 1]) + avg_val = np.mean(hsv_stats[:, 2]) + + # Logic Rules + if avg_sat < 0.15 and avg_val > 0.65: return "Minimalist" + if avg_val < 0.35: return "Dark/Moody" + if avg_sat < 0.45 and avg_val > 0.75: return "Pastel" + if avg_sat > 0.65 and avg_val > 0.5: return "Neon" + + # Earthy logic: Hue between 0.02 and 0.42 (approx 7 to 150 deg), low saturation + earthy_votes = sum(1 for p in hsv_stats if (0.02 <= p[0] <= 0.42) and p[1] < 0.8) + if earthy_votes >= 3: return "Earthy" + + # Warm/Cool logic: Warm is usually red/orange/yellow (low Hue or very high Hue) + warm_votes = sum(1 for p in hsv_stats if p[0] < 0.17 or p[0] > 0.83) + return "Warm" if warm_votes >= 3 else "Cool" + +def rgb_to_hex(rgb): + return '#{:02x}{:02x}{:02x}'.format(rgb[0], rgb[1], rgb[2]) + +# ========================================== +# MAIN API +# ========================================== def analyze_color_style(image_path): try: - model_data = _load_model_if_needed() - if model_data is None: - return json.dumps({"success": False, "scores": {}, "error": "Model failed to load"}) - - if not os.path.exists(image_path): - return json.dumps({"success": False, "scores": {}, "error": f"Image not found at: {image_path}"}) - - img = cv2.imread(image_path) - if img is None: + # 1. Load Image + # OpenCV reads in BGR by default + img_bgr = cv2.imread(image_path) + if img_bgr is None: return json.dumps({"success": False, "scores": {}, "error": "CV2 could not read image"}) + + # Convert to RGB + img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) + + # 2. Extract Colors + colors = get_dominant_colors(img_rgb) + + # 3. Classify Mood + mood = classify_mood(colors) + + # 4. Format Results + hex_colors = [rgb_to_hex(c) for c in colors] - # Extract Features - raw_features = { - "color": compute_color_features(img), - "editing": compute_editing_features(img) - } - - flat_vector = flatten_features(raw_features) - - # Predict - clf = model_data['model'] - le = model_data['encoder'] - - probs = clf.predict_proba([flat_vector])[0] - classes = le.classes_ - - results = {} - for c, p in zip(classes, probs): - results[c] = float(p) - - sorted_features = sorted(results.items(), key=lambda x: x[1], reverse=True) response = { "success": True, - "scores": {k: float(v) for k, v in sorted_features}, #[:3] + # We return the mood as a score of 1.0 to maintain compatibility + # with existing UI components that expect a map of scores. + "scores": {mood: 1.0}, + "palette": hex_colors, "error": None, } - return json.dumps(response, cls=NumpyEncoder) + return json.dumps(response) except Exception as e: - return json.dumps({"success": False, "scores": {}, "error": f"Python Exception: {str(e)} | {traceback.format_exc()}"}) -\ No newline at end of file + return json.dumps({ + "success": False, + "scores": {}, + "error": f"Python Exception: {str(e)} | {traceback.format_exc()}" + }) diff --git a/android/app/src/main/python/color_style_model.joblib b/android/app/src/main/python/color_style_model.joblib Binary files differ. diff --git a/android/app/src/main/python/stylesheet_generator.py b/android/app/src/main/python/stylesheet_generator.py @@ -18,7 +18,7 @@ PALETTE_SIZE = 5 # Number of colors in final palette def hex_to_rgb(hex_str: str) -> List[int]: """Converts '#FF5733' to [255, 87, 51] for math operations.""" try: - hex_str = hex_str.lstrip('#') + hex_str = hex_str.strip().lstrip('#') if len(hex_str) != 6: return [0, 0, 0] return [int(hex_str[i:i+2], 16) for i in (0, 2, 4)] except: @@ -274,7 +274,7 @@ class UnifiedStyleEngine: self.doc_counter = 0 self.aliases = { - "Color Pallete": "Color Palette", + "Colour Palette": "Color Palette", "Texture": "Background/Texture", "Era": "Era/Cultural Reference", "Font": "Typography", @@ -311,15 +311,27 @@ class UnifiedStyleEngine: # Map "Font" -> "Typography", etc. std_category = self.aliases.get(category, category) - # Extract scores dictionary if nested - if isinstance(payload, dict) and "scores" in payload: - payload = payload["scores"] + # PATH A: COLOR PALETTE + # NOTE: We intentionally SKIP processing 'scores' (moods) here + if std_category == "Color Palette": + if isinstance(payload, dict): + # Extract "palette" list (Actual Hex Codes) + if "palette" in payload and isinstance(payload["palette"], list): + for hex_code in payload["palette"]: + if isinstance(hex_code, str) and hex_code.startswith('#'): + self.color_pool.append(hex_to_rgb(hex_code)) + # Fallback: if payload is just a list of hex strings + elif isinstance(payload, list): + for item in payload: + if isinstance(item, str) and item.startswith('#'): + self.color_pool.append(hex_to_rgb(item)) + + # PATH B: TYPOGRAPHY + elif std_category == "Typography": + if isinstance(payload, dict) and "scores" in payload: + payload = payload["scores"] + vectors = self._normalize(payload) - # Normalize to list of tuples - vectors = self._normalize(payload) - - # PATH A: TYPOGRAPHY - if std_category == "Typography": for rank, (font_name, score) in enumerate(vectors): # Skip "No Text Detected" if "no text" in font_name.lower(): continue @@ -332,16 +344,12 @@ class UnifiedStyleEngine: if font_name not in self.font_members[cluster]: self.font_members[cluster].append(font_name) - # PATH B: COLOR PALETTE - elif std_category == "Color Palette": - for rank, (hex_code, score) in enumerate(vectors): - # Ensure we only pick valid hex codes - if rank < 5 and isinstance(hex_code, str) and hex_code.startswith('#'): - rgb = hex_to_rgb(hex_code) - self.color_pool.append(rgb) - # PATH C: OTHER TAGS else: + if isinstance(payload, dict) and "scores" in payload: + payload = payload["scores"] + vectors = self._normalize(payload) + for rank, (label, score) in enumerate(vectors): self.feature_registry[std_category][label].append({ "raw_score": score, @@ -414,10 +422,14 @@ class UnifiedStyleEngine: final_json["results"]["Typography"] = typo_output final_json["results"]["Typography_Family"] = best_fam - # 3. Process Color Palette (K-Means) - if len(self.color_pool) >= PALETTE_SIZE: + # 3. Process Color Palette + if len(self.color_pool) > 0: try: - kmeans = KMeans(n_clusters=PALETTE_SIZE, n_init='auto', random_state=42) + # If we have very few colors, just use all of them + k_clusters = min(len(self.color_pool), PALETTE_SIZE) + + # FIX: n_init='auto' crashes on older sklearn. Used n_init=1. + kmeans = KMeans(n_clusters=k_clusters, n_init=1, random_state=42) kmeans.fit(self.color_pool) centers = sorted(kmeans.cluster_centers_.astype(int).tolist(), key=sum) diff --git a/lib/services/analysis_queue_manager.dart b/lib/services/analysis_queue_manager.dart @@ -25,32 +25,42 @@ class AnalysisQueueManager { _isProcessing = true; try { - // 1. Fetch pending images from DB - List<ImageModel> pendingImages = await _imageRepo.getPendingImages(); - if (pendingImages.isNotEmpty) { - debugPrint("[Queue]: Found ${pendingImages.length} pending images"); - for (final image in pendingImages) { - await _processSingleImage(image); + // Loop until no items are left + while (true) { + bool processedAny = false; + + // 1. Fetch pending images from DB + List<ImageModel> pendingImages = await _imageRepo.getPendingImages(); + if (pendingImages.isNotEmpty) { + processedAny = true; + debugPrint("[Queue]: Found ${pendingImages.length} pending images"); + for (final image in pendingImages) { + await _processSingleImage(image); + } } - } - // 2. Fetch pending notes from DB - List<NoteModel> pendingNotes = await _noteRepo.getPendingNotes(); - if (pendingNotes.isNotEmpty) { - debugPrint("[Queue]: Found ${pendingNotes.length} pending notes"); + // 2. Fetch pending notes from DB + List<NoteModel> pendingNotes = await _noteRepo.getPendingNotes(); + if (pendingNotes.isNotEmpty) { + processedAny = true; + debugPrint("[Queue]: Found ${pendingNotes.length} pending notes"); + + // Group notes to avoid decoding parent image multiple times + final Map<String, List<NoteModel>> notesByImage = {}; + for (var note in pendingNotes) { + if (!notesByImage.containsKey(note.imageId)) { + notesByImage[note.imageId] = []; + } + notesByImage[note.imageId]!.add(note); + } - // Group notes to avoid decoding parent image multiple times - final Map<String, List<NoteModel>> notesByImage = {}; - for (var note in pendingNotes) { - if (!notesByImage.containsKey(note.imageId)) { - notesByImage[note.imageId] = []; + for (final entry in notesByImage.entries) { + await _processNoteGroup(entry.key, entry.value); } - notesByImage[note.imageId]!.add(note); } - for (final entry in notesByImage.entries) { - await _processNoteGroup(entry.key, entry.value); - } + // If no items were processed in this iteration, the queue is drained + if (!processedAny) break; } } catch (e) { debugPrint("[Queue]: Critical Error: $e"); diff --git a/lib/services/analyze/image_analyzer.dart b/lib/services/analyze/image_analyzer.dart @@ -362,7 +362,10 @@ class ImageAnalyzerService { 'Style': {"scores": results[3]['scores']}, 'Texture': {"scores": results[2]['scores']}, 'Lighting': {"scores": results[5]['scores']}, - 'Colour Palette': {"scores": results[1]['scores']}, + 'Colour Palette': { + "scores": results[1]['scores'], + "palette": results[1]['palette'] + }, 'Emotions': {"scores": results[4]['scores']}, 'Era': {"scores": results[6]['scores']}, 'Layout': {"scores": results[0]['scores']}, diff --git a/lib/services/stylesheet_service.dart b/lib/services/stylesheet_service.dart @@ -189,7 +189,6 @@ class StylesheetService { } Color _parseColor(String input) { - // 1. Try parsing Hex (e.g. "#FF0000" or "FF0000") if (input.startsWith('#') || input.length == 6) { try { String hex = input.replaceAll('#', ''); @@ -198,21 +197,6 @@ class StylesheetService { } } catch (_) {} } - - // 2. Fallback to Semantic Labels - String label = input.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); - if (label.contains('blue')) return Colors.blue; - if (label.contains('red')) return Colors.red; - return Colors.grey.shade400; // Default fallback } } diff --git a/lib/ui/pages/stylesheet_page.dart b/lib/ui/pages/stylesheet_page.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; 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'; @@ -203,7 +204,11 @@ class _StylesheetPageState extends State<StylesheetPage> { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text("No stylesheet data.", style: Variables.headerStyle.copyWith(fontSize: 18)), + Text( + "Are you ready to start building\nthe visual identity", + style: Variables.headerStyle.copyWith(fontSize: 18), + textAlign: TextAlign.center, + ), const SizedBox(height: 24), _buildGenerateButton("Generate Stylesheet"), ], @@ -281,7 +286,7 @@ class _StylesheetPageState extends State<StylesheetPage> { ), const SizedBox(height: 32), ], - if (style != null) _buildSliderSection("Style & Aesthetic", style), // Matches Composition now + 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), @@ -315,7 +320,19 @@ class _StylesheetPageState extends State<StylesheetPage> { width: 200, height: 44, decoration: BoxDecoration(color: Variables.textPrimary, borderRadius: BorderRadius.circular(112)), alignment: Alignment.center, - child: Text(label, style: Variables.buttonTextStyle), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(label, style: Variables.buttonTextStyle), + const SizedBox(width: 8), + SvgPicture.asset( + 'assets/icons/generate_icon.svg', + width: 20, + height: 20, + colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), + ), + ], + ), ), ); } @@ -372,6 +389,34 @@ class _StylesheetPageState extends State<StylesheetPage> { ); } + 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, 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.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2))], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + 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)), + ], + ), + ); + } + Widget _buildTypographySection(dynamic data) { List<String> fontNames = []; if (data is List) { @@ -405,34 +450,6 @@ class _StylesheetPageState extends State<StylesheetPage> { ); } - 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, 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.withValues(alpha: 0.03), blurRadius: 8, offset: const Offset(0, 2))], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - 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)), - ], - ), - ); - } - Widget _buildColorSection(dynamic data) { List<Map<String, dynamic>> palette = []; if (data is List) { @@ -452,7 +469,8 @@ class _StylesheetPageState extends State<StylesheetPage> { Widget _buildColorCard(String label) { Color color = _getColorFromLabel(label); - String hexCode = "#${color.value.toRadixString(16).substring(2).toUpperCase()}"; + String hexCode = label.toUpperCase(); + return Container( decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all(color: Variables.borderSubtle)), clipBehavior: Clip.antiAlias, @@ -470,7 +488,7 @@ class _StylesheetPageState extends State<StylesheetPage> { 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), + const Text("HEX", style: TextStyle(fontFamily: 'GeneralSans', fontSize: 10, color: Variables.textSecondary, overflow: TextOverflow.ellipsis), maxLines: 1), ], ), ), @@ -521,16 +539,14 @@ class _StylesheetPageState extends State<StylesheetPage> { } 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; + if (label.startsWith('#') || label.length == 6) { + try { + String hex = label.replaceAll('#', ''); + if (hex.length == 6) { + return Color(int.parse('0xFF$hex')); + } + } catch (_) {} + } + return Colors.grey.shade400; // Fallback } -} -\ No newline at end of file +}