commit ba31c4b30ed8ad2b837a1e7a8d8ae60c82949c8a
parent 6ce8861827f4b98a4a65274fecc1421a1415adb9
Author: maydayv7 <maydayv7@gmail.com>
Date: Tue, 2 Dec 2025 01:53:05 +0530
Update color analysis model
Diffstat:
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
+}