creek

The AI Image Editor of 2030
commit b04800d0a5ef4cf944514446fdbae47869f09506
parent eba934413bf3c55de786272ffb12fc9b680f4b32
Author: maydayv7 <maydayv7@gmail.com>
Date:   Thu,  4 Dec 2025 13:47:08 +0530

Format all dart files

Diffstat:
Mlib/data/models/project_model.dart | 2--
Mlib/services/analysis_queue_manager.dart | 14+++++++-------
Mlib/services/analyze/image_analyzer.dart | 2+-
Mlib/services/file_service.dart | 2+-
Mlib/services/flask_service.dart | 25++++++++++++++-----------
Mlib/services/python_service.dart | 17+++++++++--------
Mlib/services/stylesheet_service.dart | 38+++++++++++++++++++++++++++++++-------
Mlib/ui/pages/canvas_page.dart | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Mlib/ui/pages/canvas_toolbar/magic_draw_overlay.dart | 10+++++-----
Mlib/ui/pages/home_page.dart | 688+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Mlib/ui/pages/image_save_page.dart | 24+++++++++---------------
Mlib/ui/pages/project_board_page.dart | 9+++++----
Mlib/ui/pages/project_detail_page.dart | 454++++++++++++++++++++++++++++++++++++++-----------------------------------------
Mlib/ui/pages/project_file_page.dart | 70+++++++++++++++++++++++++++++++++++-----------------------------------
Mlib/ui/pages/share_handler_page.dart | 5+----
Mlib/ui/pages/share_to_file_page.dart | 3---
Mlib/ui/pages/stylesheet_page.dart | 226+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Mlib/ui/widgets/top_bar.dart | 309+++++++++++++++++++++++++++++++++++++++++--------------------------------------
18 files changed, 1099 insertions(+), 909 deletions(-)

