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:
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
+}