commit b60aa5f4900976cc2152df00bdfc8c44db3e544f
parent d175b99e260d085e66cab1a492ad5e43aaa9c8eb
Author: maydayv7 <maydayv7@gmail.com>
Date: Mon, 1 Dec 2025 16:30:37 +0530
Merge remote-tracking branch 'origin/sanjeebani'
Diffstat:
9 files changed, 1350 insertions(+), 370 deletions(-)
diff --git a/lib/data/models/project_model.dart b/lib/data/models/project_model.dart
@@ -61,4 +61,6 @@ class ProjectModel {
: [],
);
}
+
+
}
diff --git a/lib/services/flask_service.dart b/lib/services/flask_service.dart
@@ -13,12 +13,8 @@ class FlaskService {
// CONFIGURATION
// ===========================================================================
- // NOTE: REPLACE WITH YOUR WIFI IP ADDRESS
- // BOTH PC AND MOBILE SHOULD BE ON SAME WIFI
- // NO NEED FOR NGORK OR SMEE
- // PORT: 5000, http
- static const String _serverUrl =
- 'http://172.16.114.193:5000'; // --> READ NOTE (REPLACE WITH IITG_CONNECT WIFI IP)
+ // NOTE: REPLACE WITH IP ADDRESS OF LOCAL SERVER
+ static const String _serverUrl = 'http://10.150.40.117:5000';
static const Map<String, String> _headers = {
'Content-Type': 'application/json',
};
diff --git a/lib/services/project_service.dart b/lib/services/project_service.dart
@@ -76,4 +76,20 @@ class ProjectService {
await _projectRepo.deleteProject(projectId);
}
+
+ Future<List<ProjectModel>> getRecentProjectsAndEvents() async {
+ return await _projectRepo.getRecentProjectsAndEvents();
+ }
+
+ Future<List<ProjectModel>> getAllProjects() async {
+ return await _projectRepo.getAllProjects();
+ }
+
+ Future<List<ProjectModel>> getEvents(int projectId) async {
+ return await _projectRepo.getEvents(projectId);
+ }
+
+ Future<ProjectModel?> getProjectById(int id) async {
+ return await _projectRepo.getProjectById(id);
+ }
}
diff --git a/lib/services/stylesheet_service.dart b/lib/services/stylesheet_service.dart
@@ -0,0 +1,218 @@
+import 'dart:convert';
+import 'package:flutter/material.dart';
+import 'package:google_fonts/google_fonts.dart';
+
+/// A simple model to hold the extracted design tokens
+class StylesheetData {
+ final List<Color> colors;
+ final List<String> fonts;
+
+ StylesheetData({required this.colors, required this.fonts});
+}
+
+class StylesheetService {
+ /// Main entry point: Parses a raw (potentially dirty) JSON string
+ /// and returns a structured [StylesheetData] object.
+ StylesheetData parse(String? rawJson) {
+ if (rawJson == null || rawJson.isEmpty) {
+ return StylesheetData(colors: [], fonts: []);
+ }
+
+ // 1. Parse the string into a Map
+ Map<String, dynamic> data = _parseRawJson(rawJson);
+
+ // 2. Extract and Process Fonts
+ List<String> fonts = _extractFonts(data);
+
+ // 3. Extract and Process Colors
+ List<Color> colors = _extractColors(data);
+
+ return StylesheetData(colors: colors, fonts: fonts);
+ }
+
+ // ---------------------------------------------------------------------------
+ // PARSING LOGIC
+ // ---------------------------------------------------------------------------
+
+ /// Safely parses the raw string into a Map, handling dirty AI output.
+ Map<String, dynamic> _parseRawJson(String rawString) {
+ try {
+ // Try standard decode first
+ return _normalizeResult(jsonDecode(rawString));
+ } catch (e) {
+ try {
+ // Try cleaning regex then decoding
+ final cleaned = _cleanJsonString(rawString);
+ return _normalizeResult(jsonDecode(cleaned));
+ } catch (_) {
+ return {};
+ }
+ }
+ }
+
+ /// Cleans "dirty" JSON strings by fixing quotes and unquoted keys.
+ String _cleanJsonString(String raw) {
+ String cleaned = raw;
+ // Add quotes to keys
+ cleaned = cleaned.replaceAllMapped(
+ RegExp(r'([{,]\s*)([a-zA-Z0-9_\s/]+)(\s*:)'),
+ (match) => '${match[1]}"${match[2]?.trim()}"${match[3]}',
+ );
+ // Add quotes to string values that aren't booleans or numbers
+ 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;
+ }
+
+ /// Normalizes the structure if the API returns { "results": ... }
+ Map<String, dynamic> _normalizeResult(dynamic parsed) {
+ if (parsed is String) {
+ try {
+ parsed = jsonDecode(parsed);
+ } catch (_) {}
+ }
+
+ if (parsed is Map<String, dynamic>) {
+ if (parsed.containsKey('results') && parsed['results'] is Map) {
+ return parsed['results'];
+ }
+ return parsed;
+ }
+ return {};
+ }
+
+ // ---------------------------------------------------------------------------
+ // EXTRACTION LOGIC
+ // ---------------------------------------------------------------------------
+
+ /// Extracts font names and resolves them to valid Google Font strings.
+ List<String> _extractFonts(Map<String, dynamic> data) {
+ dynamic fontData = _findValue(data, ['Typography', 'fonts', 'typography']);
+ if (fontData == null) return [];
+
+ List<String> rawNames = [];
+ if (fontData is List) {
+ for (var item in fontData) {
+ if (item is Map && item.containsKey('label')) {
+ rawNames.add(item['label'].toString().trim());
+ } else if (item is String) {
+ rawNames.add(item.trim());
+ }
+ }
+ } else if (fontData is Map && fontData.containsKey('label')) {
+ rawNames.add(fontData['label'].toString().trim());
+ } else if (fontData is String) {
+ rawNames.add(fontData.trim());
+ }
+
+ return rawNames.map((name) => _resolveGoogleFontName(name)).toList();
+ }
+
+ /// Extracts colors from Hex codes or semantic labels.
+ List<Color> _extractColors(Map<String, dynamic> data) {
+ dynamic colorData = _findValue(data, [
+ 'Colour Palette',
+ 'Color Palette',
+ 'colors',
+ ]);
+ if (colorData == null) return [];
+
+ List<Color> resolvedColors = [];
+
+ if (colorData is List) {
+ for (var item in colorData) {
+ String? label;
+ if (item is Map) {
+ label = item['label']?.toString();
+ } else if (item is String) {
+ label = item;
+ }
+
+ if (label != null) {
+ resolvedColors.add(_parseColor(label));
+ }
+ }
+ }
+ return resolvedColors;
+ }
+
+ // ---------------------------------------------------------------------------
+ // HELPERS
+ // ---------------------------------------------------------------------------
+
+ dynamic _findValue(Map<String, dynamic> map, List<String> keys) {
+ for (var k in keys) {
+ if (map.containsKey(k)) return map[k];
+ // Case-insensitive check
+ for (var mapKey in map.keys) {
+ if (mapKey.toLowerCase() == k.toLowerCase()) return map[mapKey];
+ }
+ }
+ return null;
+ }
+
+ String _resolveGoogleFontName(String dirtyName) {
+ // 1. Exact match check (fastest)
+ try {
+ GoogleFonts.getFont(dirtyName);
+ return dirtyName;
+ } catch (_) {}
+
+ // 2. Fuzzy match
+ 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) {
+ return officialName;
+ }
+ }
+ return dirtyName; // Fallback
+ }
+
+ Color _parseColor(String input) {
+ // 1. Try parsing Hex (e.g. "#FF0000" or "FF0000")
+ if (input.startsWith('#') || input.length == 6) {
+ try {
+ String hex = input.replaceAll('#', '');
+ if (hex.length == 6) {
+ return Color(int.parse('0xFF$hex'));
+ }
+ } 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/canvas_board_page.dart b/lib/ui/pages/canvas_board_page.dart
@@ -7,6 +7,8 @@ import 'package:image_picker/image_picker.dart';
import 'package:undo/undo.dart';
import './canvas_toolbar/magic_draw_overlay.dart';
import './canvas_toolbar/text_tools_overlay.dart';
+import '../../data/repos/project_repo.dart';
+import '../../services/stylesheet_service.dart';
// --- MODELS ---
class DrawingPoint {
@@ -65,6 +67,7 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> {
String? selectedId;
late Size _canvasSize;
+ List<Color> _brandColors = [];
// --- TOOLS ---
bool _isMagicDrawActive = false;
@@ -92,6 +95,8 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> {
super.initState();
_canvasSize = Size(widget.width, widget.height);
+ _fetchBrandColors();
+
if (widget.initialImage != null) {
elements.add({
'id': 'bg_${DateTime.now().millisecondsSinceEpoch}',
@@ -112,6 +117,25 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> {
super.dispose();
}
+ Future<void> _fetchBrandColors() async {
+ try {
+ final int? pId = int.tryParse(widget.projectId);
+ if (pId == null) return;
+
+ final project = await ProjectRepo().getProjectById(pId);
+ if (project != null && project.globalStylesheet != null) {
+ final styleData = StylesheetService().parse(project.globalStylesheet);
+ if (mounted && styleData.colors.isNotEmpty) {
+ setState(() {
+ _brandColors = styleData.colors;
+ });
+ }
+ }
+ } catch (e) {
+ debugPrint("Error loading brand colors: $e");
+ }
+ }
+
// --- UNDO/REDO ---
// Robust undo function: call this AFTER a change is made, passing the OLD state
@@ -354,7 +378,7 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> {
color: Colors.white,
boxShadow: [
BoxShadow(
- color: Colors.black.withValues(alpha: 0.15),
+ color: Colors.black.withOpacity(0.15),
blurRadius: 40,
offset: const Offset(0, 10),
),
@@ -423,7 +447,6 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> {
textController:
isSelected ? _textEditingController : null,
focusNode: isSelected ? _textFocusNode : null,
- // Fix: pass the controller to listen for zooms
transformationController: _transformationController,
);
}),
@@ -435,7 +458,6 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> {
key: _drawingKey,
child: GestureDetector(
onPanStart: (details) {
- // Save state before drawing stroke
_gestureStartSnapshot = _getCurrentState();
},
onPanUpdate: _onPanUpdate,
@@ -473,6 +495,8 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> {
selectedColor: _selectedColor,
strokeWidth: _strokeWidth,
isEraser: _isEraser,
+ brandColors: _brandColors,
+ onClose: _saveAndCloseMagicDraw,
onColorChanged: (c) => setState(() => _selectedColor = c),
onWidthChanged: (w) => setState(() => _strokeWidth = w),
onEraserToggle: (e) => setState(() => _isEraser = e),
@@ -736,7 +760,7 @@ class _ManipulatingBox extends StatefulWidget {
required this.isSelected,
required this.isEditing,
required this.viewScale,
- required this.transformationController, // Receive controller
+ required this.transformationController,
required this.onTap,
required this.onDoubleTap,
required this.onDragStart,
@@ -779,12 +803,10 @@ class _ManipulatingBoxState extends State<_ManipulatingBox> {
@override
Widget build(BuildContext context) {
- // Listen to the transformation controller to get real-time zoom updates
return ValueListenableBuilder(
valueListenable: widget.transformationController,
builder: (context, matrix, child) {
final double currentZoom = matrix.getMaxScaleOnAxis();
- // Calculate inverse scale to keep handles visually constant
final double handleScale = (1.0 / currentZoom).clamp(0.1, 5.0);
final double touchTargetSize = 40.0 * handleScale;
final double visualSize = 24.0 * handleScale;
@@ -804,8 +826,8 @@ class _ManipulatingBoxState extends State<_ManipulatingBox> {
onPanStart: (_) => widget.onDragStart(),
onPanUpdate: (details) {
if (widget.isSelected && !widget.isEditing) {
- // Use real-time zoom to normalize drag delta
- final delta = details.delta / currentZoom;
+ // FIX: Do not divide by currentZoom
+ final delta = details.delta;
final globalDelta = _rotateVector(delta, _rot);
setState(() => _pos += globalDelta);
widget.onUpdate(_pos, _size, _rot);
@@ -824,7 +846,7 @@ class _ManipulatingBoxState extends State<_ManipulatingBox> {
)
: widget.type == 'text'
? Border.all(
- color: Colors.grey.withValues(alpha: 0.3),
+ color: Colors.grey.withOpacity(0.3),
width: 1.0 * handleScale,
)
: null,
@@ -849,21 +871,15 @@ class _ManipulatingBoxState extends State<_ManipulatingBox> {
icon: Icons.zoom_out_map,
color: Colors.blue,
onDrag: (delta) {
- final normalizedDelta = delta / currentZoom;
- final localDelta = _rotateVector(
- normalizedDelta,
- -_rot,
- );
+ // FIX: Use delta directly, no un-rotation
setState(() {
_size = Size(
- (_size.width + localDelta.dx).clamp(50.0, 10000.0),
- (_size.height + localDelta.dy).clamp(30.0, 10000.0),
- );
- final offset = Offset(
- localDelta.dx / 2,
- localDelta.dy / 2,
+ (_size.width + delta.dx).clamp(50.0, 10000.0),
+ (_size.height + delta.dy).clamp(30.0, 10000.0),
);
- _pos += _rotateVector(offset, _rot);
+ // Fix anchor point calculation
+ final offset = Offset(delta.dx / 2, delta.dy / 2);
+ _pos += _rotateVector(offset, _rot) - offset;
});
widget.onUpdate(_pos, _size, _rot);
},
@@ -1073,7 +1089,7 @@ class CanvasBottomBar extends StatelessWidget {
border: Border(top: BorderSide(color: Colors.grey[200]!)),
boxShadow: [
BoxShadow(
- color: Colors.black.withValues(alpha: 0.05),
+ color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, -5),
),
diff --git a/lib/ui/pages/canvas_toolbar/magic_draw_overlay.dart b/lib/ui/pages/canvas_toolbar/magic_draw_overlay.dart
@@ -9,6 +9,8 @@ class MagicDrawTools extends StatefulWidget {
final Function(Color) onColorChanged;
final Function(double) onWidthChanged;
final Function(bool) onEraserToggle;
+ final VoidCallback onClose; // Added onClose callback signature to match usage
+ final List<Color> brandColors;
const MagicDrawTools({
super.key,
@@ -19,6 +21,8 @@ class MagicDrawTools extends StatefulWidget {
required this.onColorChanged,
required this.onWidthChanged,
required this.onEraserToggle,
+ required this.onClose,
+ required this.brandColors,
});
@override
@@ -28,12 +32,21 @@ class MagicDrawTools extends StatefulWidget {
class _MagicDrawToolsState extends State<MagicDrawTools> {
bool _showStrokeSlider = false;
+ // Initialize recent colors here so they persist
+ final List<Color> _recentColors = [
+ Colors.blue,
+ Colors.purple,
+ const Color(0xFFD81B60),
+ Colors.pinkAccent,
+ Colors.amber,
+ ];
+
@override
Widget build(BuildContext context) {
if (!widget.isActive) return const SizedBox.shrink();
- // Only displaying the bottom tool panel now.
- // The top header is handled by the main Scaffold AppBar.
+ // KEEPING YOUR LOGIC: Only displaying the bottom tool panel.
+ // The top header/close button from the other branch is ignored.
return Positioned(
bottom: 140,
left: 16,
@@ -56,7 +69,7 @@ class _MagicDrawToolsState extends State<MagicDrawTools> {
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
- color: Colors.black.withValues(alpha: 0.1),
+ color: Colors.black.withOpacity(0.1),
blurRadius: 15,
offset: const Offset(0, 5),
),
@@ -134,7 +147,7 @@ class _MagicDrawToolsState extends State<MagicDrawTools> {
border: Border.all(color: Colors.white, width: 2),
boxShadow: [
BoxShadow(
- color: Colors.black.withValues(alpha: 0.1),
+ color: Colors.black.withOpacity(0.1),
blurRadius: 4,
),
],
@@ -246,7 +259,9 @@ class _MagicDrawToolsState extends State<MagicDrawTools> {
builder: (context) {
return _AdvancedColorPickerSheet(
initialColor: widget.selectedColor,
+ recentColors: _recentColors,
onColorChanged: widget.onColorChanged,
+ brandColors: widget.brandColors,
);
},
);
@@ -276,10 +291,14 @@ class _TaperedSliderPainter extends CustomPainter {
class _AdvancedColorPickerSheet extends StatefulWidget {
final Color initialColor;
final ValueChanged<Color> onColorChanged;
+ final List<Color> recentColors;
+ final List<Color> brandColors;
const _AdvancedColorPickerSheet({
required this.initialColor,
required this.onColorChanged,
+ required this.recentColors,
+ required this.brandColors,
});
@override
@@ -291,38 +310,50 @@ class _AdvancedColorPickerSheetState extends State<_AdvancedColorPickerSheet>
with SingleTickerProviderStateMixin {
late TabController _tabController;
late Color _currentColor;
+ late List<Color> _brandPalette;
- final List<Color> _brandPalette = [
- Colors.blue,
- const Color(0xFFCCFF00),
- Colors.purpleAccent,
- const Color(0xFFF0F0F0),
- Colors.black,
- ];
-
- final List<Color> _recentColors = [
- Colors.blue,
- Colors.purple,
- const Color(0xFFD81B60),
- Colors.pinkAccent,
- Colors.amber,
- ];
+ // Controllers for RGB Sliders (Integrated from other branch)
+ final TextEditingController _rController = TextEditingController();
+ final TextEditingController _gController = TextEditingController();
+ final TextEditingController _bController = TextEditingController();
@override
void initState() {
super.initState();
_currentColor = widget.initialColor;
_tabController = TabController(length: 3, vsync: this);
+ _brandPalette = widget.brandColors; // Integrate real brand colors
+ _updateControllers();
+ }
+
+ void _updateControllers() {
+ _rController.text = _currentColor.red.toString();
+ _gController.text = _currentColor.green.toString();
+ _bController.text = _currentColor.blue.toString();
}
@override
void dispose() {
_tabController.dispose();
+ _rController.dispose();
+ _gController.dispose();
+ _bController.dispose();
super.dispose();
}
void _updateColor(Color color) {
- setState(() => _currentColor = color);
+ setState(() {
+ _currentColor = color;
+
+ // Update recent colors list
+ widget.recentColors.removeWhere((c) => c.value == color.value);
+ widget.recentColors.insert(0, color);
+ if (widget.recentColors.length > 5) {
+ widget.recentColors.removeLast();
+ }
+
+ _updateControllers();
+ });
widget.onColorChanged(color);
}
@@ -332,13 +363,15 @@ class _AdvancedColorPickerSheetState extends State<_AdvancedColorPickerSheet>
height: MediaQuery.of(context).size.height * 0.85,
decoration: const BoxDecoration(
color: Colors.white,
- borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
+ borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
- child: SafeArea(
- child: Column(
- children: [
- const SizedBox(height: 8),
- Container(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ // --- HEADER HANDLE ---
+ const SizedBox(height: 12),
+ Center(
+ child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
@@ -346,341 +379,431 @@ class _AdvancedColorPickerSheetState extends State<_AdvancedColorPickerSheet>
borderRadius: BorderRadius.circular(2),
),
),
- const SizedBox(height: 16),
- TabBar(
- controller: _tabController,
- labelColor: Colors.black,
- unselectedLabelColor: Colors.grey,
- indicatorColor: Colors.black,
- labelStyle: const TextStyle(fontWeight: FontWeight.w600),
- tabs: const [
- Tab(text: 'Grid'),
- Tab(text: 'Spectrum'),
- Tab(text: 'Sliders'),
+ ),
+ const SizedBox(height: 16),
+
+ // --- TABS ---
+ TabBar(
+ controller: _tabController,
+ labelColor: Colors.black,
+ unselectedLabelColor: Colors.grey,
+ indicatorColor: Colors.black,
+ indicatorSize: TabBarIndicatorSize.label,
+ labelStyle: const TextStyle(
+ fontWeight: FontWeight.w600,
+ fontSize: 14,
+ ),
+ tabs: const [
+ Tab(text: 'Grid'),
+ Tab(text: 'Spectrum'),
+ Tab(text: 'Sliders'),
+ ],
+ ),
+
+ const SizedBox(height: 16),
+
+ // --- HEX / HEADER ROW ---
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ child: Row(
+ children: [
+ Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 8,
+ vertical: 6,
+ ),
+ decoration: BoxDecoration(
+ border: Border.all(color: Colors.grey.shade300),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: const [
+ Text(
+ "Hex",
+ style: TextStyle(
+ fontSize: 13,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ SizedBox(width: 4),
+ Icon(
+ Icons.keyboard_arrow_down,
+ size: 16,
+ color: Colors.grey,
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(width: 8),
+ Container(
+ width: 32,
+ height: 32,
+ decoration: BoxDecoration(
+ color: _currentColor,
+ borderRadius: BorderRadius.circular(6),
+ border: Border.all(color: Colors.grey.shade200),
+ ),
+ ),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 10,
+ vertical: 8,
+ ),
+ decoration: BoxDecoration(
+ color: Colors.grey.shade50,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Text(
+ "#${_currentColor.value.toRadixString(16).toUpperCase().substring(2)}",
+ style: const TextStyle(
+ fontWeight: FontWeight.w600,
+ fontSize: 14,
+ ),
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ ),
+ const SizedBox(width: 8),
+ const Icon(Icons.colorize, size: 20, color: Colors.black54),
],
),
- const SizedBox(height: 16),
- Expanded(
- child: TabBarView(
- controller: _tabController,
- physics: const NeverScrollableScrollPhysics(),
- children: [
- _buildGridTab(),
- _buildSpectrumTab(),
- _buildSlidersTab(),
- ],
- ),
+ ),
+
+ const SizedBox(height: 16),
+ const Divider(height: 1),
+
+ // --- MAIN CONTENT AREA ---
+ Expanded(
+ child: TabBarView(
+ controller: _tabController,
+ physics: const NeverScrollableScrollPhysics(),
+ children: [
+ _buildGridTab(),
+ _buildSpectrumTab(),
+ _buildSlidersTab(),
+ ],
),
- _buildSharedFooter(),
- ],
- ),
+ ),
+
+ // --- FOOTER ---
+ _buildSharedFooter(),
+ ],
),
);
}
+ // --- HELPER: Generate colors (Integrated from other branch) ---
+ List<Color> _generateColorGrid() {
+ List<Color> colors = [];
+
+ // 1. Top Row: Grayscale (White -> Black)
+ for (int i = 0; i < 9; i++) {
+ double lightness = 1.0 - (i / 8); // 1.0 to 0.0
+ colors.add(HSLColor.fromAHSL(1.0, 0.0, 0.0, lightness).toColor());
+ }
+
+ // 2. Main Grid: Hues (Columns) x Shades (Rows)
+ final int hueSteps = 9; // Columns
+ final int shadeSteps = 7; // Rows excluding grayscale
+
+ for (int shade = 0; shade < shadeSteps; shade++) {
+ for (int hueStep = 0; hueStep < hueSteps; hueStep++) {
+ double hue = (hueStep / hueSteps) * 360;
+ double saturation = 0.5 + (shade / shadeSteps) * 0.5;
+ double lightness = 0.8 - (shade / shadeSteps) * 0.5;
+
+ colors.add(
+ HSLColor.fromAHSL(1.0, hue, saturation, lightness).toColor(),
+ );
+ }
+ }
+ return colors;
+ }
+
+ // --- TAB 1: GRID ---
Widget _buildGridTab() {
- return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 20),
- child: Column(
- children: [
- Expanded(
- child: ClipRRect(
- borderRadius: BorderRadius.circular(12),
- child: GridView.builder(
- physics: const BouncingScrollPhysics(),
- gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
- crossAxisCount: 10,
- crossAxisSpacing: 2,
- mainAxisSpacing: 2,
- ),
- itemCount: 100,
- itemBuilder: (context, index) {
- final double hue = (index % 10) * 36.0;
- final double saturation = ((index ~/ 10) + 1) / 10.0;
- final color =
- HSVColor.fromAHSV(1.0, hue, saturation, 0.9).toColor();
-
- return GestureDetector(
- onTap: () => _updateColor(color),
- child: Container(color: color),
- );
- },
+ final gridColors = _generateColorGrid();
+
+ return Column(
+ children: [
+ Expanded(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
+ child: GridView.builder(
+ physics: const BouncingScrollPhysics(),
+ gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
+ crossAxisCount: 9,
+ crossAxisSpacing: 0,
+ mainAxisSpacing: 0,
+ childAspectRatio: 1.0,
),
+ itemCount: gridColors.length,
+ itemBuilder: (context, index) {
+ final color = gridColors[index];
+ final isSelected = _currentColor.value == color.value;
+
+ return GestureDetector(
+ onTap: () => _updateColor(color),
+ child: Stack(
+ alignment: Alignment.center,
+ children: [
+ Container(
+ decoration: BoxDecoration(
+ color: color,
+ border: Border.all(
+ color: Colors.black.withOpacity(0.05),
+ width: 0.5,
+ ),
+ ),
+ ),
+ if (isSelected)
+ Container(
+ width: 24,
+ height: 24,
+ decoration: BoxDecoration(
+ color: color,
+ shape: BoxShape.circle,
+ border: Border.all(color: Colors.white, width: 3),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withOpacity(0.2),
+ blurRadius: 4,
+ spreadRadius: 1,
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ },
),
),
- const SizedBox(height: 10),
- _buildHueSlider(),
- _buildOpacitySlider(),
- ],
- ),
+ ),
+ ],
);
}
+ // --- TAB 2: SPECTRUM ---
Widget _buildSpectrumTab() {
return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 20),
- child: Column(
- children: [
- Expanded(
- child: ColorPicker(
- pickerColor: _currentColor,
- onColorChanged: _updateColor,
- enableAlpha: false,
- displayThumbColor: true,
- paletteType: PaletteType.hsvWithHue,
- labelTypes: const [],
- pickerAreaHeightPercent: 0.8,
- pickerAreaBorderRadius: BorderRadius.circular(12),
+ padding: const EdgeInsets.all(20),
+ child: LayoutBuilder(
+ builder: (context, constraints) {
+ if (constraints.maxHeight < 150) {
+ return const Center(child: Text("Rotate device"));
+ }
+ return SizedBox(
+ width: constraints.maxWidth,
+ height: constraints.maxHeight,
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(12),
+ child: ColorPicker(
+ pickerColor: _currentColor,
+ onColorChanged: _updateColor,
+ enableAlpha: false,
+ labelTypes: const [],
+ displayThumbColor: true,
+ paletteType: PaletteType.hsvWithHue,
+ pickerAreaHeightPercent: 0.8,
+ hexInputBar: false,
+ ),
),
- ),
- ],
+ );
+ },
),
);
}
+ // --- TAB 3: SLIDERS ---
Widget _buildSlidersTab() {
return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 20),
- child: Column(
- children: [
- const SizedBox(height: 20),
- _buildRGBSlider("R", Colors.red, _currentColor.red, (v) {
- _updateColor(_currentColor.withRed(v.toInt()));
- }),
- _buildRGBSlider("G", Colors.green, _currentColor.green, (v) {
- _updateColor(_currentColor.withGreen(v.toInt()));
- }),
- _buildRGBSlider("B", Colors.blue, _currentColor.blue, (v) {
- _updateColor(_currentColor.withBlue(v.toInt()));
- }),
- ],
+ padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
+ child: SingleChildScrollView(
+ child: Column(
+ children: [
+ _buildSingleRGBSlider("Red", Colors.red, _currentColor.red, (v) {
+ _updateColor(_currentColor.withRed(v.toInt()));
+ }),
+ const SizedBox(height: 24),
+ _buildSingleRGBSlider(
+ "Green",
+ Colors.green,
+ _currentColor.green,
+ (v) {
+ _updateColor(_currentColor.withGreen(v.toInt()));
+ },
+ ),
+ const SizedBox(height: 24),
+ _buildSingleRGBSlider("Blue", Colors.blue, _currentColor.blue, (v) {
+ _updateColor(_currentColor.withBlue(v.toInt()));
+ }),
+ ],
+ ),
),
);
}
- Widget _buildRGBSlider(
+ Widget _buildSingleRGBSlider(
String label,
Color activeColor,
int value,
ValueChanged<double> onChanged,
) {
- return Padding(
- padding: const EdgeInsets.only(bottom: 16.0),
- child: Row(
- children: [
- Expanded(
- child: SizedBox(
- height: 36,
- child: SliderTheme(
- data: SliderThemeData(
- trackHeight: 36,
- activeTrackColor: activeColor,
- inactiveTrackColor: activeColor.withValues(alpha: 0.2),
- thumbColor: Colors.transparent,
- thumbShape: const RoundSliderThumbShape(
- enabledThumbRadius: 0,
- ),
- overlayShape: SliderComponentShape.noOverlay,
- trackShape: const RectangularSliderTrackShape(),
- ),
- child: ClipRRect(
- borderRadius: BorderRadius.circular(18),
- child: Slider(
- value: value.toDouble(),
- min: 0,
- max: 255,
- onChanged: onChanged,
- ),
+ return Row(
+ children: [
+ Expanded(
+ child: SizedBox(
+ height: 30,
+ child: SliderTheme(
+ data: SliderThemeData(
+ trackHeight: 6,
+ activeTrackColor: activeColor,
+ inactiveTrackColor: activeColor.withOpacity(0.15),
+ thumbColor: Colors.white,
+ thumbShape: const RoundSliderThumbShape(
+ enabledThumbRadius: 14,
+ elevation: 4,
),
+ overlayShape: const RoundSliderOverlayShape(overlayRadius: 24),
+ trackShape: const RoundedRectSliderTrackShape(),
+ ),
+ child: Slider(
+ value: value.toDouble(),
+ min: 0,
+ max: 255,
+ onChanged: onChanged,
),
),
),
- const SizedBox(width: 12),
- Container(
- width: 50,
- padding: const EdgeInsets.all(8),
- alignment: Alignment.center,
- decoration: BoxDecoration(
- border: Border.all(color: Colors.grey.shade300),
- borderRadius: BorderRadius.circular(8),
- ),
- child: Text(
- "$value",
- style: const TextStyle(fontWeight: FontWeight.bold),
- ),
- ),
- ],
- ),
- );
- }
-
- Widget _buildHueSlider() {
- return SizedBox(
- height: 15,
- child: SliderTheme(
- data: SliderThemeData(
- trackHeight: 8,
- trackShape: const RectangularSliderTrackShape(),
- thumbShape: const RoundSliderThumbShape(
- enabledThumbRadius: 10,
- elevation: 2,
- ),
- thumbColor: Colors.white,
- overlayShape: SliderComponentShape.noOverlay,
- ),
- child: ColorPicker(
- pickerColor: _currentColor,
- onColorChanged: _updateColor,
- enableAlpha: false,
- displayThumbColor: true,
- paletteType: PaletteType.hsv,
- labelTypes: const [],
- pickerAreaHeightPercent: 0.0,
),
- ),
- );
- }
-
- Widget _buildOpacitySlider() {
- return Padding(
- padding: const EdgeInsets.only(top: 8.0),
- child: SizedBox(
- height: 15,
- child: SliderTheme(
- data: SliderThemeData(
- trackHeight: 8,
- thumbShape: const RoundSliderThumbShape(
- enabledThumbRadius: 10,
- elevation: 2,
- ),
- thumbColor: Colors.white,
- overlayShape: SliderComponentShape.noOverlay,
+ const SizedBox(width: 12),
+ Container(
+ width: 50,
+ height: 36,
+ decoration: BoxDecoration(
+ border: Border.all(color: Colors.grey.shade300),
+ borderRadius: BorderRadius.circular(8),
+ color: Colors.white,
),
- child: Slider(
- value: 1.0,
- onChanged: (v) {},
- activeColor: Colors.blue,
- inactiveColor: Colors.grey[300],
+ alignment: Alignment.center,
+ child: Text(
+ value.toString(),
+ style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
),
),
- ),
+ ],
);
}
+ // --- FOOTER ---
Widget _buildSharedFooter() {
- return Container(
- padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- const Text(
- "Recently used",
- style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold),
- ),
- const SizedBox(height: 8),
- Row(
- children: _recentColors.map((c) => _buildColorCircle(c)).toList(),
- ),
- const SizedBox(height: 16),
-
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- const Text(
- "Brand Palette",
- style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold),
- ),
- Text(
- "Edit",
- style: TextStyle(
- fontSize: 12,
- color: Colors.blue[700],
- fontWeight: FontWeight.bold,
- ),
+ return SafeArea(
+ top: false,
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ border: Border(top: BorderSide(color: Colors.grey.shade100)),
+ ),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Text(
+ "Recently used",
+ style: TextStyle(
+ fontSize: 12,
+ fontWeight: FontWeight.w600,
+ color: Colors.grey,
),
- ],
- ),
- const SizedBox(height: 8),
- Row(
- children: [
- Container(
- width: 32,
- height: 32,
- margin: const EdgeInsets.only(right: 8),
- decoration: BoxDecoration(
- shape: BoxShape.circle,
- border: Border.all(color: Colors.grey.shade300),
- ),
- child: Icon(Icons.block, size: 16, color: Colors.red[300]),
+ ),
+ const SizedBox(height: 10),
+ SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ child: Row(
+ children:
+ widget.recentColors
+ .map((c) => _buildColorCircle(c))
+ .toList(),
),
- ..._brandPalette.map((c) => _buildColorCircle(c)),
- Container(
- width: 32,
- height: 32,
- decoration: BoxDecoration(
- color: Colors.grey[200],
- shape: BoxShape.circle,
+ ),
+ const SizedBox(height: 16),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ const Text(
+ "Brand Palette",
+ style: TextStyle(
+ fontSize: 12,
+ fontWeight: FontWeight.w600,
+ color: Colors.grey,
+ ),
),
- child: const Icon(Icons.add, size: 18, color: Colors.black54),
- ),
- ],
- ),
- const SizedBox(height: 16),
-
- const Text(
- "Gradients",
- style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold),
- ),
- const SizedBox(height: 8),
- Row(
- children: [
- _buildGradientCircle(Colors.black, Colors.white),
- _buildGradientCircle(Colors.grey, Colors.black),
- _buildGradientCircle(Colors.white, Colors.grey),
- _buildGradientCircle(Colors.black, Colors.grey),
- const SizedBox(width: 8),
- Container(
- width: 32,
- height: 32,
- decoration: BoxDecoration(
- color: Colors.grey[100],
- shape: BoxShape.circle,
+ Text(
+ "Edit",
+ style: TextStyle(
+ fontSize: 12,
+ color: Colors.blue[700],
+ fontWeight: FontWeight.bold,
+ ),
),
- child: const Icon(Icons.tune, size: 16, color: Colors.black54),
+ ],
+ ),
+ const SizedBox(height: 10),
+ SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ child: Row(
+ children: [
+ _buildAddButton(),
+ const SizedBox(width: 12),
+ ..._brandPalette.map((c) => _buildColorCircle(c)).toList(),
+ ],
),
- ],
- ),
- ],
+ ),
+ // EXCLUDED: Gradient feature as requested.
+ ],
+ ),
),
);
}
Widget _buildColorCircle(Color color) {
- return Container(
- width: 32,
- height: 32,
- margin: const EdgeInsets.only(right: 12),
- decoration: BoxDecoration(
- color: color,
- shape: BoxShape.circle,
- border: Border.all(color: Colors.grey.withValues(alpha: 0.2)),
+ return GestureDetector(
+ onTap: () => _updateColor(color),
+ child: Container(
+ width: 36,
+ height: 36,
+ margin: const EdgeInsets.only(right: 12),
+ decoration: BoxDecoration(
+ color: color,
+ shape: BoxShape.circle,
+ border: Border.all(color: Colors.grey.shade200, width: 1),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withOpacity(0.05),
+ blurRadius: 4,
+ offset: const Offset(0, 2),
+ ),
+ ],
+ ),
),
);
}
- Widget _buildGradientCircle(Color c1, Color c2) {
+ Widget _buildAddButton() {
return Container(
- width: 32,
- height: 32,
- margin: const EdgeInsets.only(right: 12),
+ width: 36,
+ height: 36,
decoration: BoxDecoration(
+ color: Colors.grey[100],
shape: BoxShape.circle,
- gradient: LinearGradient(
- begin: Alignment.topLeft,
- end: Alignment.bottomRight,
- colors: [c1, c2],
- ),
+ border: Border.all(color: Colors.grey.shade300),
),
+ child: const Icon(Icons.add, size: 20, color: Colors.black54),
);
}
}
diff --git a/lib/ui/pages/create_file_page.dart b/lib/ui/pages/create_file_page.dart
@@ -2,11 +2,20 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'canvas_board_page.dart';
+import 'define_brand_page.dart';
+
+// Services
+import '../../services/project_service.dart';
+import '../../services/image_service.dart';
+
+// Models
+import '../../data/models/project_model.dart';
class CreateFilePage extends StatefulWidget {
final File? file; // Made optional for blank canvas creation
+ final int? projectId; // Optional: If null, user can select a project
- const CreateFilePage({super.key, this.file});
+ const CreateFilePage({super.key, this.file, this.projectId});
@override
State<CreateFilePage> createState() => _CreateFilePageState();
@@ -15,6 +24,10 @@ class CreateFilePage extends StatefulWidget {
class _CreateFilePageState extends State<CreateFilePage> {
final TextEditingController _searchController = TextEditingController();
+ // Project Selection State
+ int? _selectedProjectId;
+ String _selectedProjectTitle = "Select Project";
+
// Master list of presets
final List<CanvasPreset> _allPresets = [
CanvasPreset(
@@ -82,6 +95,12 @@ class _CreateFilePageState extends State<CreateFilePage> {
void initState() {
super.initState();
_filteredPresets = _allPresets;
+
+ // Initialize from passed project ID
+ _selectedProjectId = widget.projectId;
+ if (_selectedProjectId != null) {
+ _selectedProjectTitle = "Current Project";
+ }
}
@override
@@ -109,6 +128,39 @@ class _CreateFilePageState extends State<CreateFilePage> {
});
}
+ void _openProjectSelection() {
+ showModalBottomSheet(
+ context: context,
+ isScrollControlled: true,
+ backgroundColor: Colors.transparent,
+ builder:
+ (context) => DraggableScrollableSheet(
+ initialChildSize: 0.85,
+ minChildSize: 0.5,
+ maxChildSize: 0.95,
+ builder:
+ (_, controller) => Container(
+ decoration: const BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.vertical(
+ top: Radius.circular(20),
+ ),
+ ),
+ child: ProjectSelectionModal(
+ scrollController: controller,
+ onProjectSelected: (id, title) {
+ setState(() {
+ _selectedProjectId = id;
+ _selectedProjectTitle = title;
+ });
+ Navigator.pop(context);
+ },
+ ),
+ ),
+ ),
+ );
+ }
+
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -129,10 +181,41 @@ class _CreateFilePageState extends State<CreateFilePage> {
),
),
actions: [
- IconButton(
- icon: const Icon(Icons.add, color: Colors.black),
- onPressed: () {},
- ),
+ // ONLY show selection button if projectId was NOT passed in
+ if (widget.projectId == null)
+ Padding(
+ padding: const EdgeInsets.only(right: 16.0),
+ child: TextButton.icon(
+ onPressed: _openProjectSelection,
+ style: TextButton.styleFrom(
+ backgroundColor: Colors.white,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(20),
+ ),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 12,
+ vertical: 8,
+ ),
+ ),
+ icon: Icon(
+ _selectedProjectId == null
+ ? Icons.create_new_folder_outlined
+ : Icons.folder_open,
+ size: 18,
+ color: Colors.black,
+ ),
+ label: Text(
+ _selectedProjectTitle,
+ style: const TextStyle(
+ color: Colors.black,
+ fontSize: 12,
+ fontWeight: FontWeight.w500,
+ ),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ ),
],
),
body: Column(
@@ -150,7 +233,7 @@ class _CreateFilePageState extends State<CreateFilePage> {
controller: _searchController,
onChanged: _runFilter,
decoration: InputDecoration(
- hintText: 'Search',
+ hintText: 'Search sizes',
hintStyle: TextStyle(color: Colors.grey[400], fontSize: 14),
prefixIcon: Icon(
Icons.search,
@@ -260,7 +343,7 @@ class _CreateFilePageState extends State<CreateFilePage> {
return InkWell(
onTap: () {
if (isCustom) {
- _navigateToEditor(1000, 1000); // Default custom size or show dialog
+ _navigateToEditor(1000, 1000);
} else {
_navigateToEditor(preset.width, preset.height);
}
@@ -276,20 +359,17 @@ class _CreateFilePageState extends State<CreateFilePage> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- // Top Section (Blue Box + White Box + Icon)
Expanded(
child: Container(
width: double.infinity,
decoration: BoxDecoration(
- color: const Color(0xFFE0E7FF), // Blue BG
+ color: const Color(0xFFE0E7FF),
borderRadius: BorderRadius.circular(8),
),
child: Center(
child:
isCustom
- // Custom uses hardcoded Icon
? const Icon(Icons.add, size: 30, color: Colors.blue)
- // Others use AspectRatio box + SVG
: Padding(
padding: const EdgeInsets.all(12.0),
child: AspectRatio(
@@ -329,10 +409,7 @@ class _CreateFilePageState extends State<CreateFilePage> {
),
),
),
-
const SizedBox(height: 8),
-
- // Bottom Text
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -360,17 +437,24 @@ class _CreateFilePageState extends State<CreateFilePage> {
}
void _navigateToEditor(int width, int height) {
+ // FORCE Selection: If no project is selected, open the modal and return.
+ if (_selectedProjectId == null) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text("Please select a destination project")),
+ );
+ _openProjectSelection();
+ return;
+ }
+
Navigator.push(
context,
MaterialPageRoute(
builder:
(context) => CanvasBoardPage(
- // Generate a random ID for the project
- projectId: 'proj_${DateTime.now().millisecondsSinceEpoch}',
- // Pass the dimensions from the preset
+ // Use the selected project ID
+ projectId: _selectedProjectId.toString(),
width: width.toDouble(),
height: height.toDouble(),
- // Pass the file if it exists (e.g. from "Image to Canvas" flow)
initialImage: widget.file,
),
),
@@ -393,3 +477,524 @@ class CanvasPreset {
this.svgPath,
});
}
+
+// --------------------------------------------------------------------------
+// --- PROJECT SELECTION MODAL ---
+// --------------------------------------------------------------------------
+
+class ProjectItemViewModel {
+ final ProjectModel item;
+ final String? parentTitle;
+ final String? coverPath;
+
+ ProjectItemViewModel({required this.item, this.parentTitle, this.coverPath});
+
+ String get title => item.title;
+ bool get isEvent => item.isEvent;
+ int get id => item.id!;
+}
+
+class ProjectGroup {
+ final ProjectModel project;
+ final List<ProjectItemViewModel> events;
+ final String? coverPath;
+ bool isExpanded;
+
+ ProjectGroup({
+ required this.project,
+ this.events = const [],
+ this.coverPath,
+ this.isExpanded = false,
+ });
+}
+
+class ProjectSelectionModal extends StatefulWidget {
+ final Function(int id, String title) onProjectSelected;
+ final ScrollController scrollController;
+
+ const ProjectSelectionModal({
+ super.key,
+ required this.onProjectSelected,
+ required this.scrollController,
+ });
+
+ @override
+ State<ProjectSelectionModal> createState() => _ProjectSelectionModalState();
+}
+
+class _ProjectSelectionModalState extends State<ProjectSelectionModal> {
+ final ProjectService _projectService = ProjectService();
+ final ImageService _imageService = ImageService();
+
+ List<ProjectItemViewModel> _recentViewModels = [];
+ List<ProjectGroup> _groupedProjects = [];
+ List<ProjectGroup> _filteredGroupedProjects = [];
+
+ bool _isLoading = true;
+ String _searchQuery = "";
+ final TextEditingController _searchController = TextEditingController();
+
+ @override
+ void initState() {
+ super.initState();
+ _loadData();
+ }
+
+ @override
+ void dispose() {
+ _searchController.dispose();
+ super.dispose();
+ }
+
+ Future<String?> _getProjectCover(int projectId) async {
+ try {
+ final images = await _imageService.getImages(projectId);
+ if (images.isNotEmpty) {
+ return images.first.filePath;
+ }
+ } catch (e) {
+ debugPrint("Error fetching cover for project $projectId: $e");
+ }
+ return null;
+ }
+
+ Future<void> _loadData() async {
+ setState(() => _isLoading = true);
+
+ final recentItems = await _projectService.getRecentProjectsAndEvents();
+ final allProjects = await _projectService.getAllProjects();
+
+ // Build Recent View Models
+ final List<ProjectItemViewModel> recents = [];
+ for (var item in recentItems.take(3)) {
+ String? parentTitle;
+ if (item.parentId != null) {
+ final parent = await _projectService.getProjectById(item.parentId!);
+ parentTitle = parent?.title;
+ }
+ final cover = await _getProjectCover(item.id!);
+ recents.add(
+ ProjectItemViewModel(
+ item: item,
+ parentTitle: parentTitle,
+ coverPath: cover,
+ ),
+ );
+ }
+ _recentViewModels = recents;
+
+ // Build Grouped Projects
+ final List<ProjectGroup> groups = [];
+ for (final p in allProjects) {
+ final rawEvents = await _projectService.getEvents(p.id!);
+ final List<ProjectItemViewModel> eventVMs = [];
+ for (final e in rawEvents) {
+ final eCover = await _getProjectCover(e.id!);
+ eventVMs.add(ProjectItemViewModel(item: e, coverPath: eCover));
+ }
+ final pCover = await _getProjectCover(p.id!);
+ groups.add(ProjectGroup(project: p, events: eventVMs, coverPath: pCover));
+ }
+
+ _groupedProjects = groups;
+ _filteredGroupedProjects = groups;
+
+ if (mounted) setState(() => _isLoading = false);
+ }
+
+ void _filterProjects(String query) {
+ setState(() {
+ _searchQuery = query;
+ if (query.isEmpty) {
+ _filteredGroupedProjects = _groupedProjects;
+ } else {
+ final q = query.toLowerCase();
+ final List<ProjectGroup> filtered = [];
+ for (final g in _groupedProjects) {
+ final projectMatch = g.project.title.toLowerCase().contains(q);
+ final matchingEvents =
+ g.events.where((e) => e.title.toLowerCase().contains(q)).toList();
+
+ if (projectMatch) {
+ filtered.add(
+ ProjectGroup(
+ project: g.project,
+ events: g.events,
+ isExpanded: true,
+ coverPath: g.coverPath,
+ ),
+ );
+ } else if (matchingEvents.isNotEmpty) {
+ filtered.add(
+ ProjectGroup(
+ project: g.project,
+ events: matchingEvents,
+ isExpanded: true,
+ coverPath: g.coverPath,
+ ),
+ );
+ }
+ }
+ _filteredGroupedProjects = filtered;
+ }
+ });
+ }
+
+ Future<void> _createNewProject() async {
+ final result = await Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder:
+ (context) => const DefineBrandPage(
+ projectName:
+ "", // Can pass empty string if you want user to type it
+ ),
+ ),
+ );
+
+ // Check if a project was created and returned
+ if (result != null && result is Map) {
+ final newId = result['id'];
+ final title = result['title'];
+ if (newId != null && title != null) {
+ widget.onProjectSelected(newId, title);
+ }
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ children: [
+ // Header handle
+ Center(
+ child: Container(
+ margin: const EdgeInsets.only(top: 12, bottom: 8),
+ width: 40,
+ height: 4,
+ decoration: BoxDecoration(
+ color: Colors.grey[300],
+ borderRadius: BorderRadius.circular(2),
+ ),
+ ),
+ ),
+
+ // Header Row
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ const Text(
+ "Select Destination",
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ fontFamily: 'GeneralSans',
+ ),
+ ),
+ IconButton(
+ icon: const Icon(Icons.add),
+ onPressed: _createNewProject,
+ tooltip: "Create New Project",
+ ),
+ ],
+ ),
+ ),
+
+ // Search Bar
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ child: SizedBox(
+ height: 42,
+ child: TextField(
+ controller: _searchController,
+ onChanged: _filterProjects,
+ decoration: InputDecoration(
+ hintText: "Search Projects",
+ prefixIcon: const Icon(
+ Icons.search,
+ size: 20,
+ color: Color(0xFF9F9FA9),
+ ),
+ filled: true,
+ fillColor: const Color(0xFFE4E4E7),
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(8),
+ borderSide: BorderSide.none,
+ ),
+ contentPadding: const EdgeInsets.symmetric(
+ vertical: 0,
+ horizontal: 16,
+ ),
+ ),
+ ),
+ ),
+ ),
+
+ Divider(color: Colors.grey[200]),
+
+ // Content
+ Expanded(
+ child:
+ _isLoading
+ ? const Center(child: CircularProgressIndicator())
+ : ListView(
+ controller: widget.scrollController,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 8,
+ ),
+ children: [
+ if (_searchQuery.isEmpty &&
+ _recentViewModels.isNotEmpty) ...[
+ const Padding(
+ padding: EdgeInsets.only(bottom: 8, top: 8),
+ child: Text(
+ "Recent Projects/Events",
+ style: TextStyle(
+ fontSize: 14,
+ fontFamily: 'GeneralSans',
+ color: Color(0xFF27272A),
+ fontWeight: FontWeight.w400,
+ ),
+ ),
+ ),
+ ..._recentViewModels.map((vm) => _buildRecentItem(vm)),
+ const SizedBox(height: 16),
+ ],
+
+ Padding(
+ padding: const EdgeInsets.only(bottom: 8),
+ child: Text(
+ _searchQuery.isEmpty
+ ? "All Projects/Events"
+ : "Search Results",
+ style: const TextStyle(
+ fontSize: 14,
+ fontFamily: 'GeneralSans',
+ color: Color(0xFF27272A),
+ fontWeight: FontWeight.w400,
+ ),
+ ),
+ ),
+ ..._filteredGroupedProjects.map(
+ (g) => _buildProjectGroup(g),
+ ),
+ const SizedBox(height: 40),
+ ],
+ ),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildRecentItem(ProjectItemViewModel vm) {
+ return Container(
+ margin: const EdgeInsets.only(bottom: 8),
+ child: InkWell(
+ onTap: () => widget.onProjectSelected(vm.id, vm.title),
+ borderRadius: BorderRadius.circular(16),
+ child: Container(
+ padding: const EdgeInsets.fromLTRB(4, 4, 0, 4),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(color: const Color(0xFFE4E4E7), width: 1),
+ ),
+ child: Row(
+ children: [
+ Container(
+ width: 56,
+ height: 56,
+ decoration: BoxDecoration(
+ color: const Color(0xFFFAFAFA),
+ borderRadius: BorderRadius.circular(8),
+ image:
+ vm.coverPath != null
+ ? DecorationImage(
+ image: FileImage(File(vm.coverPath!)),
+ fit: BoxFit.cover,
+ )
+ : null,
+ ),
+ child:
+ vm.coverPath == null
+ ? Icon(Icons.image, color: Colors.grey[400], size: 28)
+ : null,
+ ),
+ const SizedBox(width: 10),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ if (vm.isEvent && vm.parentTitle != null)
+ Padding(
+ padding: const EdgeInsets.only(bottom: 2),
+ child: Text(
+ vm.parentTitle!,
+ style: const TextStyle(
+ fontFamily: 'GeneralSans',
+ fontSize: 12,
+ color: Color(0xFF27272A),
+ fontWeight: FontWeight.w400,
+ ),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ Text(
+ vm.title,
+ style: const TextStyle(
+ fontFamily: 'GeneralSans',
+ fontSize: 16,
+ fontWeight: FontWeight.w500,
+ color: Color(0xFF27272A),
+ ),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildProjectGroup(ProjectGroup g) {
+ final hasEvents = g.events.isNotEmpty;
+ return Container(
+ margin: const EdgeInsets.only(bottom: 8),
+ clipBehavior: Clip.antiAlias,
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(color: const Color(0xFFE4E4E7)),
+ ),
+ child: Column(
+ children: [
+ ListTile(
+ onTap:
+ () =>
+ hasEvents
+ ? setState(() => g.isExpanded = !g.isExpanded)
+ : widget.onProjectSelected(
+ g.project.id!,
+ g.project.title,
+ ),
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 4,
+ ),
+ visualDensity: VisualDensity.compact,
+ leading: Container(
+ width: 48,
+ height: 48,
+ decoration: BoxDecoration(
+ color: Colors.grey[100],
+ borderRadius: BorderRadius.circular(12),
+ image:
+ g.coverPath != null
+ ? DecorationImage(
+ image: FileImage(File(g.coverPath!)),
+ fit: BoxFit.cover,
+ )
+ : null,
+ ),
+ child:
+ g.coverPath == null
+ ? Icon(Icons.folder, color: Colors.grey[500])
+ : null,
+ ),
+ title: Text(
+ g.project.title,
+ style: const TextStyle(
+ fontFamily: 'GeneralSans',
+ fontSize: 16,
+ fontWeight: FontWeight.w500,
+ color: Color(0xFF27272A),
+ ),
+ ),
+ trailing:
+ hasEvents
+ ? IconButton(
+ icon: Icon(
+ g.isExpanded
+ ? Icons.keyboard_arrow_up
+ : Icons.keyboard_arrow_down,
+ color: Colors.grey[600],
+ ),
+ onPressed: () {
+ setState(() => g.isExpanded = !g.isExpanded);
+ },
+ )
+ : null,
+ ),
+ if (hasEvents && g.isExpanded)
+ AnimatedCrossFade(
+ firstChild: const SizedBox.shrink(),
+ secondChild: Container(
+ width: double.infinity,
+ color: const Color(0xFFF9FAFB),
+ child: Column(
+ children:
+ g.events.map((e) {
+ return ListTile(
+ onTap: () => widget.onProjectSelected(e.id, e.title),
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 24,
+ vertical: 2,
+ ),
+ visualDensity: VisualDensity.compact,
+ leading: Container(
+ width: 40,
+ height: 40,
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(10),
+ border: Border.all(color: Colors.grey.shade200),
+ image:
+ e.coverPath != null
+ ? DecorationImage(
+ image: FileImage(File(e.coverPath!)),
+ fit: BoxFit.cover,
+ )
+ : null,
+ ),
+ child:
+ e.coverPath == null
+ ? const Icon(
+ Icons.event,
+ size: 20,
+ color: Colors.grey,
+ )
+ : null,
+ ),
+ title: Text(
+ e.title,
+ style: const TextStyle(
+ fontFamily: 'GeneralSans',
+ fontSize: 15,
+ fontWeight: FontWeight.w500,
+ color: Color(0xFF27272A),
+ ),
+ ),
+ );
+ }).toList(),
+ ),
+ ),
+ crossFadeState:
+ g.isExpanded
+ ? CrossFadeState.showSecond
+ : CrossFadeState.showFirst,
+ duration: const Duration(milliseconds: 200),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/ui/pages/define_brand_page.dart b/lib/ui/pages/define_brand_page.dart
@@ -59,7 +59,7 @@ class _DefineBrandPageState extends State<DefineBrandPage> {
}
Future<void> _handleFinish() async {
- // 1. Basic Validation - only Project Name is required
+ // 1. Basic Validation
if (_projectNameController.text.trim().isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Project name is required.')),
@@ -72,31 +72,19 @@ class _DefineBrandPageState extends State<DefineBrandPage> {
});
try {
- // 2. Prepare the data
- final brandData = {
- 'description': _descriptionController.text.trim(),
- 'problem': _problemController.text.trim(),
- 'goal': _goalController.text.trim(),
- 'keywords': _keywords,
- 'competitors': _competitorBrands.map((b) => b['name']).toList(),
- 'appear': _whereWillAppearController.text.trim(),
- };
-
- // 3. Call the Service
- debugPrint('BrandData: $brandData');
- await _projectService.createProject(
- _projectNameController.text.trim().isEmpty
- ? 'Untitled Project'
- : _projectNameController.text.trim(),
- description:
- _descriptionController.text.trim().isEmpty
- ? null
- : _descriptionController.text.trim(),
+ // 2. Prepare data
+ final String title = _projectNameController.text.trim();
+ final String description = _descriptionController.text.trim();
+
+ // 3. Call the Service and capture the NEW ID
+ final int newId = await _projectService.createProject(
+ title,
+ description: description.isEmpty ? null : description,
);
- // 4. Success Handling
+ // 4. Success Handling - Return the ID and Title map
if (mounted) {
- Navigator.pop(context, true);
+ Navigator.pop(context, {'id': newId, 'title': title});
}
} catch (e) {
// 5. Error Handling
diff --git a/lib/ui/pages/project_file_page.dart b/lib/ui/pages/project_file_page.dart
@@ -54,7 +54,22 @@ class _ProjectFilePageState extends State<ProjectFilePage> {
);
if (pickedFile != null && mounted) {
- _showAddFileDialog(File(pickedFile.path));
+ // Navigate to CreateFilePage with the selected image
+ await Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder:
+ (_) => CreateFilePage(
+ file: File(pickedFile.path),
+ projectId: widget.projectId, // PASS PROJECT ID
+ ),
+ ),
+ );
+
+ // Refresh the list after returning from the creation flow
+ if (mounted) {
+ _loadData();
+ }
}
} catch (e) {
debugPrint("Error picking file: $e");
@@ -169,7 +184,8 @@ class _ProjectFilePageState extends State<ProjectFilePage> {
context,
MaterialPageRoute(
// Passing no file implies a "Blank Canvas"
- builder: (_) => const CreateFilePage(),
+ // Pass projectId so CreateFilePage knows we are in a specific project context
+ builder: (_) => CreateFilePage(projectId: widget.projectId),
),
).then((_) => _loadData());
}