diff --git a/lib/data/models/project_model.dart b/lib/data/models/project_model.dart @@ -61,6 +61,4 @@ class ProjectModel { : [], ); } - - } diff --git a/lib/services/analysis_queue_manager.dart b/lib/services/analysis_queue_manager.dart @@ -124,10 +124,10 @@ class AnalysisQueueManager { } final appDir = await getApplicationDocumentsDirectory(); - - final cropDirPath = p.join(appDir.path, 'crops'); + + final cropDirPath = p.join(appDir.path, 'crops'); final cropDir = Directory(cropDirPath); - + if (!await cropDir.exists()) { await cropDir.create(recursive: true); } @@ -187,14 +187,14 @@ class AnalysisQueueManager { } await _noteRepo.updateNote( - note.id!, + note.id!, status: 'analyzing', - cropFilePath: cropPath + cropFilePath: cropPath, ); // 3. Analysis debugPrint("[Queue]: Analyzing Note ${note.id} tag: ${note.category}"); - + // Now when this calls generateAsset internally, the DB lookup will succeed final result = await ImageAnalyzerService.analyzeSelected(cropPath, [ note.category, @@ -204,7 +204,7 @@ class AnalysisQueueManager { if (result['success'] == true && result['data'] != null) { final resultsMap = result['data']['results'] ?? {}; final jsonString = jsonEncode(resultsMap); - + await _noteRepo.updateNote( note.id!, analysisData: jsonString, diff --git a/lib/services/analyze/image_analyzer.dart b/lib/services/analyze/image_analyzer.dart @@ -364,7 +364,7 @@ class ImageAnalyzerService { 'Lighting': {"scores": results[5]['scores']}, 'Colour Palette': { "scores": results[1]['scores'], - "palette": results[1]['palette'] + "palette": results[1]['palette'], }, 'Emotions': {"scores": results[4]['scores']}, 'Era': {"scores": results[6]['scores']}, diff --git a/lib/services/file_service.dart b/lib/services/file_service.dart @@ -85,5 +85,5 @@ class FileService { Future<void> renameFile(String id, String newName) async { await _repo.updateDetails(id, name: newName); - } + } } diff --git a/lib/services/flask_service.dart b/lib/services/flask_service.dart @@ -53,10 +53,12 @@ class FlaskService { debugPrint("🔗 [Pipeline] Starting Sketch-to-Image..."); // 1. Analyze Sketch (Use cached description if available) - final String? sketchDescription = imageDescription ?? await describeImage( - imagePath: sketchPath, - prompt: '<MORE_DETAILED_CAPTION>', - ); + final String? sketchDescription = + imageDescription ?? + await describeImage( + imagePath: sketchPath, + prompt: '<MORE_DETAILED_CAPTION>', + ); if (sketchDescription == null) { debugPrint("❌ [Pipeline] Failed: Could not analyze sketch."); @@ -81,13 +83,14 @@ class FlaskService { ); if (magicPrompt != null) { - await _logToFile("debug_magic_prompt.txt", magicPrompt); - debugPrint("🐛 [DEBUG] 4. Magic Prompt: $magicPrompt"); + await _logToFile("debug_magic_prompt.txt", magicPrompt); + debugPrint("🐛 [DEBUG] 4. Magic Prompt: $magicPrompt"); } else { - debugPrint("🐛 [DEBUG] 4. Magic Prompt: null"); + debugPrint("🐛 [DEBUG] 4. Magic Prompt: null"); } - final String globalPrompt = magicPrompt ?? "$userPrompt. The image features: $sketchDescription"; + final String globalPrompt = + magicPrompt ?? "$userPrompt. The image features: $sketchDescription"; debugPrint("🔗 [Pipeline] Generating base image..."); @@ -142,10 +145,10 @@ class FlaskService { ); if (magicPrompt != null) { - await _logToFile("debug_magic_prompt.txt", magicPrompt); - debugPrint("🐛 [DEBUG] 4. Magic Prompt: $magicPrompt"); + await _logToFile("debug_magic_prompt.txt", magicPrompt); + debugPrint("🐛 [DEBUG] 4. Magic Prompt: $magicPrompt"); } else { - debugPrint("🐛 [DEBUG] 4. Magic Prompt: null"); + debugPrint("🐛 [DEBUG] 4. Magic Prompt: null"); } final String globalPrompt = diff --git a/lib/services/python_service.dart b/lib/services/python_service.dart @@ -3,9 +3,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; class PythonService { - static const MethodChannel _channel = MethodChannel( - 'com.creek.ui/methods', - ); + static const MethodChannel _channel = MethodChannel('com.creek.ui/methods'); // 1. Layout Analysis (OpenCV) Future<Map<String, dynamic>> analyzeLayout(String imagePath) async { @@ -74,11 +72,14 @@ class PythonService { required String userPrompt, }) async { try { - final String? result = await _channel.invokeMethod('generateMagicPrompt', { - 'stylesheetJson': stylesheetJson, - 'caption': caption, - 'userPrompt': userPrompt, - }); + final String? result = await _channel.invokeMethod( + 'generateMagicPrompt', + { + 'stylesheetJson': stylesheetJson, + 'caption': caption, + 'userPrompt': userPrompt, + }, + ); return result; } catch (e) { debugPrint("Magic Prompt Generation Error: $e"); diff --git a/lib/services/stylesheet_service.dart b/lib/services/stylesheet_service.dart @@ -50,10 +50,25 @@ class StylesheetService { return StylesheetData( colors: colors, fonts: fonts, - graphics: _extractStrings(data, ['Graphics', 'graphics'], valueKey: 'path'), - compositions: _extractStrings(data, ['Compositions', 'Composition', 'compositions']), - materialLook: _extractStrings(data, ['Material look', 'Material Look', 'material_look']), - textures: _extractStrings(data, ['Textures', 'Background/Texture', 'textures']), + graphics: _extractStrings(data, [ + 'Graphics', + 'graphics', + ], valueKey: 'path'), + compositions: _extractStrings(data, [ + 'Compositions', + 'Composition', + 'compositions', + ]), + materialLook: _extractStrings(data, [ + 'Material look', + 'Material Look', + 'material_look', + ]), + textures: _extractStrings(data, [ + 'Textures', + 'Background/Texture', + 'textures', + ]), lighting: _extractStrings(data, ['Lighting', 'lighting']), style: _extractStrings(data, ['Style', 'style']), era: _extractStrings(data, ['Era/Cultural Reference', 'Era', 'era']), @@ -133,7 +148,12 @@ class StylesheetService { // 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', 'Fonts']); + dynamic fontData = _findValue(data, [ + 'Typography', + 'fonts', + 'typography', + 'Fonts', + ]); if (fontData == null) return []; List<String> rawNames = []; @@ -160,7 +180,7 @@ class StylesheetService { 'Colour Palette', 'Color Palette', 'colors', - 'Colors' + 'Colors', ]); if (colorData == null) return []; @@ -185,7 +205,11 @@ class StylesheetService { // Generic helper to extract a list of strings from various keys // Supports extraction from [{ "label": "val" }] or ["val"] or "val" - List<String> _extractStrings(Map<String, dynamic> data, List<String> keys, {String valueKey = 'label'}) { + List<String> _extractStrings( + Map<String, dynamic> data, + List<String> keys, { + String valueKey = 'label', + }) { dynamic rawData = _findValue(data, keys); if (rawData == null) return []; diff --git a/lib/ui/pages/canvas_page.dart b/lib/ui/pages/canvas_page.dart @@ -1290,25 +1290,50 @@ class _CanvasPageState extends State<CanvasPage> { right: 0, child: Center( child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), decoration: BoxDecoration( color: Colors.black87, borderRadius: BorderRadius.circular(30), boxShadow: [ - BoxShadow(color: Colors.black26, blurRadius: 8, offset: Offset(0, 2)) - ] + BoxShadow( + color: Colors.black26, + blurRadius: 8, + offset: Offset(0, 2), + ), + ], ), child: Row( mainAxisSize: MainAxisSize.min, children: [ if (_isRemovingBg) ...[ - const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)), - const SizedBox(width: 8), - const Text("Removing...", style: TextStyle(color: Colors.white, fontFamily: 'GeneralSans', fontSize: 14)), + const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ), + const SizedBox(width: 8), + const Text( + "Removing...", + style: TextStyle( + color: Colors.white, + fontFamily: 'GeneralSans', + fontSize: 14, + ), + ), ] else ...[ - const Text( + const Text( "Remove background?", - style: TextStyle(color: Colors.white, fontFamily: 'GeneralSans', fontSize: 14), + style: TextStyle( + color: Colors.white, + fontFamily: 'GeneralSans', + fontSize: 14, + ), ), const SizedBox(width: 8), InkWell( @@ -1319,15 +1344,26 @@ class _CanvasPageState extends State<CanvasPage> { color: Colors.white, shape: BoxShape.circle, ), - child: const Icon(Icons.check, size: 16, color: Colors.black), + child: const Icon( + Icons.check, + size: 16, + color: Colors.black, + ), ), ), const SizedBox(width: 8), InkWell( - onTap: () => setState(() => _showBgRemovalBanner = false), - child: const Icon(Icons.close, size: 18, color: Colors.white54), + onTap: + () => setState( + () => _showBgRemovalBanner = false, + ), + child: const Icon( + Icons.close, + size: 18, + color: Colors.white54, + ), ), - ] + ], ], ), ), @@ -1525,7 +1561,7 @@ class _CanvasPageState extends State<CanvasPage> { Future<void> _confirmRemoveBackground() async { if (_bgRemovalTargetId == null || _bgRemovalTargetPath == null) return; - + _bgRemovalBannerTimer?.cancel(); setState(() { @@ -1534,35 +1570,39 @@ class _CanvasPageState extends State<CanvasPage> { try { // Call service to remove background - final newPath = await FlaskService().generateAsset(imagePath: _bgRemovalTargetPath!); + final newPath = await FlaskService().generateAsset( + imagePath: _bgRemovalTargetPath!, + ); if (newPath != null && mounted) { // Find the element and update its content final index = elements.indexWhere((e) => e['id'] == _bgRemovalTargetId); if (index != -1) { - final oldState = _getCurrentState(); - setState(() { - elements[index]['content'] = newPath; // Replace with transparent png - _showBgRemovalBanner = false; - _isRemovingBg = false; - _hasUnsavedChanges = true; - }); - _recordChange(oldState); + final oldState = _getCurrentState(); + setState(() { + elements[index]['content'] = + newPath; // Replace with transparent png + _showBgRemovalBanner = false; + _isRemovingBg = false; + _hasUnsavedChanges = true; + }); + _recordChange(oldState); } } } catch (e) { debugPrint("BG Removal Failed: $e"); } finally { - if (mounted) setState(() { - _isRemovingBg = false; - _showBgRemovalBanner = false; - }); + if (mounted) + setState(() { + _isRemovingBg = false; + _showBgRemovalBanner = false; + }); } } Future<void> _processInpainting(String prompt, String modelId) async { // Lock editing and hide keyboard - FocusScope.of(context).unfocus(); + FocusScope.of(context).unfocus(); setState(() => _isInpainting = true); _resetInactivityTimer(); @@ -1591,15 +1631,15 @@ class _CanvasPageState extends State<CanvasPage> { // CASE 1: INPAINTING (When hasImageLayers is TRUE) if (hasImageLayers) { if (modelId == 'inpaint_api') { - // Call API Inpainting - newImageUrl = await FlaskService().inpaintApiImage( + // Call API Inpainting + newImageUrl = await FlaskService().inpaintApiImage( imagePath: _tempBaseImage!.path, maskPath: maskFile.path, prompt: prompt, ); } else { - // Local Inpainting - newImageUrl = await FlaskService().inpaintImage( + // Local Inpainting + newImageUrl = await FlaskService().inpaintImage( imagePath: _tempBaseImage!.path, maskPath: maskFile.path, prompt: prompt, @@ -1608,15 +1648,14 @@ class _CanvasPageState extends State<CanvasPage> { } // CASE 2: SKETCH TO IMAGE (When hasImageLayers is FALSE) else { - if(modelId == 'sketch_fusion'){ + if (modelId == 'sketch_fusion') { newImageUrl = await FlaskService().sketchToImage( projectId: widget.projectId, sketchPath: _tempBaseImage!.path, userPrompt: prompt, imageDescription: _aiDescription, ); - } - else if(modelId == 'sketch_advanced'){ + } else if (modelId == 'sketch_advanced') { newImageUrl = await FlaskService().sketchToImageAPI( projectId: widget.projectId, sketchPath: _tempBaseImage!.path, @@ -1624,8 +1663,7 @@ class _CanvasPageState extends State<CanvasPage> { option: 1, imageDescription: _aiDescription, ); - } - else if (modelId == 'sketch_creative') { + } else if (modelId == 'sketch_creative') { newImageUrl = await FlaskService().sketchToImageAPI( projectId: widget.projectId, sketchPath: _tempBaseImage!.path, diff --git a/lib/ui/pages/canvas_toolbar/magic_draw_overlay.dart b/lib/ui/pages/canvas_toolbar/magic_draw_overlay.dart @@ -81,13 +81,13 @@ class _MagicDrawToolsState extends State<MagicDrawTools> { // --- 2. LIST FOR SKETCH TO IMAGE (When No Image Exists) --- final List<AIModelOption> _sketchModels = [ + AIModelOption(id: 'sketch_advanced', name: 'Nano Banana', badge: 'Premium'), + AIModelOption(id: 'sketch_creative', name: 'FLUX Dev', badge: 'Fast'), AIModelOption( - id: 'sketch_advanced', - name: 'Nano Banana', - badge: 'Premium', + id: 'sketch_fusion', + name: 'Stable Diffusion v1.5', + badge: null, ), - AIModelOption(id: 'sketch_creative', name: 'FLUX Dev', badge: 'Fast'), - AIModelOption(id: 'sketch_fusion', name: 'Stable Diffusion v1.5', badge: null), ]; @override diff --git a/lib/ui/pages/home_page.dart b/lib/ui/pages/home_page.dart @@ -198,65 +198,84 @@ class _HomePageState extends State<HomePage> { final didRename = await showDialog<bool>( context: context, - builder: (ctx) => AlertDialog( - backgroundColor: theme.cardColor, - title: Text( - 'Rename Project', - style: TextStyle( - fontFamily: 'GeneralSans', - fontSize: 18, - color: theme.colorScheme.onSurface, - ), - ), - content: TextField( - controller: controller, - autofocus: true, - style: TextStyle(fontFamily: 'GeneralSans', color: theme.colorScheme.onSurface), - decoration: InputDecoration( - hintText: 'Enter new name', - hintStyle: TextStyle(color: theme.colorScheme.onSurface.withValues(alpha: 0.5)), - enabledBorder: UnderlineInputBorder( - borderSide: BorderSide(color: theme.colorScheme.primary.withValues(alpha: 0.5)), + builder: + (ctx) => AlertDialog( + backgroundColor: theme.cardColor, + title: Text( + 'Rename Project', + style: TextStyle( + fontFamily: 'GeneralSans', + fontSize: 18, + color: theme.colorScheme.onSurface, + ), ), - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide(color: theme.colorScheme.primary), + content: TextField( + controller: controller, + autofocus: true, + style: TextStyle( + fontFamily: 'GeneralSans', + color: theme.colorScheme.onSurface, + ), + decoration: InputDecoration( + hintText: 'Enter new name', + hintStyle: TextStyle( + color: theme.colorScheme.onSurface.withValues(alpha: 0.5), + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: theme.colorScheme.primary.withValues(alpha: 0.5), + ), + ), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: theme.colorScheme.primary), + ), + ), + textCapitalization: TextCapitalization.sentences, ), + actions: [ + TextButton( + child: Text( + 'Cancel', + style: TextStyle( + color: theme.colorScheme.onSurface.withValues(alpha: 0.7), + ), + ), + onPressed: () => Navigator.pop(ctx, false), + ), + TextButton( + child: Text( + 'Save', + style: TextStyle(color: theme.colorScheme.primary), + ), + onPressed: () { + if (controller.text.trim().isNotEmpty) { + Navigator.pop(ctx, true); + } + }, + ), + ], ), - textCapitalization: TextCapitalization.sentences, - ), - actions: [ - TextButton( - child: Text('Cancel', style: TextStyle(color: theme.colorScheme.onSurface.withValues(alpha: 0.7))), - onPressed: () => Navigator.pop(ctx, false), - ), - TextButton( - child: Text('Save', style: TextStyle(color: theme.colorScheme.primary)), - onPressed: () { - if (controller.text.trim().isNotEmpty) { - Navigator.pop(ctx, true); - } - }, - ), - ], - ), ); if (didRename == true && project.id != null) { setState(() => _isLoading = true); try { - await _projectService.updateProjectDetails(project.id!, title: controller.text.trim()); + await _projectService.updateProjectDetails( + project.id!, + title: controller.text.trim(), + ); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Project renamed')), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('Project renamed'))); } _loadData(); } catch (e) { if (mounted) { setState(() => _isLoading = false); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Error renaming: $e')), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Error renaming: $e'))); } } } @@ -267,31 +286,43 @@ class _HomePageState extends State<HomePage> { final theme = Theme.of(context); final confirm = await showDialog<bool>( context: context, - builder: (ctx) => AlertDialog( - backgroundColor: theme.cardColor, - title: Text( - 'Delete Project', - style: TextStyle( - fontFamily: 'GeneralSans', - fontSize: 18, - color: theme.colorScheme.onSurface, - ), - ), - content: Text( - 'Are you sure you want to delete "${project.title}"? This cannot be undone.', - style: TextStyle(fontFamily: 'GeneralSans', color: theme.colorScheme.onSurface.withValues(alpha: 0.8)), - ), - actions: [ - TextButton( - child: Text('Cancel', style: TextStyle(color: theme.colorScheme.onSurface.withValues(alpha: 0.7))), - onPressed: () => Navigator.pop(ctx, false), - ), - TextButton( - child: const Text('Delete', style: TextStyle(color: Colors.red)), - onPressed: () => Navigator.pop(ctx, true), + builder: + (ctx) => AlertDialog( + backgroundColor: theme.cardColor, + title: Text( + 'Delete Project', + style: TextStyle( + fontFamily: 'GeneralSans', + fontSize: 18, + color: theme.colorScheme.onSurface, + ), + ), + content: Text( + 'Are you sure you want to delete "${project.title}"? This cannot be undone.', + style: TextStyle( + fontFamily: 'GeneralSans', + color: theme.colorScheme.onSurface.withValues(alpha: 0.8), + ), + ), + actions: [ + TextButton( + child: Text( + 'Cancel', + style: TextStyle( + color: theme.colorScheme.onSurface.withValues(alpha: 0.7), + ), + ), + onPressed: () => Navigator.pop(ctx, false), + ), + TextButton( + child: const Text( + 'Delete', + style: TextStyle(color: Colors.red), + ), + onPressed: () => Navigator.pop(ctx, true), + ), + ], ), - ], - ), ); if (confirm == true && project.id != null) { @@ -307,9 +338,9 @@ class _HomePageState extends State<HomePage> { } catch (e) { if (mounted) { setState(() => _isLoading = false); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Error deleting project: $e')), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Error deleting project: $e'))); } } } @@ -347,12 +378,13 @@ class _HomePageState extends State<HomePage> { Navigator.push( context, MaterialPageRoute( - builder: (_) => CanvasPage( - projectId: file.projectId, - width: width, - height: height, - existingFile: file, - ), + builder: + (_) => CanvasPage( + projectId: file.projectId, + width: width, + height: height, + existingFile: file, + ), ), ).then((_) => _loadData()); } @@ -575,7 +607,7 @@ class _HomePageState extends State<HomePage> { onRename: () => _renameProject(p), onDelete: () => _deleteProject(p), isHorizontal: false, - showGrid: false, + showGrid: false, ), ), ), @@ -594,24 +626,23 @@ class _HomePageState extends State<HomePage> { ), ), const SizedBox(height: 8), - ...filteredFiles.map( - (f) { - final meta = _fileMetadata[f.id] ?? {}; - return Padding( - padding: const EdgeInsets.only(bottom: 12), - child: _FileCard( - file: f, - theme: theme, - isDark: isDark, - breadcrumb: _getProjectBreadcrumb(f), - dimensions: meta['dimensions'] ?? 'Unknown', - previewPath: meta['preview'] ?? '', - timeAgo: _formatTimeAgo(f.lastUpdated), - onTap: () => _openFile(f), - ), - ); - }, - ), + ...filteredFiles.map((f) { + final meta = _fileMetadata[f.id] ?? {}; + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: _FileCard( + file: f, + theme: theme, + isDark: isDark, + breadcrumb: _getProjectBreadcrumb(f), + dimensions: + meta['dimensions'] ?? 'Unknown', + previewPath: meta['preview'] ?? '', + timeAgo: _formatTimeAgo(f.lastUpdated), + onTap: () => _openFile(f), + ), + ); + }), ], ]), ), @@ -634,33 +665,27 @@ class _HomePageState extends State<HomePage> { ), ), const SizedBox(height: 12), - ...(_recentFiles - .take(2) - .map( - (file) { - final meta = _fileMetadata[file.id] ?? {}; - return Padding( - padding: const EdgeInsets.only( - bottom: 12, - ), - child: _FileCard( - file: file, - theme: theme, - isDark: isDark, - breadcrumb: _getProjectBreadcrumb( - file, - ), - dimensions: meta['dimensions'] ?? 'Unknown', - previewPath: meta['preview'] ?? '', - timeAgo: _formatTimeAgo( - file.lastUpdated, - ), - onTap: () => _openFile(file), - ), - ); - }, - ) - .toList()), + ...(_recentFiles.take(2).map((file) { + final meta = _fileMetadata[file.id] ?? {}; + return Padding( + padding: const EdgeInsets.only( + bottom: 12, + ), + child: _FileCard( + file: file, + theme: theme, + isDark: isDark, + breadcrumb: _getProjectBreadcrumb(file), + dimensions: + meta['dimensions'] ?? 'Unknown', + previewPath: meta['preview'] ?? '', + timeAgo: _formatTimeAgo( + file.lastUpdated, + ), + onTap: () => _openFile(file), + ), + ); + }).toList()), const SizedBox(height: 24), ], _buildSectionHeader( @@ -673,37 +698,41 @@ class _HomePageState extends State<HomePage> { ), ), const SizedBox(height: 12), - + // Show "Create Project" if empty, else list _allProjects.isEmpty ? _buildCreateProjectCard(theme, isDark) : SizedBox( - height: 140, // Reduced height - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: _allProjects.length, - itemBuilder: (context, index) { - final project = _allProjects[index]; - return Padding( - padding: const EdgeInsets.only( - right: 12, - ), - child: _ProjectCard( - project: project, - theme: theme, - isDark: isDark, - previewImages: - _projectPreviews[project.id] ?? - [], - onTap: () => _openProject(project), - onRename: () => _renameProject(project), - onDelete: () => _deleteProject(project), - showGrid: false, - ), - ); - }, - ), + height: 140, // Reduced height + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: _allProjects.length, + itemBuilder: (context, index) { + final project = _allProjects[index]; + return Padding( + padding: const EdgeInsets.only( + right: 12, + ), + child: _ProjectCard( + project: project, + theme: theme, + isDark: isDark, + previewImages: + _projectPreviews[project + .id] ?? + [], + onTap: + () => _openProject(project), + onRename: + () => _renameProject(project), + onDelete: + () => _deleteProject(project), + showGrid: false, + ), + ); + }, ), + ), const SizedBox(height: 24), _buildSectionHeader( 'Explore templates', @@ -722,14 +751,15 @@ class _HomePageState extends State<HomePage> { ), ), ), - floatingActionButton: _allProjects.isEmpty - ? null - : FloatingActionButton( - onPressed: _createNewProject, - backgroundColor: isDark ? Colors.grey[900] : Colors.grey[900], - foregroundColor: Colors.white, - child: const Icon(Icons.add, size: 24), - ), + floatingActionButton: + _allProjects.isEmpty + ? null + : FloatingActionButton( + onPressed: _createNewProject, + backgroundColor: isDark ? Colors.grey[900] : Colors.grey[900], + foregroundColor: Colors.white, + child: const Icon(Icons.add, size: 24), + ), ); } @@ -774,7 +804,7 @@ class _HomePageState extends State<HomePage> { return GestureDetector( onTap: _createNewProject, child: Container( - height: 130, + height: 130, width: 130, margin: const EdgeInsets.only(right: 12), decoration: BoxDecoration( @@ -941,14 +971,14 @@ class _FileCard extends StatelessWidget { borderRadius: BorderRadius.circular(8), ), child: Row( - crossAxisAlignment: CrossAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ SizedBox( width: 88, height: 88, child: Center( child: Container( - width: 80, + width: 80, height: 80, decoration: BoxDecoration( color: isDark ? Colors.grey[800] : Colors.grey[200], @@ -963,34 +993,36 @@ class _FileCard extends StatelessWidget { ), child: ClipRRect( borderRadius: BorderRadius.circular(8), - child: previewPath.isNotEmpty - ? Image.file( - File(previewPath), - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) => - Container( - color: isDark ? Colors.grey[800] : Colors.grey[200], - child: Icon( - Icons.broken_image, - size: 20, - color: - theme.colorScheme.onSurface.withValues( - alpha: 0.3, + child: + previewPath.isNotEmpty + ? Image.file( + File(previewPath), + fit: BoxFit.cover, + errorBuilder: + (context, error, stackTrace) => Container( + color: + isDark + ? Colors.grey[800] + : Colors.grey[200], + child: Icon( + Icons.broken_image, + size: 20, + color: theme.colorScheme.onSurface + .withValues(alpha: 0.3), ), - ), - ), - ) - : Container( - color: isDark ? Colors.grey[800] : Colors.grey[200], - child: Icon( - Icons.image, - size: 24, - color: - theme.colorScheme.onSurface.withValues( - alpha: 0.3, ), + ) + : Container( + color: + isDark ? Colors.grey[800] : Colors.grey[200], + child: Icon( + Icons.image, + size: 24, + color: theme.colorScheme.onSurface.withValues( + alpha: 0.3, + ), + ), ), - ), ), ), ), @@ -1001,7 +1033,8 @@ class _FileCard extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 4), child: Column( crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, // Keep height minimal for centering + mainAxisSize: + MainAxisSize.min, // Keep height minimal for centering children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, @@ -1125,40 +1158,46 @@ class _ProjectCard extends StatelessWidget { children: [ Expanded( child: ClipRRect( - borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), - child: showGrid - ? Padding( - padding: const EdgeInsets.all(4.0), - child: _buildGridPreview(), - ) - : (previewImages.isNotEmpty - ? Image.file( - File(previewImages.first), - fit: BoxFit.cover, - width: double.infinity, - errorBuilder: (context, error, stackTrace) => - Container( - color: isDark ? Colors.grey[800] : Colors.grey[200], - child: Icon( - Icons.broken_image, - color: theme.colorScheme.onSurface.withValues( - alpha: 0.3, - ), - ), - ), - ) - : Container( - color: isDark ? Colors.grey[800] : Colors.grey[200], - child: Center( - child: Icon( - Icons.folder_outlined, - size: 32, - color: theme.colorScheme.onSurface.withValues( - alpha: 0.3, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(12), + ), + child: + showGrid + ? Padding( + padding: const EdgeInsets.all(4.0), + child: _buildGridPreview(), + ) + : (previewImages.isNotEmpty + ? Image.file( + File(previewImages.first), + fit: BoxFit.cover, + width: double.infinity, + errorBuilder: + (context, error, stackTrace) => Container( + color: + isDark + ? Colors.grey[800] + : Colors.grey[200], + child: Icon( + Icons.broken_image, + color: theme.colorScheme.onSurface + .withValues(alpha: 0.3), + ), + ), + ) + : Container( + color: + isDark ? Colors.grey[800] : Colors.grey[200], + child: Center( + child: Icon( + Icons.folder_outlined, + size: 32, + color: theme.colorScheme.onSurface.withValues( + alpha: 0.3, + ), ), ), - ), - )), + )), ), ), Padding( @@ -1186,7 +1225,9 @@ class _ProjectCard extends StatelessWidget { icon: Icon( Icons.more_vert, size: 16, - color: theme.colorScheme.onSurface.withValues(alpha: 0.6), + color: theme.colorScheme.onSurface.withValues( + alpha: 0.6, + ), ), padding: EdgeInsets.zero, shape: RoundedRectangleBorder( @@ -1199,28 +1240,42 @@ class _ProjectCard extends StatelessWidget { onDelete!(); } }, - itemBuilder: (context) => [ - const PopupMenuItem( - value: 'rename', - child: Row( - children: [ - Icon(Icons.edit_outlined, size: 18), - SizedBox(width: 12), - Text('Rename', style: TextStyle(fontFamily: 'GeneralSans')), - ], - ), - ), - const PopupMenuItem( - value: 'delete', - child: Row( - children: [ - Icon(Icons.delete_outline, size: 18, color: Colors.red), - SizedBox(width: 12), - Text('Delete', style: TextStyle(fontFamily: 'GeneralSans', color: Colors.red)), - ], - ), - ), - ], + itemBuilder: + (context) => [ + const PopupMenuItem( + value: 'rename', + child: Row( + children: [ + Icon(Icons.edit_outlined, size: 18), + SizedBox(width: 12), + Text( + 'Rename', + style: TextStyle(fontFamily: 'GeneralSans'), + ), + ], + ), + ), + const PopupMenuItem( + value: 'delete', + child: Row( + children: [ + Icon( + Icons.delete_outline, + size: 18, + color: Colors.red, + ), + SizedBox(width: 12), + Text( + 'Delete', + style: TextStyle( + fontFamily: 'GeneralSans', + color: Colors.red, + ), + ), + ], + ), + ), + ], ), ), ], @@ -1282,16 +1337,21 @@ class _ProjectCard extends StatelessWidget { borderRadius: BorderRadius.circular(6), // Rounded grid items ), clipBehavior: Clip.antiAlias, - child: imagePath != null - ? Image.file( - File(imagePath), - fit: BoxFit.cover, - width: double.infinity, - height: double.infinity, - errorBuilder: (context, error, stackTrace) => - const Icon(Icons.broken_image, size: 16, color: Colors.grey), - ) - : null, // Empty placeholder + child: + imagePath != null + ? Image.file( + File(imagePath), + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + errorBuilder: + (context, error, stackTrace) => const Icon( + Icons.broken_image, + size: 16, + color: Colors.grey, + ), + ) + : null, // Empty placeholder ); } } @@ -1348,12 +1408,12 @@ class _SeeAllPageState extends State<_SeeAllPage> { List<String> eventImages = []; // Get 1 image from up to 4 distinct events for (final event in events.take(4)) { - if (event.id != null) { - final imgs = await widget.imageRepo.getImages(event.id!); - if (imgs.isNotEmpty) { - eventImages.add(imgs.first.filePath); - } - } + if (event.id != null) { + final imgs = await widget.imageRepo.getImages(event.id!); + if (imgs.isNotEmpty) { + eventImages.add(imgs.first.filePath); + } + } } _projectPreviews[p.id!] = eventImages; } @@ -1446,63 +1506,65 @@ class _SeeAllPageState extends State<_SeeAllPage> { elevation: 0, iconTheme: IconThemeData(color: theme.colorScheme.onSurface), ), - body: _isLoading - ? const Center(child: CircularProgressIndicator()) - : Padding( - padding: const EdgeInsets.all(16.0), - child: widget.isProjects - ? GridView.builder( - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - childAspectRatio: 0.8, - crossAxisSpacing: 12, - mainAxisSpacing: 12, - ), - itemCount: _items.length, - itemBuilder: (context, index) { - final project = _items[index] as ProjectModel; - return _ProjectCard( - project: project, - theme: theme, - isDark: isDark, - previewImages: _projectPreviews[project.id] ?? [], - onTap: () => widget.onProjectTap(project), - // Chain callbacks, reload data on completion - onRename: () async { - await widget.onProjectRename(project); - _fetchData(); + body: + _isLoading + ? const Center(child: CircularProgressIndicator()) + : Padding( + padding: const EdgeInsets.all(16.0), + child: + widget.isProjects + ? GridView.builder( + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 0.8, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + itemCount: _items.length, + itemBuilder: (context, index) { + final project = _items[index] as ProjectModel; + return _ProjectCard( + project: project, + theme: theme, + isDark: isDark, + previewImages: _projectPreviews[project.id] ?? [], + onTap: () => widget.onProjectTap(project), + // Chain callbacks, reload data on completion + onRename: () async { + await widget.onProjectRename(project); + _fetchData(); + }, + onDelete: () async { + await widget.onProjectDelete(project); + _fetchData(); + }, + isHorizontal: false, + showGrid: true, + ); }, - onDelete: () async { - await widget.onProjectDelete(project); - _fetchData(); + ) + : ListView.builder( + itemCount: _items.length, + itemBuilder: (context, index) { + final file = _items[index] as FileModel; + final meta = _fileMetadata[file.id] ?? {}; + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: _FileCard( + file: file, + theme: theme, + isDark: isDark, + breadcrumb: _getBreadcrumb(file), + dimensions: meta['dimensions'] ?? 'Unknown', + previewPath: meta['preview'] ?? '', + timeAgo: _timeAgo(file.lastUpdated), + onTap: () => widget.onFileTap(file), + ), + ); }, - isHorizontal: false, - showGrid: true, - ); - }, - ) - : ListView.builder( - itemCount: _items.length, - itemBuilder: (context, index) { - final file = _items[index] as FileModel; - final meta = _fileMetadata[file.id] ?? {}; - return Padding( - padding: const EdgeInsets.only(bottom: 12), - child: _FileCard( - file: file, - theme: theme, - isDark: isDark, - breadcrumb: _getBreadcrumb(file), - dimensions: meta['dimensions'] ?? 'Unknown', - previewPath: meta['preview'] ?? '', - timeAgo: _timeAgo(file.lastUpdated), - onTap: () => widget.onFileTap(file), - ), - ); - }, - ), - ), + ), + ), ); } } diff --git a/lib/ui/pages/image_save_page.dart b/lib/ui/pages/image_save_page.dart @@ -566,7 +566,8 @@ class _ImageSavePageState extends State<ImageSavePage> { height: 16 / 12, ), decoration: InputDecoration( - hintText: "I love the serif font and how it is used...", + hintText: + "I love the serif font and how it is used...", hintStyle: const TextStyle( fontFamily: 'GeneralSans', fontSize: 12, @@ -703,10 +704,11 @@ class _ImageSavePageState extends State<ImageSavePage> { Navigator.pushAndRemoveUntil( context, MaterialPageRoute( - builder: (_) => ProjectBoardPage( - projectId: widget.projectId, - initialShowAlternateView: true, // Show grid view - ), + builder: + (_) => ProjectBoardPage( + projectId: widget.projectId, + initialShowAlternateView: true, // Show grid view + ), ), (route) => false, // Remove all previous routes ); @@ -764,21 +766,13 @@ class _ImageSavePageState extends State<ImageSavePage> { // CONFIRM SELECTION BUTTON (Visible only in resizing mode) if (_isResizing && _finalSelectionRect != null) IconButton( - icon: const Icon( - Icons.check, - color: Color(0xFF7C86FF), - size: 24, - ), + icon: const Icon(Icons.check, color: Color(0xFF7C86FF), size: 24), onPressed: _confirmSelectionAndShowModal, ), // CANCEL SELECTION BUTTON (Visible only in drawing/resizing mode) if (_isDrawMode || _isResizing) IconButton( - icon: const Icon( - Icons.close, - color: Colors.black87, - size: 24, - ), + icon: const Icon(Icons.close, color: Colors.black87, size: 24), onPressed: _resetSelectionMode, ), ], diff --git a/lib/ui/pages/project_board_page.dart b/lib/ui/pages/project_board_page.dart @@ -180,10 +180,11 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> { Navigator.push( context, MaterialPageRoute( - builder: (_) => StylesheetPage( - projectId: _currentProject!.id!, - autoGenerate: true, - ), + builder: + (_) => StylesheetPage( + projectId: _currentProject!.id!, + autoGenerate: true, + ), ), ); }, diff --git a/lib/ui/pages/project_detail_page.dart b/lib/ui/pages/project_detail_page.dart @@ -92,226 +92,229 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { context: context, backgroundColor: Colors.transparent, isScrollControlled: true, - builder: (context) => Container( - decoration: const BoxDecoration( - color: Color(0xFFFAFAFA), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(24), - topRight: Radius.circular(24), - ), - ), - child: Padding( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Drag handle - Container( - padding: const EdgeInsets.only(top: 8, bottom: 16), - child: Center( - child: Container( - width: 48, - height: 2, - decoration: BoxDecoration( - color: const Color(0xFF71717B), - borderRadius: BorderRadius.circular(100), - ), - ), - ), + builder: + (context) => Container( + decoration: const BoxDecoration( + color: Color(0xFFFAFAFA), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(24), + topRight: Radius.circular(24), ), - // Header section - Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide( - color: Color(0xFFE4E4E7), - width: 1, - ), - ), - ), - child: const Text( - "Event Details", - style: TextStyle( - fontFamily: 'GeneralSans', - fontSize: 14, - fontWeight: FontWeight.normal, - color: Color(0xFF27272A), - height: 20 / 14, - ), - ), + ), + child: Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, ), - // Content section - Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Name field - Container( - height: 40, - decoration: BoxDecoration( - color: const Color(0xFFE4E4E7), - borderRadius: BorderRadius.circular(8), - ), - child: TextField( - controller: nameController, - autofocus: true, - style: const TextStyle( - fontFamily: 'GeneralSans', - fontSize: 12, - fontWeight: FontWeight.normal, - color: Color(0xFF27272A), - height: 16 / 12, - ), - decoration: const InputDecoration( - hintText: "Add Name*", - hintStyle: TextStyle( - fontFamily: 'GeneralSans', - fontSize: 12, - fontWeight: FontWeight.normal, - color: Color(0xFF71717B), - height: 16 / 12, - ), - contentPadding: EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Drag handle + Container( + padding: const EdgeInsets.only(top: 8, bottom: 16), + child: Center( + child: Container( + width: 48, + height: 2, + decoration: BoxDecoration( + color: const Color(0xFF71717B), + borderRadius: BorderRadius.circular(100), ), ), ), - const SizedBox(height: 16), - // Description field - Container( - height: 80, - decoration: BoxDecoration( - color: const Color(0xFFE4E4E7), - borderRadius: BorderRadius.circular(8), + ), + // Header section + Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: Color(0xFFE4E4E7), width: 1), ), - child: TextField( - controller: descriptionController, - maxLines: null, - expands: true, - textAlignVertical: TextAlignVertical.top, - style: const TextStyle( - fontFamily: 'GeneralSans', - fontSize: 12, - fontWeight: FontWeight.normal, - color: Color(0xFF27272A), - height: 16 / 12, - ), - decoration: const InputDecoration( - hintText: "Add Description", - hintStyle: TextStyle( - fontFamily: 'GeneralSans', - fontSize: 12, - fontWeight: FontWeight.normal, - color: Color(0xFF71717B), - height: 16 / 12, - ), - contentPadding: EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - ), + ), + child: const Text( + "Event Details", + style: TextStyle( + fontFamily: 'GeneralSans', + fontSize: 14, + fontWeight: FontWeight.normal, + color: Color(0xFF27272A), + height: 20 / 14, ), ), - ], - ), - ), - // Bottom actions - Container( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 32), - child: Row( - children: [ - // Cancel button - Expanded( - child: GestureDetector( - onTap: () => Navigator.pop(context), - child: Container( + ), + // Content section + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Name field + Container( height: 40, decoration: BoxDecoration( color: const Color(0xFFE4E4E7), - borderRadius: BorderRadius.circular(1000), + borderRadius: BorderRadius.circular(8), ), - child: const Center( - child: Text( - "Cancel", - style: TextStyle( + child: TextField( + controller: nameController, + autofocus: true, + style: const TextStyle( + fontFamily: 'GeneralSans', + fontSize: 12, + fontWeight: FontWeight.normal, + color: Color(0xFF27272A), + height: 16 / 12, + ), + decoration: const InputDecoration( + hintText: "Add Name*", + hintStyle: TextStyle( fontFamily: 'GeneralSans', - fontSize: 14, - fontWeight: FontWeight.w500, - color: Color(0xFF27272A), - height: 20 / 14, - letterSpacing: 0.25, + fontSize: 12, + fontWeight: FontWeight.normal, + color: Color(0xFF71717B), + height: 16 / 12, + ), + contentPadding: EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, ), + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, ), ), ), - ), - ), - const SizedBox(width: 8), - // Add Event button - Expanded( - child: GestureDetector( - onTap: () async { - if (nameController.text.trim().isNotEmpty) { - try { - await _projectService.createProject( - nameController.text.trim(), - description: - descriptionController.text.trim().isEmpty - ? null - : descriptionController.text.trim(), - parentId: _project!.id, - ); - if (context.mounted) { - Navigator.pop(context); - _loadData(); // Refresh to show new event - } - } catch (e) { - debugPrint("Error creating event: $e"); - } - } - }, - child: Container( - height: 40, + const SizedBox(height: 16), + // Description field + Container( + height: 80, decoration: BoxDecoration( - color: const Color(0xFF27272A), - borderRadius: BorderRadius.circular(1000), + color: const Color(0xFFE4E4E7), + borderRadius: BorderRadius.circular(8), ), - child: const Center( - child: Text( - "Add Event", - style: TextStyle( + child: TextField( + controller: descriptionController, + maxLines: null, + expands: true, + textAlignVertical: TextAlignVertical.top, + style: const TextStyle( + fontFamily: 'GeneralSans', + fontSize: 12, + fontWeight: FontWeight.normal, + color: Color(0xFF27272A), + height: 16 / 12, + ), + decoration: const InputDecoration( + hintText: "Add Description", + hintStyle: TextStyle( fontFamily: 'GeneralSans', - fontSize: 14, - fontWeight: FontWeight.w500, - color: Color(0xFFFAFAFA), - height: 20 / 14, - letterSpacing: 0.25, + fontSize: 12, + fontWeight: FontWeight.normal, + color: Color(0xFF71717B), + height: 16 / 12, ), + contentPadding: EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, ), ), ), - ), + ], ), - ], - ), + ), + // Bottom actions + Container( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 32), + child: Row( + children: [ + // Cancel button + Expanded( + child: GestureDetector( + onTap: () => Navigator.pop(context), + child: Container( + height: 40, + decoration: BoxDecoration( + color: const Color(0xFFE4E4E7), + borderRadius: BorderRadius.circular(1000), + ), + child: const Center( + child: Text( + "Cancel", + style: TextStyle( + fontFamily: 'GeneralSans', + fontSize: 14, + fontWeight: FontWeight.w500, + color: Color(0xFF27272A), + height: 20 / 14, + letterSpacing: 0.25, + ), + ), + ), + ), + ), + ), + const SizedBox(width: 8), + // Add Event button + Expanded( + child: GestureDetector( + onTap: () async { + if (nameController.text.trim().isNotEmpty) { + try { + await _projectService.createProject( + nameController.text.trim(), + description: + descriptionController.text + .trim() + .isEmpty + ? null + : descriptionController.text.trim(), + parentId: _project!.id, + ); + if (context.mounted) { + Navigator.pop(context); + _loadData(); // Refresh to show new event + } + } catch (e) { + debugPrint("Error creating event: $e"); + } + } + }, + child: Container( + height: 40, + decoration: BoxDecoration( + color: const Color(0xFF27272A), + borderRadius: BorderRadius.circular(1000), + ), + child: const Center( + child: Text( + "Add Event", + style: TextStyle( + fontFamily: 'GeneralSans', + fontSize: 14, + fontWeight: FontWeight.w500, + color: Color(0xFFFAFAFA), + height: 20 / 14, + letterSpacing: 0.25, + ), + ), + ), + ), + ), + ), + ], + ), + ), + ], ), - ], + ), ), - ), - ), ); } @@ -395,7 +398,7 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 16), - + // Action Cards Row _buildActionCardsRow(_project!.id!), @@ -415,13 +418,15 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(12), - border: Border.all( - color: Colors.grey[300]!, - ), + border: Border.all(color: Colors.grey[300]!), ), child: Column( children: [ - Icon(Icons.event_note, size: 48, color: Colors.grey[400]), + Icon( + Icons.event_note, + size: 48, + color: Colors.grey[400], + ), const SizedBox(height: 8), Text( "No events created yet", @@ -439,9 +444,7 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { separatorBuilder: (_, __) => const SizedBox(height: 16), itemBuilder: (context, index) { final event = _events[index]; - return Center( - child: _buildEventCard(event), - ); + return Center(child: _buildEventCard(event)); }, ), @@ -499,10 +502,7 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), - border: Border.all( - color: const Color(0xFFE4E4E7), - width: 1, - ), + border: Border.all(color: const Color(0xFFE4E4E7), width: 1), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -530,9 +530,7 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { blobImage, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { - return Container( - color: const Color(0xFF27272A), - ); + return Container(color: const Color(0xFF27272A)); }, ), ), @@ -606,12 +604,7 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { return Container( padding: const EdgeInsets.only(bottom: 8, top: 16), decoration: const BoxDecoration( - border: Border( - bottom: BorderSide( - color: Color(0xFFE4E4E7), - width: 1, - ), - ), + border: Border(bottom: BorderSide(color: Color(0xFFE4E4E7), width: 1)), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -652,7 +645,7 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { Widget _buildEventCard(ProjectModel event) { final eventImages = _eventImages[event.id] ?? []; - + return GestureDetector( onTap: () { if (event.id != null) { @@ -663,10 +656,7 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { width: 328, decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), - border: Border.all( - color: const Color(0xFFE4E4E7), - width: 1, - ), + border: Border.all(color: const Color(0xFFE4E4E7), width: 1), ), clipBehavior: Clip.antiAlias, child: Column( @@ -683,19 +673,16 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { Expanded( child: ClipRRect( borderRadius: BorderRadius.circular(4), - child: i < eventImages.length - ? Image.file( - File(eventImages[i].filePath), - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Container( - color: Colors.grey[300], - ); - }, - ) - : Container( - color: Colors.grey[200], - ), + child: + i < eventImages.length + ? Image.file( + File(eventImages[i].filePath), + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container(color: Colors.grey[300]); + }, + ) + : Container(color: Colors.grey[200]), ), ), if (i < 2) const SizedBox(width: 4), @@ -722,4 +709,4 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { ), ); } -} -\ No newline at end of file +} diff --git a/lib/ui/pages/project_file_page.dart b/lib/ui/pages/project_file_page.dart @@ -398,9 +398,6 @@ class _ProjectFilePageState extends State<ProjectFilePage> { ); } - - - // --------------------------------------- // BREADCRUMB // --------------------------------------- @@ -531,7 +528,9 @@ class _ProjectFilePageState extends State<ProjectFilePage> { ), const SizedBox(height: 16), Text( - _search.isEmpty ? "No files yet" : "No files found", + _search.isEmpty + ? "No files yet" + : "No files found", style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, @@ -626,38 +625,40 @@ class _ProjectFilePageState extends State<ProjectFilePage> { _eventFiles.isEmpty ? Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(vertical: 48), - child: Column( - children: [ - Icon( - Icons.insert_drive_file_outlined, - size: 64, - color: Colors.grey[400], - ), - const SizedBox(height: 16), - Text( - "No files in this event yet", - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - fontFamily: 'GeneralSans', - color: Colors.grey[600], - ), + width: double.infinity, + padding: const EdgeInsets.symmetric( + vertical: 48, + ), + child: Column( + children: [ + Icon( + Icons.insert_drive_file_outlined, + size: 64, + color: Colors.grey[400], + ), + const SizedBox(height: 16), + Text( + "No files in this event yet", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + fontFamily: 'GeneralSans', + color: Colors.grey[600], ), - const SizedBox(height: 8), - Text( - "Create a file to add it to this event", - style: TextStyle( - fontSize: 14, - fontFamily: 'GeneralSans', - color: Colors.grey[500], - ), - textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + "Create a file to add it to this event", + style: TextStyle( + fontSize: 14, + fontFamily: 'GeneralSans', + color: Colors.grey[500], ), - ], - ), - ) + textAlign: TextAlign.center, + ), + ], + ), + ) : Column( children: _eventFiles @@ -746,7 +747,6 @@ class _ProjectFilePageState extends State<ProjectFilePage> { ); } - // --------------------------------------- // FILTER // --------------------------------------- diff --git a/lib/ui/pages/share_handler_page.dart b/lib/ui/pages/share_handler_page.dart @@ -82,10 +82,7 @@ class _ShareHandlerPageState extends State<ShareHandlerPage> { Navigator.pushReplacement( context, MaterialPageRoute( - builder: - (_) => ShareToFilePage( - sharedImage: tempFiles.first, - ), + builder: (_) => ShareToFilePage(sharedImage: tempFiles.first), ), ); } else { diff --git a/lib/ui/pages/share_to_file_page.dart b/lib/ui/pages/share_to_file_page.dart @@ -253,8 +253,6 @@ class _ShareToFilePageState extends State<ShareToFilePage> { } } - - String _formatDate(DateTime date) { final now = DateTime.now(); final diff = now.difference(date); @@ -574,7 +572,6 @@ class _ShareToFilePageState extends State<ShareToFilePage> { ); } - Widget _buildFileItem(FileModel file) { final preview = _resolvePreviewPath(file); diff --git a/lib/ui/pages/stylesheet_page.dart b/lib/ui/pages/stylesheet_page.dart @@ -19,7 +19,11 @@ class StylesheetPage extends StatefulWidget { final int projectId; final bool autoGenerate; - const StylesheetPage({super.key, required this.projectId, this.autoGenerate = false}); + const StylesheetPage({ + super.key, + required this.projectId, + this.autoGenerate = false, + }); @override State<StylesheetPage> createState() => _StylesheetPageState(); @@ -95,14 +99,19 @@ class _StylesheetPageState extends State<StylesheetPage> { String _formatLabel(String label) { String clean = label.replaceAll(RegExp(r'[-_]'), ' '); List<String> words = clean.split(' '); - return words.map((w) { - if (w.isEmpty) return ''; - return w[0].toUpperCase() + w.substring(1).toLowerCase(); - }).join(' '); + return words + .map((w) { + if (w.isEmpty) return ''; + return w[0].toUpperCase() + w.substring(1).toLowerCase(); + }) + .join(' '); } String? _findAssetPath(Map<String, String> assetMap, String label) { - String normalized = label.toLowerCase().replaceAll(RegExp(r'[^a-z0-9]'), ''); + String normalized = label.toLowerCase().replaceAll( + RegExp(r'[^a-z0-9]'), + '', + ); return assetMap[normalized]; } @@ -151,15 +160,27 @@ class _StylesheetPageState extends State<StylesheetPage> { try { final images = await ImageRepo().getImages(_currentProjectId); - final List<String> analysisData = images.map((img) => img.analysisData).where((data) => data != null && data.isNotEmpty).cast<String>().toList(); + final List<String> analysisData = + images + .map((img) => img.analysisData) + .where((data) => data != null && data.isNotEmpty) + .cast<String>() + .toList(); final notes = await NoteRepo().getNotesByProjectId(_currentProjectId); - final List<String> noteAnalysisData = notes.map((n) => n.analysisData).where((data) => data != null && data.isNotEmpty).cast<String>().toList(); + final List<String> noteAnalysisData = + notes + .map((n) => n.analysisData) + .where((data) => data != null && data.isNotEmpty) + .cast<String>() + .toList(); analysisData.addAll(noteAnalysisData); if (analysisData.isEmpty) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("No analyzed images or notes found."))); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("No analyzed images or notes found.")), + ); } return; } @@ -185,21 +206,28 @@ class _StylesheetPageState extends State<StylesheetPage> { appBar: TopBar( currentProjectId: _currentProjectId, onBack: () => Navigator.of(context).pop(), - onProjectChanged: (p) => setState(() { - _currentProjectId = p.id!; - _stylesheetData = null; - _projectAssets = []; - _logoPaths = []; - _loadSavedStylesheet(); - }), + onProjectChanged: + (p) => setState(() { + _currentProjectId = p.id!; + _stylesheetData = null; + _projectAssets = []; + _logoPaths = []; + _loadSavedStylesheet(); + }), onSettingsPressed: () {}, ), - body: _isLoading - ? const Center(child: CircularProgressIndicator(color: Variables.textPrimary)) - : (_stylesheetData == null && _rawJsonString == null) + body: + _isLoading + ? const Center( + child: CircularProgressIndicator(color: Variables.textPrimary), + ) + : (_stylesheetData == null && _rawJsonString == null) ? _buildEmptyState() : _buildContent(), - bottomNavigationBar: BottomBar(currentTab: BottomBarItem.stylesheet, projectId: _currentProjectId), + bottomNavigationBar: BottomBar( + currentTab: BottomBarItem.stylesheet, + projectId: _currentProjectId, + ), ); } @@ -233,19 +261,22 @@ class _StylesheetPageState extends State<StylesheetPage> { children: [ Center(child: _buildGenerateButton("Regenerate Stylesheet")), const SizedBox(height: 24), - + _buildLogosSection(null), - if (data.graphics.isNotEmpty || _projectAssets.isNotEmpty) _buildGraphicsSection(data.graphics), + if (data.graphics.isNotEmpty || _projectAssets.isNotEmpty) + _buildGraphicsSection(data.graphics), if (data.colors.isNotEmpty) _buildColorsSection(data.colors), if (data.fonts.isNotEmpty) _buildFontsSection(data.fonts), - if (data.compositions.isNotEmpty) _buildCompositionsSection(data.compositions), - if (data.materialLook.isNotEmpty) _buildMaterialLookSection(data.materialLook), + if (data.compositions.isNotEmpty) + _buildCompositionsSection(data.compositions), + if (data.materialLook.isNotEmpty) + _buildMaterialLookSection(data.materialLook), if (data.textures.isNotEmpty) _buildTexturesSection(data.textures), if (data.lighting.isNotEmpty) _buildLightingSection(data.lighting), if (data.style.isNotEmpty) _buildStyleSection(data.style), if (data.era.isNotEmpty) _buildEraSection(data.era), if (data.emotions.isNotEmpty) _buildEmotionsSection(data.emotions), - + const SizedBox(height: 24), ], ), @@ -259,8 +290,8 @@ class _StylesheetPageState extends State<StylesheetPage> { child: Container( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), decoration: BoxDecoration( - color: Variables.textPrimary, - borderRadius: BorderRadius.circular(112) + color: Variables.textPrimary, + borderRadius: BorderRadius.circular(112), ), child: Row( mainAxisSize: MainAxisSize.min, @@ -270,9 +301,12 @@ class _StylesheetPageState extends State<StylesheetPage> { const SizedBox(width: 8), SvgPicture.asset( 'assets/icons/generate_icon.svg', - width: 18, + width: 18, height: 18, - colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), + colorFilter: const ColorFilter.mode( + Colors.white, + BlendMode.srcIn, + ), ), ], ), @@ -281,7 +315,11 @@ class _StylesheetPageState extends State<StylesheetPage> { } // Modified to support a custom trailing widget (e.g. Eye Icon) - Widget _buildSectionHeader(String title, {bool showArrow = true, Widget? trailing}) { + Widget _buildSectionHeader( + String title, { + bool showArrow = true, + Widget? trailing, + }) { return Padding( padding: const EdgeInsets.only(bottom: 12), child: Row( @@ -308,7 +346,10 @@ class _StylesheetPageState extends State<StylesheetPage> { 'assets/icons/arrow-left-s-line.svg', width: 24, height: 24, - colorFilter: const ColorFilter.mode(Variables.textPrimary, BlendMode.srcIn), + colorFilter: const ColorFilter.mode( + Variables.textPrimary, + BlendMode.srcIn, + ), ), ), ], @@ -319,7 +360,9 @@ class _StylesheetPageState extends State<StylesheetPage> { // --- Logos --- Future<void> _pickLogo() async { try { - final XFile? pickedFile = await _picker.pickImage(source: ImageSource.gallery); + final XFile? pickedFile = await _picker.pickImage( + source: ImageSource.gallery, + ); if (pickedFile != null) { setState(() { _logoPaths.add(pickedFile.path); @@ -335,10 +378,13 @@ class _StylesheetPageState extends State<StylesheetPage> { if (data is List) { for (var item in data) { - if (item is Map && item.containsKey('path')) logoPaths.add(item['path'].toString()); - else if (item is String) logoPaths.add(item); + if (item is Map && item.containsKey('path')) + logoPaths.add(item['path'].toString()); + else if (item is String) + logoPaths.add(item); } - } else if (data is String) logoPaths.add(data); + } else if (data is String) + logoPaths.add(data); return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -364,8 +410,12 @@ class _StylesheetPageState extends State<StylesheetPage> { child: Center( child: SvgPicture.asset( 'assets/icons/add-line.svg', - width: 20, height: 20, - colorFilter: const ColorFilter.mode(Variables.textSecondary, BlendMode.srcIn), + width: 20, + height: 20, + colorFilter: const ColorFilter.mode( + Variables.textSecondary, + BlendMode.srcIn, + ), ), ), ), @@ -397,7 +447,8 @@ class _StylesheetPageState extends State<StylesheetPage> { scrollDirection: Axis.horizontal, itemCount: graphicPaths.length, separatorBuilder: (_, __) => const SizedBox(width: 8), - itemBuilder: (context, index) => _buildGraphicCard(graphicPaths[index]), + itemBuilder: + (context, index) => _buildGraphicCard(graphicPaths[index]), ), ), const SizedBox(height: 16), @@ -418,14 +469,19 @@ class _StylesheetPageState extends State<StylesheetPage> { color: Variables.surfaceSubtle, ), clipBehavior: Clip.antiAlias, - child: file != null - ? Image.file(file, fit: BoxFit.cover) - : Container( - color: Variables.surfaceSubtle, - child: const Center( - child: Icon(Icons.broken_image, color: Colors.grey, size: 20), + child: + file != null + ? Image.file(file, fit: BoxFit.cover) + : Container( + color: Variables.surfaceSubtle, + child: const Center( + child: Icon( + Icons.broken_image, + color: Colors.grey, + size: 20, + ), + ), ), - ), ); }, ); @@ -478,19 +534,22 @@ class _StylesheetPageState extends State<StylesheetPage> { Text( "Aa", style: sampleStyle.copyWith( - fontSize: 28, - height: 1.2, - fontWeight: FontWeight.w400, + fontSize: 28, + height: 1.2, + fontWeight: FontWeight.w400, color: Variables.textPrimary, ), ), Text( - displayFontName, + displayFontName, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle( - fontFamily: 'GeneralSans', fontSize: 12, height: 16 / 12, - color: Variables.textSecondary, fontWeight: FontWeight.w400, + fontFamily: 'GeneralSans', + fontSize: 12, + height: 16 / 12, + color: Variables.textSecondary, + fontWeight: FontWeight.w400, ), ), ], @@ -507,7 +566,7 @@ class _StylesheetPageState extends State<StylesheetPage> { children: [ // Using custom trailing widget for the Eye Icon toggle _buildSectionHeader( - "Colors", + "Colors", showArrow: false, trailing: GestureDetector( onTap: () => setState(() => _showHexCodes = !_showHexCodes), @@ -519,8 +578,10 @@ class _StylesheetPageState extends State<StylesheetPage> { ), ), Wrap( - spacing: 8, runSpacing: 8, - children: colors.take(5).map((color) => _buildColorSwatch(color)).toList(), + spacing: 8, + runSpacing: 8, + children: + colors.take(5).map((color) => _buildColorSwatch(color)).toList(), ), const SizedBox(height: 16), ], @@ -528,25 +589,32 @@ class _StylesheetPageState extends State<StylesheetPage> { } Widget _buildColorSwatch(Color color) { - final String hexCode = '#${color.value.toRadixString(16).padLeft(8, '0').substring(2).toUpperCase()}'; - final Color textColor = color.computeLuminance() > 0.5 ? Colors.black : Colors.white; + final String hexCode = + '#${color.value.toRadixString(16).padLeft(8, '0').substring(2).toUpperCase()}'; + final Color textColor = + color.computeLuminance() > 0.5 ? Colors.black : Colors.white; return Container( - width: 104, height: 52, - decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(8)), + width: 104, + height: 52, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(8), + ), alignment: Alignment.center, // Only show text if _showHexCodes is true - child: _showHexCodes - ? Text( - hexCode, - style: TextStyle( - fontFamily: 'GeneralSans', - fontSize: 13, - fontWeight: FontWeight.w500, - color: textColor, - ), - ) - : null, + child: + _showHexCodes + ? Text( + hexCode, + style: TextStyle( + fontFamily: 'GeneralSans', + fontSize: 13, + fontWeight: FontWeight.w500, + color: textColor, + ), + ) + : null, ); } @@ -570,11 +638,14 @@ class _StylesheetPageState extends State<StylesheetPage> { child: Image.asset( assetPath, fit: BoxFit.cover, - errorBuilder: (c, o, s) => Container(color: Variables.surfaceSubtle), + errorBuilder: + (c, o, s) => Container(color: Variables.surfaceSubtle), ), ), Positioned( - left: 0, right: 0, bottom: 0, + left: 0, + right: 0, + bottom: 0, height: 40, child: Container( decoration: BoxDecoration( @@ -587,7 +658,9 @@ class _StylesheetPageState extends State<StylesheetPage> { ), ), Positioned( - left: 4, right: 4, bottom: 4, + left: 4, + right: 4, + bottom: 4, child: Text( formattedLabel, style: const TextStyle( @@ -628,7 +701,11 @@ class _StylesheetPageState extends State<StylesheetPage> { } } - Widget _buildAttributeSection(String title, List<String> items, Map<String, String>? assetMap) { + Widget _buildAttributeSection( + String title, + List<String> items, + Map<String, String>? assetMap, + ) { if (items.isEmpty) return const SizedBox(); return Column( @@ -643,7 +720,8 @@ class _StylesheetPageState extends State<StylesheetPage> { separatorBuilder: (_, __) => const SizedBox(width: 8), itemBuilder: (context, index) { String label = items[index]; - String? assetPath = assetMap != null ? _findAssetPath(assetMap, label) : null; + String? assetPath = + assetMap != null ? _findAssetPath(assetMap, label) : null; return _buildUnifiedCard(label, assetPath); }, ), diff --git a/lib/ui/widgets/top_bar.dart b/lib/ui/widgets/top_bar.dart @@ -36,9 +36,10 @@ class TopBar extends StatefulWidget implements PreferredSizeWidget { State<TopBar> createState() => _TopBarState(); @override - Size get preferredSize => hideSecondRow - ? const Size.fromHeight(48.0) // Height for single row - : const Size.fromHeight(88.0); // Height for two rows + Size get preferredSize => + hideSecondRow + ? const Size.fromHeight(48.0) // Height for single row + : const Size.fromHeight(88.0); // Height for two rows } class _TopBarState extends State<TopBar> { @@ -136,32 +137,33 @@ class _TopBarState extends State<TopBar> { ], // Title Expanded( - child: widget.titleOverride != null - ? Text( - widget.titleOverride!, - style: const TextStyle( - fontFamily: 'GeneralSans', - fontSize: 20, - fontWeight: FontWeight.w500, - height: 24 / 20, // line-height: 24px - color: Variables.textPrimary, - ), - overflow: TextOverflow.ellipsis, - ) - : (!_isLoading && + child: + widget.titleOverride != null + ? Text( + widget.titleOverride!, + style: const TextStyle( + fontFamily: 'GeneralSans', + fontSize: 20, + fontWeight: FontWeight.w500, + height: 24 / 20, // line-height: 24px + color: Variables.textPrimary, + ), + overflow: TextOverflow.ellipsis, + ) + : (!_isLoading && _currentProject != null && _rootProject != null) ? Text( - _rootProject!.title, - style: const TextStyle( - fontFamily: 'GeneralSans', - fontSize: 20, - fontWeight: FontWeight.w500, - height: 24 / 20, - color: Variables.textPrimary, - ), - overflow: TextOverflow.ellipsis, - ) + _rootProject!.title, + style: const TextStyle( + fontFamily: 'GeneralSans', + fontSize: 20, + fontWeight: FontWeight.w500, + height: 24 / 20, + color: Variables.textPrimary, + ), + overflow: TextOverflow.ellipsis, + ) : const SizedBox(), ), // Settings Icon (only show if second row is visible) @@ -184,140 +186,92 @@ class _TopBarState extends State<TopBar> { // Second Row: Global Dropdown and Action Buttons if (!widget.hideSecondRow) Container( - height: 40.0, // py-[8px] = 16px + 24px content - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Row( - children: [ - // Left group: Global Dropdown and Layout Button - Row( - children: [ - // Global Dropdown - if (!_isLoading && - _currentProject != null && - _rootProject != null) - PopupMenuButton<ProjectModel>( - padding: EdgeInsets.zero, - onSelected: (project) => - widget.onProjectChanged?.call(project), - color: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - offset: const Offset(0, 42), - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 10, - ), - decoration: BoxDecoration( - color: Variables.borderSubtle, // #e4e4e7 - borderRadius: BorderRadius.circular(1000), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - _currentProject!.id == _rootProject!.id - ? "Global" - : _currentProject!.title, - style: const TextStyle( - fontFamily: 'GeneralSans', - fontSize: 14, - fontWeight: FontWeight.w500, - height: 20 / 14, - color: Variables.textPrimary, - ), - ), - const SizedBox(width: 8), - const Icon( - Icons.keyboard_arrow_down_rounded, - size: 20, - color: Variables.textPrimary, - ), - ], - ), - ), - itemBuilder: (context) { - return _contextList.map((project) { - final isSelected = project.id == _currentProject!.id; - final isRoot = project.id == _rootProject!.id; - return PopupMenuItem<ProjectModel>( - value: project, - child: Text( - isRoot ? "Global" : project.title, - style: Variables.bodyStyle.copyWith( - fontFamily: 'GeneralSans', - fontWeight: isSelected - ? FontWeight.bold - : FontWeight.normal, - color: isSelected - ? Variables.textPrimary - : Variables.textSecondary, - ), - ), - ); - }).toList(); - }, - ), - // Layout Icon Button (Toggle between All Images/Categorized) or Edit Icon - if (widget.onLayoutToggle != null || widget.onLayoutPressed != null) ...[ - const SizedBox(width: 8), - GestureDetector( - onTap: widget.onLayoutToggle ?? widget.onLayoutPressed, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 10, - ), - decoration: BoxDecoration( - color: Variables.borderSubtle, - borderRadius: BorderRadius.circular(1000), - ), - child: Icon( - widget.onLayoutToggle != null - ? (widget.isAlternateView == true - ? Icons.dashboard - : Icons.view_agenda_outlined) - : Icons.edit, // Use edit icon when only onLayoutPressed is provided - size: 20, - color: Variables.textPrimary, - ), - ), - ), - ], - ], - ), - const Spacer(), - // Right group: Filter and AI/Sparkle Buttons - if (widget.onFilterPressed != null || widget.onAIPressed != null) + height: 40.0, // py-[8px] = 16px + 24px content + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + children: [ + // Left group: Global Dropdown and Layout Button Row( children: [ - // Filter Icon Button - if (widget.onFilterPressed != null) - GestureDetector( - onTap: widget.onFilterPressed, + // Global Dropdown + if (!_isLoading && + _currentProject != null && + _rootProject != null) + PopupMenuButton<ProjectModel>( + padding: EdgeInsets.zero, + onSelected: + (project) => + widget.onProjectChanged?.call(project), + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + offset: const Offset(0, 42), child: Container( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 10, ), decoration: BoxDecoration( - color: Variables.borderSubtle, + color: Variables.borderSubtle, // #e4e4e7 borderRadius: BorderRadius.circular(1000), ), - child: const Icon( - Icons.tune, - size: 20, - color: Variables.textPrimary, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + _currentProject!.id == _rootProject!.id + ? "Global" + : _currentProject!.title, + style: const TextStyle( + fontFamily: 'GeneralSans', + fontSize: 14, + fontWeight: FontWeight.w500, + height: 20 / 14, + color: Variables.textPrimary, + ), + ), + const SizedBox(width: 8), + const Icon( + Icons.keyboard_arrow_down_rounded, + size: 20, + color: Variables.textPrimary, + ), + ], ), ), + itemBuilder: (context) { + return _contextList.map((project) { + final isSelected = + project.id == _currentProject!.id; + final isRoot = project.id == _rootProject!.id; + return PopupMenuItem<ProjectModel>( + value: project, + child: Text( + isRoot ? "Global" : project.title, + style: Variables.bodyStyle.copyWith( + fontFamily: 'GeneralSans', + fontWeight: + isSelected + ? FontWeight.bold + : FontWeight.normal, + color: + isSelected + ? Variables.textPrimary + : Variables.textSecondary, + ), + ), + ); + }).toList(); + }, ), - if (widget.onFilterPressed != null && widget.onAIPressed != null) + // Layout Icon Button (Toggle between All Images/Categorized) or Edit Icon + if (widget.onLayoutToggle != null || + widget.onLayoutPressed != null) ...[ const SizedBox(width: 8), - // AI/Sparkle Icon Button - if (widget.onAIPressed != null) GestureDetector( - onTap: widget.onAIPressed, + onTap: + widget.onLayoutToggle ?? widget.onLayoutPressed, child: Container( padding: const EdgeInsets.symmetric( horizontal: 16, @@ -327,18 +281,75 @@ class _TopBarState extends State<TopBar> { color: Variables.borderSubtle, borderRadius: BorderRadius.circular(1000), ), - child: const Icon( - Icons.auto_awesome, + child: Icon( + widget.onLayoutToggle != null + ? (widget.isAlternateView == true + ? Icons.dashboard + : Icons.view_agenda_outlined) + : Icons + .edit, // Use edit icon when only onLayoutPressed is provided size: 20, color: Variables.textPrimary, ), ), ), + ], ], ), - ], + const Spacer(), + // Right group: Filter and AI/Sparkle Buttons + if (widget.onFilterPressed != null || + widget.onAIPressed != null) + Row( + children: [ + // Filter Icon Button + if (widget.onFilterPressed != null) + GestureDetector( + onTap: widget.onFilterPressed, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 10, + ), + decoration: BoxDecoration( + color: Variables.borderSubtle, + borderRadius: BorderRadius.circular(1000), + ), + child: const Icon( + Icons.tune, + size: 20, + color: Variables.textPrimary, + ), + ), + ), + if (widget.onFilterPressed != null && + widget.onAIPressed != null) + const SizedBox(width: 8), + // AI/Sparkle Icon Button + if (widget.onAIPressed != null) + GestureDetector( + onTap: widget.onAIPressed, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 10, + ), + decoration: BoxDecoration( + color: Variables.borderSubtle, + borderRadius: BorderRadius.circular(1000), + ), + child: const Icon( + Icons.auto_awesome, + size: 20, + color: Variables.textPrimary, + ), + ), + ), + ], + ), + ], + ), ), - ), ], ), ),