creek

The AI Image Editor of 2030
commit 6edff54249844eed179e1c290f0dff9808b28e67
parent 8e027a38a84df683b0e9d16ced86a40736e8b4aa
Author: maydayv7 <maydayv7@gmail.com>
Date:   Sat, 29 Nov 2025 15:59:26 +0530

Run analysis in queue according to tag

Also fix top bar

Diffstat:
Mlib/services/analyze/image_analyzer.dart | 313+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Mlib/services/image_service.dart | 117+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Mlib/ui/pages/image_analysis_page.dart | 137+++++++++++--------------------------------------------------------------------
Mlib/ui/pages/image_details_page.dart | 85+++++++++++++++++++++++--------------------------------------------------------
Dlib/ui/pages/image_page.dart | 1-
Mlib/ui/pages/image_save_page.dart | 13++++++-------
Mlib/ui/widgets/top_bar.dart | 152+++++++++++++++++++++++++++++++++++++++++++------------------------------------
7 files changed, 412 insertions(+), 406 deletions(-)

diff --git a/lib/services/analyze/image_analyzer.dart b/lib/services/analyze/image_analyzer.dart @@ -107,7 +107,33 @@ class ImageAnalyzerService { } } + // --------------------------------------------------------------------------- + // PUBLIC METHODS + // --------------------------------------------------------------------------- + + // Runs ALL analysis models on the image static Future<Map<String, dynamic>> analyzeFullSuite(String imagePath) async { + return _runAnalysisInternal(imagePath, (_) => true); + } + + // Runs ONLY the analysis models corresponding to the provided [tags] + static Future<Map<String, dynamic>> analyzeSelected( + String imagePath, + List<String> tags, + ) async { + return _runAnalysisInternal(imagePath, (validTags) { + return tags.any((tag) => validTags.contains(tag)); + }); + } + + // --------------------------------------------------------------------------- + // INTERNAL LOGIC + // --------------------------------------------------------------------------- + + static Future<Map<String, dynamic>> _runAnalysisInternal( + String imagePath, + bool Function(List<String> validTags) shouldRun, + ) async { final totalSw = Stopwatch()..start(); final RootIsolateToken? token = RootIsolateToken.instance; @@ -119,136 +145,168 @@ class ImageAnalyzerService { // 1. Prepare Assets on Main Thread (Safe) final assetPaths = await _prepareAssets(); + // Helper for skipped tasks to maintain list structure + Future<Map<String, dynamic>> skipTask() async { + return {'success': true, 'scores': {}, 'execution_time': 0}; + } + // 2. Run Analysis final results = await Future.wait([ // --- GROUP A: PARALLEL --- - _runProfiledJob( - name: 'Layout', - imagePath: imagePath, - rootToken: token, - runInIsolate: true, - assetPaths: assetPaths, - task: (path, _) => LayoutAnalyzerService().analyze(path), - ), - - _runProfiledJob( - name: 'Color', - imagePath: imagePath, - rootToken: token, - runInIsolate: true, - assetPaths: assetPaths, - task: (path, _) => ColorAnalyzerService().analyze(path), - ), - - _runProfiledJob( - name: 'Texture', - imagePath: imagePath, - rootToken: token, - runInIsolate: true, - assetPaths: assetPaths, - task: (path, assets) async { - final service = TextureAnalyzerService(); - // Pass specific paths - final res = await service.analyze( - path, - modelPath: assets['texture_model'], - jsonPath: assets['texture_json'], - ); - service.dispose(); - return res; - }, - ), - - _runProfiledJob( - name: 'Embedding', - imagePath: imagePath, - rootToken: token, - runInIsolate: true, - assetPaths: assetPaths, - task: (path, assets) async { - final service = EmbeddingAnalyzerService(); - final res = await service.analyze( - path, - modelPath: assets['clip_model'], - jsonPath: assets['embedding_json'], - ); - service.dispose(); - return res; - }, - ), - - _runProfiledJob( - name: 'Emotional', - imagePath: imagePath, - rootToken: token, - runInIsolate: true, - assetPaths: assetPaths, - task: (path, assets) async { - final service = EmbeddingAnalyzerService(); - final res = await service.analyze( - path, - modelPath: assets['clip_model'], - jsonPath: assets['emotion_json'], - ); - service.dispose(); - return res; - }, - ), - - _runProfiledJob( - name: 'Lighting', - imagePath: imagePath, - rootToken: token, - runInIsolate: true, - assetPaths: assetPaths, - task: (path, assets) async { - final service = EmbeddingAnalyzerService(); - final res = await service.analyze( - path, - modelPath: assets['clip_model'], - jsonPath: assets['lighting_json'], - ); - service.dispose(); - return res; - }, - ), - - _runProfiledJob( - name: 'Era', - imagePath: imagePath, - rootToken: token, - runInIsolate: true, - assetPaths: assetPaths, - task: (path, assets) async { - final service = EmbeddingAnalyzerService(); - final res = await service.analyze( - path, - modelPath: assets['clip_model'], - jsonPath: assets['era_json'], - ); - service.dispose(); - return res; - }, - ), + + // TODO: 'Subject' + + // 1. Layout + shouldRun(['Compositions']) + ? _runProfiledJob( + name: 'Layout', + imagePath: imagePath, + rootToken: token, + runInIsolate: true, + assetPaths: assetPaths, + task: (path, _) => LayoutAnalyzerService().analyze(path), + ) + : skipTask(), + + // 2. Color + shouldRun(['Colours']) + ? _runProfiledJob( + name: 'Color', + imagePath: imagePath, + rootToken: token, + runInIsolate: true, + assetPaths: assetPaths, + task: (path, _) => ColorAnalyzerService().analyze(path), + ) + : skipTask(), + + // 3. Texture + shouldRun(['Texture', 'Material Look']) + ? _runProfiledJob( + name: 'Texture', + imagePath: imagePath, + rootToken: token, + runInIsolate: true, + assetPaths: assetPaths, + task: (path, assets) async { + final service = TextureAnalyzerService(); + final res = await service.analyze( + path, + modelPath: assets['texture_model'], + jsonPath: assets['texture_json'], + ); + service.dispose(); + return res; + }, + ) + : skipTask(), + + // 4. Embeddings + shouldRun(['Style']) + ? _runProfiledJob( + name: 'Embedding', + imagePath: imagePath, + rootToken: token, + runInIsolate: true, + assetPaths: assetPaths, + task: (path, assets) async { + final service = EmbeddingAnalyzerService(); + final res = await service.analyze( + path, + modelPath: assets['clip_model'], + jsonPath: assets['embedding_json'], + ); + service.dispose(); + return res; + }, + ) + : skipTask(), + + // 5. Emotion + shouldRun(['Emotion']) + ? _runProfiledJob( + name: 'Emotional', + imagePath: imagePath, + rootToken: token, + runInIsolate: true, + assetPaths: assetPaths, + task: (path, assets) async { + final service = EmbeddingAnalyzerService(); + final res = await service.analyze( + path, + modelPath: assets['clip_model'], + jsonPath: assets['emotion_json'], + ); + service.dispose(); + return res; + }, + ) + : skipTask(), + + // 6. Lighting + shouldRun(['Lighting']) + ? _runProfiledJob( + name: 'Lighting', + imagePath: imagePath, + rootToken: token, + runInIsolate: true, + assetPaths: assetPaths, + task: (path, assets) async { + final service = EmbeddingAnalyzerService(); + final res = await service.analyze( + path, + modelPath: assets['clip_model'], + jsonPath: assets['lighting_json'], + ); + service.dispose(); + return res; + }, + ) + : skipTask(), + + // 7. Era + shouldRun(['Era']) + ? _runProfiledJob( + name: 'Era', + imagePath: imagePath, + rootToken: token, + runInIsolate: true, + assetPaths: assetPaths, + task: (path, assets) async { + final service = EmbeddingAnalyzerService(); + final res = await service.analyze( + path, + modelPath: assets['clip_model'], + jsonPath: assets['era_json'], + ); + service.dispose(); + return res; + }, + ) + : skipTask(), // --- GROUP B: MAIN THREAD --- - _runProfiledJob( - name: 'Font', - imagePath: imagePath, - rootToken: token, - runInIsolate: false, - assetPaths: assetPaths, - task: (path, assets) async { - final service = FontIdentifierService(); - final res = await service.analyze( - path, - modelPath: assets['fannet_model'], - jsonPath: assets['font_json'], - ); - service.dispose(); - return res; - }, - ), + + // 8. Font + shouldRun(['Fonts']) + ? _runProfiledJob( + name: 'Font', + imagePath: imagePath, + rootToken: token, + runInIsolate: false, + assetPaths: assetPaths, + task: (path, assets) async { + final service = FontIdentifierService(); + final res = await service.analyze( + path, + modelPath: assets['fannet_model'], + jsonPath: assets['font_json'], + ); + service.dispose(); + return res; + }, + ) + : skipTask(), ]); totalSw.stop(); @@ -321,10 +379,11 @@ class ImageAnalyzerService { output.writeln('──────────────────────────────────────────────────────'); subTasks.forEach((key, value) { - final int barLength = (value / 50).ceil().clamp(0, 20); + final int val = value as int; + final int barLength = (val / 50).ceil().clamp(0, 20); final String bar = '█' * barLength; output.writeln( - ' ${key.padRight(12)} : ${value.toString().padLeft(4)}ms $bar', + ' ${key.padRight(12)} : ${val.toString().padLeft(4)}ms $bar', ); }); diff --git a/lib/services/image_service.dart b/lib/services/image_service.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:flutter/foundation.dart'; @@ -11,9 +12,60 @@ class ImageService { final ImageRepo _repo = ImageRepo(); final Uuid _uuid = const Uuid(); - /// Saves a single image and returns its ID. - /// This return type (Future<String>) is REQUIRED for ImageSavePage. - Future<String> saveImage(File file, int projectId) async { + // ANALYSIS QUEUE + // Static queue ensures all instances share the same processing line + static final List<Future<void> Function()> _analysisQueue = []; + static bool _isProcessingQueue = false; + + static void _enqueueAnalysis( + String debugLabel, + Future<void> Function() task, + ) { + debugPrint( + "[Queue] Enqueueing task: $debugLabel. (Queue size: ${_analysisQueue.length + 1})", + ); + _analysisQueue.add(task); + + if (!_isProcessingQueue) { + _processQueue(); + } + } + + static Future<void> _processQueue() async { + if (_isProcessingQueue) return; + _isProcessingQueue = true; + + debugPrint("[Queue] Queue processor started."); + + while (_analysisQueue.isNotEmpty) { + final task = _analysisQueue.removeAt(0); + + try { + debugPrint( + "[Queue] Starting next task. Remaining in queue: ${_analysisQueue.length}", + ); + await task(); + debugPrint("[Queue] Task completed"); + } catch (e) { + debugPrint("[Queue] Task failed: $e"); + } + + // Small delay to let the UI update between heavy jobs + await Future.delayed(const Duration(milliseconds: 100)); + } + + debugPrint("[Queue] Queue empty. All background jobs finished."); + _isProcessingQueue = false; + } + + // --- PUBLIC METHODS --- + + // Saves a single image and returns its ID + Future<String> saveImage( + File file, + int projectId, { + List<String> tags = const [], + }) async { // 1. Prepare Directory final dir = await getApplicationDocumentsDirectory(); final folder = Directory("${dir.path}/images"); @@ -40,41 +92,63 @@ class ImageService { createdAt: DateTime.now(), tags: [], ); - await _repo.addImage(image); - // 4. Run Analysis in Background - _analyzeInBackground(id, newPath); + // 4. Update Tags & Trigger Analysis + await updateTags(id, tags); - // 5. RETURN THE ID (Critical fix) return id; } - /// Bulk save method (optional, but good helper) - Future<List<String>> saveImages(List<File> files, int projectId) async { + // Bulk save method + Future<List<String>> saveImages( + List<File> files, + int projectId, { + List<String> tags = const [], + }) async { List<String> ids = []; for (var file in files) { - // Reuse the single save logic to avoid code duplication - String id = await saveImage(file, projectId); + String id = await saveImage(file, projectId, tags: tags); ids.add(id); } return ids; } - Future<void> _analyzeInBackground(String imageId, String filePath) async { + // Updates tags and triggers relevant analysis + Future<void> updateTags(String imageId, List<String> newTags) async { + // 1. Update Tags in Database + await _repo.updateTags(imageId, newTags); + + // 2. Fetch Image path + final image = await _repo.getById(imageId); + if (image != null) { + // 3. Enqueue Analysis + _enqueueAnalysis("Analyze $imageId", () async { + await _analyzeInBackground(imageId, image.filePath, tags: newTags); + }); + } + } + + Future<void> _analyzeInBackground( + String imageId, + String filePath, { + List<String> tags = const [], + }) async { try { - debugPrint("[Background] Starting analysis for $imageId..."); - final result = await ImageAnalyzerService.analyzeFullSuite(filePath); + Map<String, dynamic>? result; + if (tags.isEmpty) { + result = await ImageAnalyzerService.analyzeFullSuite(filePath); + } else { + result = await ImageAnalyzerService.analyzeSelected(filePath, tags); + } + if (result != null) { - // Convert Map to JSON String final String jsonString = jsonEncode(result); - - // Update DB without user intervention await _repo.updateAnalysis(imageId, jsonString); - debugPrint("[Background] Analysis saved for $imageId"); } } catch (e) { - debugPrint("[Background] Analysis failed: $e"); + // Re-throw so the queue knows it failed + throw Exception("Analysis failed for $imageId: $e"); } } @@ -91,11 +165,6 @@ class ImageService { return await _repo.getTagsForImage(imageId); } - - Future<void> updateTags(String imageId, List<String> newTags) async { - await _repo.updateTags(imageId, newTags); - } - Future<void> deleteImage(String id) async { final img = await _repo.getById(id); if (img != null) { diff --git a/lib/ui/pages/image_analysis_page.dart b/lib/ui/pages/image_analysis_page.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:path_provider/path_provider.dart'; import 'package:adobe/services/analyze/image_analyzer.dart'; +import '../styles/variables.dart'; class ImageAnalysisPage extends StatefulWidget { const ImageAnalysisPage({super.key}); @@ -148,10 +149,10 @@ class _ImageAnalysisPageState extends State<ImageAnalysisPage> { Center( child: Container( width: double.infinity, - height: 300, + height: 250, decoration: BoxDecoration( color: Colors.grey[100], - borderRadius: BorderRadius.circular(24), + borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), @@ -161,7 +162,7 @@ class _ImageAnalysisPageState extends State<ImageAnalysisPage> { ], ), child: ClipRRect( - borderRadius: BorderRadius.circular(24), + borderRadius: BorderRadius.circular(16), child: Stack( fit: StackFit.expand, children: [ @@ -171,11 +172,11 @@ class _ImageAnalysisPageState extends State<ImageAnalysisPage> { Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.add_photo_alternate_outlined, + Icon(Icons.bug_report_outlined, size: 48, color: Colors.grey[400]), const SizedBox(height: 12), Text( - "Select an image to analyze", + "Select image to test full suite", style: TextStyle( fontFamily: 'GeneralSans', color: Colors.grey[500], @@ -196,6 +197,8 @@ class _ImageAnalysisPageState extends State<ImageAnalysisPage> { ), ), const SizedBox(height: 24), + + // Error Message if (_errorMessage != null) Container( width: double.infinity, @@ -210,31 +213,19 @@ class _ImageAnalysisPageState extends State<ImageAnalysisPage> { style: const TextStyle(color: Colors.red, fontFamily: 'GeneralSans'), ), ), + + // Raw JSON Result if (_analysisResult != null) ...[ const Text( "Results", style: TextStyle( - fontSize: 20, + fontSize: 18, fontWeight: FontWeight.bold, fontFamily: 'GeneralSans', ), ), - - const SizedBox(height: 16), - _buildFormattedResults(_analysisResult!), - const SizedBox(height: 32), - ExpansionTile( - title: const Text( - "View Raw JSON", - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - fontFamily: 'GeneralSans', - color: Colors.grey, - ), - ), - children: [_buildJsonViewer(_analysisResult!)], - ), + const SizedBox(height: 12), + _buildJsonViewer(_analysisResult!), ], const SizedBox(height: 80), ], @@ -244,105 +235,15 @@ class _ImageAnalysisPageState extends State<ImageAnalysisPage> { onPressed: _showSourceSelector, backgroundColor: colorScheme.primary, foregroundColor: colorScheme.onPrimary, - icon: const Icon(Icons.camera_alt), + icon: const Icon(Icons.add_photo_alternate), label: Text( - _selectedImage == null ? "Pick Image" : "Change Image", + _selectedImage == null ? "Select Image" : "Change Image", style: const TextStyle(fontFamily: 'GeneralSans'), ), ), ); } - Widget _buildFormattedResults(Map<String, dynamic> root) { - final data = root['data']; - if (data == null || data['results'] == null) { - return const Text("No detailed results found."); - } - final results = data['results'] as Map<String, dynamic>; - - return Column( - children: [ - _buildList("Style", results['Style']), - _buildList("Era", results['Era']), - _buildList("Emotions", results['Emotions']), - _buildList("Lighting", results['Lighting']), - _buildList("Layout Composition", results['Layout']), - _buildList("Color Palette", results['Colour Palette']), - _buildList("Texture", results['Texture']), - _buildList("Font", results['Font']), - ], - ); - } - - Widget _buildList(String title, dynamic categoryData) { - if (categoryData == null || categoryData['scores'] == null) { - return const SizedBox.shrink(); - } - - final scoresMap = categoryData['scores'] as Map<String, dynamic>; - - final sortedEntries = scoresMap.entries.toList() - ..sort((a, b) => (b.value as num).compareTo(a.value as num)); - - return Container( - margin: const EdgeInsets.only(bottom: 12), - width: double.infinity, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.grey.shade300), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.bold, - color: Colors.black87, - fontFamily: 'GeneralSans', - ), - ), - const SizedBox(height: 8), - const Divider(height: 1), - const SizedBox(height: 8), - ...sortedEntries.map((e) { - final val = e.value as num; - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - e.key, - style: const TextStyle( - fontSize: 13, - color: Colors.black87, - fontFamily: 'GeneralSans', - ), - ), - ), - Text( - val.toStringAsFixed(4), // High precision for debugging - style: TextStyle( - fontSize: 13, - fontFamily: Platform.isIOS ? 'Courier' : 'monospace', - color: Colors.grey[700], - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ); - }), - ], - ), - ); - } - Widget _buildJsonViewer(Map<String, dynamic> data) { const encoder = JsonEncoder.withIndent(' '); final String prettyJson = encoder.convert(data); @@ -351,16 +252,16 @@ class _ImageAnalysisPageState extends State<ImageAnalysisPage> { width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.grey[50], + color: Variables.borderSubtle, borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.grey[300]!), + border: Border.all(color: Colors.grey[800]!), ), child: SelectableText( prettyJson, style: TextStyle( fontFamily: Platform.isIOS ? 'Courier' : 'monospace', - fontSize: 11, - color: Colors.grey[800], + fontSize: 12, + color: Variables.textPrimary, height: 1.3, ), ), diff --git a/lib/ui/pages/image_details_page.dart b/lib/ui/pages/image_details_page.dart @@ -1,8 +1,6 @@ import 'dart:io'; -import 'dart:ui' as ui; +import 'dart:ui' as ui; import 'package:flutter/material.dart'; - -//services,models import '../../services/image_service.dart'; import '../../services/note_service.dart'; import '../../data/models/note_model.dart'; @@ -88,13 +86,6 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> { // --- ACTIONS --- - Future<void> _removeTag(String tag) async { - setState(() { - _currentTags.remove(tag); - }); - await _imageService.updateTags(widget.imageId, _currentTags); - } - void _activateDrawMode() { setState(() { _isDrawMode = true; @@ -692,66 +683,40 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> { runSpacing: 8, children: _currentTags.map((tag) { - // Style as "Selected" (Purple with X) - return GestureDetector( - onTap: - () => _removeTag( - tag, + return Container( + padding: + const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, ), - child: Container( - padding: - const EdgeInsets.symmetric( - horizontal: - 12, - vertical: 6, + decoration: BoxDecoration( + color: const Color( + 0xFFEEF0FF, + ), + borderRadius: + BorderRadius.circular( + 20, ), - decoration: BoxDecoration( + border: Border.all( color: const Color( - 0xFFEEF0FF, - ), - borderRadius: - BorderRadius.circular( - 20, + 0xFF7C4DFF, ), - border: Border.all( - color: - const Color( - 0xFF7C4DFF, - ), - width: 1, - ), + width: 1, ), - child: Row( - mainAxisSize: - MainAxisSize - .min, - children: [ - Text( - tag, - style: const TextStyle( - fontSize: - 13, - fontWeight: - FontWeight - .w500, - color: Color( - 0xFF7C4DFF, - ), - ), - ), - const SizedBox( - width: 4, - ), - const Icon( - Icons.close, - size: 14, + ), + child: Text( + tag, + style: + const TextStyle( + fontSize: 13, + fontWeight: + FontWeight + .w500, color: Color( 0xFF7C4DFF, ), ), - ], - ), ), ); }).toList(), diff --git a/lib/ui/pages/image_page.dart b/lib/ui/pages/image_page.dart @@ -1 +0,0 @@ -// BROKEN diff --git a/lib/ui/pages/image_save_page.dart b/lib/ui/pages/image_save_page.dart @@ -336,15 +336,14 @@ class _ImageSavePageState extends State<ImageSavePage> { if (!file.existsSync()) continue; // 1. Save Image - final imageId = await _imageService.saveImage(file, widget.projectId); - - // 2. Save Tags for this specific image final tags = _tagsPerImage[i] ?? {}; - if (tags.isNotEmpty) { - await _imageService.updateTags(imageId, tags.toList()); - } + final imageId = await _imageService.saveImage( + file, + widget.projectId, + tags: tags.toList(), + ); - // 3. Save Notes for this specific image + // 2. Save Notes for this specific image final notes = _notesPerImage[i] ?? []; for (var note in notes) { await _noteService.addNote( diff --git a/lib/ui/widgets/top_bar.dart b/lib/ui/widgets/top_bar.dart @@ -1,14 +1,14 @@ import 'package:flutter/material.dart'; -import '../../data/models/project_model.dart'; // Adjust path as needed -import '../../data/repos/project_repo.dart'; // Adjust path as needed -import '../styles/variables.dart'; // Adjust path as needed +import 'package:adobe/data/models/project_model.dart'; +import 'package:adobe/data/repos/project_repo.dart'; +import '../styles/variables.dart'; class TopBar extends StatefulWidget implements PreferredSizeWidget { final int currentProjectId; final VoidCallback? onBack; final Function(ProjectModel)? onProjectChanged; final VoidCallback? onSettingsPressed; - final String? titleOverride; // New: For Tag Page title + final String? titleOverride; const TopBar({ super.key, @@ -22,9 +22,8 @@ class TopBar extends StatefulWidget implements PreferredSizeWidget { @override State<TopBar> createState() => _TopBarState(); - // FIX: Reduced from 80 to 56 (Standard Toolbar Height) @override - Size get preferredSize => const Size.fromHeight(kToolbarHeight); + Size get preferredSize => const Size.fromHeight(kToolbarHeight); } class _TopBarState extends State<TopBar> { @@ -51,8 +50,12 @@ class _TopBarState extends State<TopBar> { Future<void> _loadData() async { try { - final current = await _projectRepo.getProjectById(widget.currentProjectId); - if (current == null) return; + final current = + await _projectRepo.getProjectById(widget.currentProjectId); + if (current == null) { + setState(() => _isLoading = false); + return; + } ProjectModel? root; List<ProjectModel> events = []; @@ -78,17 +81,19 @@ class _TopBarState extends State<TopBar> { } } catch (e) { debugPrint("TopBar Error: $e"); + setState(() => _isLoading = false); } } @override Widget build(BuildContext context) { + final textScaler = MediaQuery.of(context).textScaler; return Container( color: Variables.background, child: SafeArea( bottom: false, child: SizedBox( - height: kToolbarHeight, // FIX: Use standard height + height: kToolbarHeight, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( @@ -110,7 +115,7 @@ class _TopBarState extends State<TopBar> { ? Text( widget.titleOverride!, style: const TextStyle( - fontSize: 20, // Slightly smaller for tags + fontSize: 20, fontWeight: FontWeight.w600, color: Variables.textPrimary, ), @@ -123,7 +128,7 @@ class _TopBarState extends State<TopBar> { child: Text( _rootProject!.title, style: const TextStyle( - fontSize: 22, // Adjusted font size + fontSize: 22, fontWeight: FontWeight.w600, color: Variables.textPrimary, ), @@ -131,69 +136,79 @@ class _TopBarState extends State<TopBar> { ), ), const SizedBox(width: 8), - PopupMenuButton<ProjectModel>( - padding: EdgeInsets.zero, - onSelected: (project) { - widget.onProjectChanged?.call(project); - }, - color: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - offset: const Offset(0, 40), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), - decoration: BoxDecoration( - color: Variables.surfaceSubtle, - borderRadius: BorderRadius.circular(8), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - _currentProject!.id == _rootProject!.id - ? "Main" - : _currentProject!.title, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Variables.textPrimary, - ), - ), - const SizedBox(width: 4), - const Icon( - Icons.keyboard_arrow_down_rounded, - color: Variables.textPrimary, - size: 18, - ) - ], - ), - ), - itemBuilder: (context) { - return _contextList.map((ProjectModel project) { - final isRoot = project.id == _rootProject!.id; - final isSelected = project.id == _currentProject!.id; - return PopupMenuItem<ProjectModel>( - value: project, - child: Text( - isRoot ? "Main Project" : project.title, - style: Variables.bodyStyle.copyWith( - fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, - color: isSelected ? Variables.textPrimary : Variables.textSecondary, - ), - ), - ); - }).toList(); - }, - ), ], ) : const SizedBox(), ), + 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(16), + ), + offset: const Offset(0, 38), + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 14, + vertical: 10, + ), + decoration: BoxDecoration( + color: Variables.surfaceSubtle, + borderRadius: BorderRadius.circular(16), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + _currentProject!.id == _rootProject!.id + ? "Main" + : _currentProject!.title, + style: Variables.bodyStyle.copyWith( + fontSize: 15 * textScaler.scale(1.1), + fontWeight: FontWeight.w500, + color: Variables.textPrimary, + ), + ), + const SizedBox(width: 6), + const Icon( + Icons.keyboard_arrow_down_rounded, + size: 18, + 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 ? "Main Project" : project.title, + style: Variables.bodyStyle.copyWith( + fontWeight: isSelected + ? FontWeight.bold + : FontWeight.normal, + color: isSelected + ? Variables.textPrimary + : Variables.textSecondary, + ), + ), + ); + }).toList(); + }, + ), + const SizedBox(width: 12), // 3. Settings Icon IconButton( - icon: const Icon(Icons.settings_outlined, color: Variables.textPrimary), + icon: const Icon(Icons.settings_outlined, + color: Variables.textPrimary), onPressed: widget.onSettingsPressed, padding: EdgeInsets.zero, constraints: const BoxConstraints(), @@ -205,4 +220,4 @@ class _TopBarState extends State<TopBar> { ), ); } -} -\ No newline at end of file +}