commit c3ba6bac163a343e61a0d89d86b8af29d299c367
parent c1729cfeb806830f4dc75c683e2f4df110166a64
Author: ajcoder13 <avnijhalani@gmail.com>
Date: Sun, 30 Nov 2025 01:16:17 +0530
Made create(define) project page
Diffstat:
6 files changed, 853 insertions(+), 309 deletions(-)
diff --git a/assets/icons/painting-ai-line.svg b/assets/icons/painting-ai-line.svg
@@ -0,0 +1,3 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M3.52512 3.5254C4.89197 2.15872 7.10756 2.15861 8.47434 3.5254L8.64622 3.6963C8.84149 3.89145 9.15802 3.89152 9.35325 3.6963L9.52512 3.5254C10.8077 2.24294 12.8371 2.16412 14.2126 3.28809L12.947 4.83594C12.3587 4.35525 11.4883 4.39054 10.9392 4.93946L10.7673 5.11133C9.79111 6.08721 8.2084 6.08713 7.23216 5.11133L7.06028 4.93946C6.47455 4.35373 5.52499 4.35383 4.93919 4.93946C4.3534 5.52525 4.3534 6.47477 4.93919 7.06055L5.11106 7.23243C6.08681 8.20862 6.08678 9.79137 5.11106 10.7676L4.93919 10.9395C4.3534 11.5252 4.3534 12.4748 4.93919 13.0606L5.11106 13.2324C6.08681 14.2086 6.08678 15.7914 5.11106 16.7676L4.93919 16.9395C4.3534 17.5252 4.3534 18.4748 4.93919 19.0606C5.52498 19.6462 6.47453 19.6463 7.06028 19.0606L7.23216 18.8887C8.2084 17.9129 9.79113 17.9128 10.7673 18.8887L10.9392 19.0606C11.525 19.6462 12.4745 19.6463 13.0603 19.0606L13.2321 18.8887C14.2084 17.9129 15.7911 17.9128 16.7673 18.8887L16.9392 19.0606C17.525 19.6462 18.4745 19.6463 19.0603 19.0606C19.6459 18.4748 19.6459 17.5252 19.0603 16.9395L18.8884 16.7676C17.9125 15.7914 17.9125 14.2087 18.8884 13.2324L19.0603 13.0606C19.6092 12.5115 19.6444 11.641 19.1638 11.0527L20.7116 9.78712C21.8355 11.1626 21.7568 13.192 20.4743 14.4746L20.3034 14.6465C20.1082 14.8418 20.1082 15.1583 20.3034 15.3535L20.4743 15.5254C21.841 16.8922 21.841 19.1078 20.4743 20.4746C19.1075 21.8414 16.892 21.8413 15.5251 20.4746L15.3532 20.3037C15.158 20.1085 14.8415 20.1086 14.6462 20.3037L14.4743 20.4746C13.1075 21.8414 10.892 21.8413 9.52512 20.4746L9.35325 20.3037C9.15804 20.1085 8.84149 20.1086 8.64622 20.3037L8.47434 20.4746C7.10754 21.8414 4.89197 21.8413 3.52512 20.4746C2.15829 19.1078 2.15829 16.8922 3.52512 15.5254L3.69602 15.3535C3.89109 15.1583 3.89112 14.8417 3.69602 14.6465L3.52512 14.4746C2.15829 13.1078 2.15829 10.8922 3.52512 9.5254L3.69602 9.35352C3.89109 9.15828 3.89112 8.84171 3.69602 8.64649L3.52512 8.47462C2.15829 7.10778 2.15829 4.89223 3.52512 3.5254ZM14.4997 7.50001C15.6042 7.50015 16.4997 8.39555 16.4997 9.50001V14.5C16.4997 15.6045 15.6042 16.4999 14.4997 16.5H9.49973C8.39516 16.5 7.49973 15.6046 7.49973 14.5V9.50001C7.49976 8.39546 8.39518 7.50001 9.49973 7.50001H14.4997ZM9.49973 14.5H14.4997V9.50001H9.49973V14.5ZM18.529 1.31934C18.7057 0.893621 19.294 0.893485 19.4704 1.31934L19.7234 1.93067C20.1553 2.97354 20.9612 3.8062 21.9743 4.25684L22.6911 4.57618C23.1019 4.75887 23.1019 5.35638 22.6911 5.53907L21.9314 5.87696C20.9437 6.3163 20.1521 7.11944 19.7126 8.12794L19.4665 8.69337C19.2861 9.1075 18.7134 9.1075 18.5329 8.69337L18.2859 8.12794C17.8463 7.11943 17.0548 6.31625 16.0671 5.87696L15.3073 5.53907C14.8969 5.35629 14.8968 4.7589 15.3073 4.57618L16.0251 4.25684C17.0381 3.80616 17.8433 2.9734 18.2751 1.93067L18.529 1.31934Z" fill="#7C86FF"/>
+</svg>
diff --git a/lib/ui/pages/define_brand_page.dart b/lib/ui/pages/define_brand_page.dart
@@ -0,0 +1,458 @@
+import 'package:flutter/material.dart';
+import 'package:adobe/ui/styles/variables.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+// Import your service here
+import '../../services/project_service.dart';
+
+class DefineBrandPage extends StatefulWidget {
+ final String projectName;
+ final String? projectDescription;
+
+ const DefineBrandPage({
+ super.key,
+ this.projectDescription,
+ required this.projectName,
+ });
+ @override
+ State<DefineBrandPage> createState() => _DefineBrandPageState();
+}
+
+class _DefineBrandPageState extends State<DefineBrandPage> {
+ // Service
+ final ProjectService _projectService = ProjectService();
+
+ // Controllers
+ final _projectNameController = TextEditingController();
+ final _descriptionController = TextEditingController();
+ final _problemController = TextEditingController();
+ final _goalController = TextEditingController();
+ // controllers for quick add dialogs
+ final _whereWillAppearController = TextEditingController();
+
+ // State
+ bool _isLoading = false;
+ final List<String> _keywords = ['Colour', 'Fonts', 'Composition'];
+ final List<Map<String, String>> _competitorBrands = [
+ {'name': 'Mcdonalds', 'initial': 'M'},
+ {'name': 'The Good Folks', 'initial': 'G'},
+ ];
+
+ @override
+ void initState() {
+ super.initState();
+ // Pre-fill description if passed from previous page
+ if (widget.projectDescription != null) {
+ _descriptionController.text = widget.projectDescription!;
+ }
+ // Pre-fill project name if provided
+ _projectNameController.text = widget.projectName;
+ }
+
+ @override
+ void dispose() {
+ _projectNameController.dispose();
+ _descriptionController.dispose();
+ _problemController.dispose();
+ _goalController.dispose();
+ // audience controller removed; not used in this form
+ // no controller for keywords or competitors (we use chip lists)
+ _whereWillAppearController.dispose();
+ super.dispose();
+ }
+
+ Future<void> _handleFinish() async {
+ // 1. Basic Validation
+ if (_projectNameController.text.trim().isEmpty) {
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(const SnackBar(content: Text('Project name is missing.')));
+ return;
+ }
+
+ setState(() {
+ _isLoading = true;
+ });
+
+ try {
+ // 2. Prepare the data
+ // Note: Your createProject method needs to be updated to accept
+ // 'brandData' or these specific fields if you want to save them.
+ final brandData = {
+ 'description': _descriptionController.text.trim(),
+ 'problem': _problemController.text.trim(),
+ 'goal': _goalController.text.trim(),
+ 'keywords': _keywords,
+ 'competitors': _competitorBrands.map((b) => b['name']).toList(),
+ 'appear': _whereWillAppearController.text.trim(),
+ };
+
+ // 3. Call the Service
+ // Debug: log brandData while backend integration is pending
+ debugPrint('BrandData: $brandData');
+ await _projectService.createProject(
+ _projectNameController.text.trim(),
+ description:
+ _descriptionController.text.trim().isEmpty
+ ? null
+ : _descriptionController.text.trim(),
+ // TODO: Pass brandData here if your service supports it
+ // brandData: brandData,
+ );
+
+ // 4. Success Handling
+ if (mounted) {
+ // Pop with 'true' to indicate success so previous page can reload if needed
+ Navigator.pop(context, true);
+ }
+ } catch (e) {
+ // 5. Error Handling
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text('Error creating project: $e'),
+ backgroundColor: Colors.red,
+ ),
+ );
+ }
+ } finally {
+ if (mounted) {
+ setState(() {
+ _isLoading = false;
+ });
+ }
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: Variables.background,
+ appBar: AppBar(
+ backgroundColor: Variables.background,
+ elevation: 0,
+ leading: IconButton(
+ icon: const Icon(Icons.arrow_back, color: Variables.textPrimary),
+ onPressed: () => Navigator.pop(context),
+ ),
+ title: const SizedBox.shrink(),
+ centerTitle: false,
+ // no actions (Skip removed)
+ ),
+ body: SafeArea(
+ child: Column(
+ children: [
+ Expanded(
+ child: SingleChildScrollView(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const SizedBox(height: 8),
+ // Header area with icon, title and subtitle
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Container(
+ width: 44,
+ height: 44,
+ decoration: BoxDecoration(
+ color: const Color(0xFFEFF6FF),
+ borderRadius: BorderRadius.circular(44),
+ ),
+ child: Center(
+ child: SvgPicture.asset(
+ 'assets/icons/painting-ai-line.svg',
+ width: 22,
+ height: 22,
+ color: Variables.textPrimary,
+ ),
+ ),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: const [
+ Text(
+ 'Define Your Brand',
+ style: TextStyle(
+ fontFamily: 'GeneralSans',
+ fontSize: 20,
+ fontWeight: FontWeight.w500,
+ color: Variables.textPrimary,
+ height: 24 / 20,
+ ),
+ ),
+ SizedBox(height: 6),
+ Text(
+ 'Answer a few quick questions to help us craft your unique style guide.',
+ style: TextStyle(
+ fontFamily: 'GeneralSans',
+ fontSize: 14,
+ fontWeight: FontWeight.w400,
+ color: Variables.textSecondary,
+ height: 18 / 14,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 20),
+ _buildSectionHeader("Project Name*"),
+ const SizedBox(height: 8),
+ _buildTextField(
+ controller: _projectNameController,
+ hintText: 'Project Name',
+ ),
+ const SizedBox(height: 18),
+ _buildSectionHeader("What do you want & who is it for"),
+ const SizedBox(height: 8),
+ _buildTextField(
+ controller: _descriptionController,
+ hintText: "Describe your work and your audience.",
+ maxLines: 4,
+ ),
+ const SizedBox(height: 18),
+
+ _buildSectionHeader("What problem you solve"),
+ const SizedBox(height: 8),
+ _buildTextField(
+ controller: _problemController,
+ hintText: "Explain the main issue your brand addresses.",
+ maxLines: 3,
+ ),
+ const SizedBox(height: 18),
+
+ _buildSectionHeader("Long-term goal for the brand"),
+ const SizedBox(height: 8),
+ _buildTextField(
+ controller: _goalController,
+ hintText: "E.g - Improving food availability...",
+ ),
+ const SizedBox(height: 18),
+
+ _buildSectionHeader("2-3 vibe keywords*"),
+ const SizedBox(height: 8),
+ Wrap(
+ spacing: 8,
+ runSpacing: 8,
+ children: [
+ for (final k in _keywords)
+ InputChip(
+ label: Text(k),
+ onDeleted:
+ () => setState(() => _keywords.remove(k)),
+ ),
+ ActionChip(
+ label: const Text('Add More +'),
+ onPressed: _showAddKeywordDialog,
+ ),
+ ],
+ ),
+ const SizedBox(height: 18),
+
+ _buildSectionHeader("2-3 reference/competitor brands*"),
+ const SizedBox(height: 8),
+ SizedBox(
+ height: 56,
+ child: ListView.separated(
+ scrollDirection: Axis.horizontal,
+ padding: EdgeInsets.zero,
+ itemBuilder: (context, index) {
+ final b = _competitorBrands[index];
+ return Chip(
+ label: Text(b['name'] ?? ''),
+ avatar: CircleAvatar(
+ child: Text(b['initial'] ?? ''),
+ ),
+ onDeleted:
+ () => setState(
+ () => _competitorBrands.removeAt(index),
+ ),
+ );
+ },
+ separatorBuilder: (_, __) => const SizedBox(width: 8),
+ itemCount: _competitorBrands.length,
+ ),
+ ),
+ const SizedBox(height: 10),
+ TextButton(
+ onPressed: _showAddCompetitorDialog,
+ child: const Text('Add more +'),
+ ),
+ const SizedBox(height: 18),
+
+ _buildSectionHeader("Where will the brand appear"),
+ const SizedBox(height: 8),
+ _buildTextField(
+ controller: _whereWillAppearController,
+ hintText: "Banners, Posters, Instagram...",
+ ),
+ ],
+ ),
+ ),
+ ),
+
+ // Bottom Button
+ Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: SizedBox(
+ width: double.infinity,
+ child: ElevatedButton(
+ onPressed: _isLoading ? null : _handleFinish,
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Variables.textPrimary, // Black
+ foregroundColor: Variables.textWhite, // White text
+ padding: const EdgeInsets.symmetric(vertical: 16),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ elevation: 0,
+ ),
+ child:
+ _isLoading
+ ? const SizedBox(
+ height: 20,
+ width: 20,
+ child: CircularProgressIndicator(
+ color: Colors.white,
+ strokeWidth: 2,
+ ),
+ )
+ : Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text(
+ "Create Project",
+ style: Variables.buttonTextStyle.copyWith(
+ fontSize: 16,
+ ),
+ ),
+ const SizedBox(width: 8),
+ SvgPicture.asset(
+ 'assets/icons/generate_icon.svg',
+ width: 18,
+ height: 18,
+ color: Variables.textWhite,
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildSectionHeader(String title) {
+ return Text(
+ title,
+ style: Variables.bodyStyle.copyWith(
+ fontWeight: FontWeight.w600,
+ fontSize: 14,
+ color: Variables.textPrimary,
+ ),
+ );
+ }
+
+ Widget _buildTextField({
+ required TextEditingController controller,
+ required String hintText,
+ int maxLines = 1,
+ }) {
+ return Container(
+ decoration: BoxDecoration(
+ color: const Color(0xFFFAFAFA), // Light grey background
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(color: Variables.borderSubtle),
+ ),
+ child: TextField(
+ controller: controller,
+ maxLines: maxLines,
+ style: Variables.bodyStyle,
+ decoration: InputDecoration(
+ hintText: hintText,
+ hintStyle: Variables.bodyStyle.copyWith(
+ color: Variables.textDisabled,
+ ),
+ border: InputBorder.none,
+ contentPadding: const EdgeInsets.all(16),
+ ),
+ ),
+ );
+ }
+
+ void _showAddKeywordDialog() {
+ final controller = TextEditingController();
+ showDialog<void>(
+ context: context,
+ builder:
+ (context) => AlertDialog(
+ title: const Text('Add Keyword'),
+ content: TextField(
+ controller: controller,
+ autofocus: true,
+ decoration: const InputDecoration(hintText: 'e.g., Minimal'),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Cancel'),
+ ),
+ ElevatedButton(
+ onPressed: () {
+ final val = controller.text.trim();
+ if (val.isNotEmpty && !_keywords.contains(val)) {
+ setState(() => _keywords.add(val));
+ }
+ Navigator.pop(context);
+ },
+ child: const Text('Add'),
+ ),
+ ],
+ ),
+ );
+ }
+
+ void _showAddCompetitorDialog() {
+ final controller = TextEditingController();
+ showDialog<void>(
+ context: context,
+ builder:
+ (context) => AlertDialog(
+ title: const Text('Add Competitor'),
+ content: TextField(
+ controller: controller,
+ autofocus: true,
+ decoration: const InputDecoration(hintText: 'Brand name'),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Cancel'),
+ ),
+ ElevatedButton(
+ onPressed: () {
+ final val = controller.text.trim();
+ if (val.isNotEmpty) {
+ final initial = val.isNotEmpty ? val[0].toUpperCase() : '';
+ setState(
+ () => _competitorBrands.add({
+ 'name': val,
+ 'initial': initial,
+ }),
+ );
+ }
+ Navigator.pop(context);
+ },
+ child: const Text('Add'),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/ui/pages/home_page.dart b/lib/ui/pages/home_page.dart
@@ -6,6 +6,7 @@ import 'package:adobe/data/repos/image_repo.dart';
import 'package:adobe/services/project_service.dart';
import 'project_detail_page.dart';
import 'image_analysis_page.dart';
+import 'define_brand_page.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@@ -66,89 +67,19 @@ class _HomePageState extends State<HomePage> {
}
}
- Future<void> _createProjectDialog() async {
- final nameController = TextEditingController();
- final descriptionController = TextEditingController();
-
- if (!mounted) return;
-
- await showDialog(
- context: context,
- builder:
- (context) => AlertDialog(
- title: const Text(
- "Create New Project",
- style: TextStyle(
- fontFamily: 'GeneralSans',
- fontWeight: FontWeight.w600,
- ),
- ),
- content: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- TextField(
- controller: nameController,
- decoration: const InputDecoration(
- hintText: "Project Name",
- labelText: "Name",
- border: OutlineInputBorder(),
- ),
- autofocus: true,
- style: const TextStyle(fontFamily: 'GeneralSans'),
- ),
- const SizedBox(height: 16),
- TextField(
- controller: descriptionController,
- decoration: const InputDecoration(
- hintText: "Description (optional)",
- labelText: "Description",
- border: OutlineInputBorder(),
- ),
- maxLines: 3,
- style: const TextStyle(fontFamily: 'GeneralSans'),
- ),
- ],
+ Future<void> _createNewProject() async {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder:
+ (_) => const DefineBrandPage(
+ projectName: "",
+ projectDescription: null,
),
- actions: [
- TextButton(
- onPressed: () => Navigator.pop(context),
- child: const Text(
- "Cancel",
- style: TextStyle(fontFamily: 'GeneralSans'),
- ),
- ),
- ElevatedButton(
- onPressed: () async {
- if (nameController.text.trim().isNotEmpty) {
- try {
- await _projectService.createProject(
- nameController.text.trim(),
- description:
- descriptionController.text.trim().isEmpty
- ? null
- : descriptionController.text.trim(),
- );
- if (context.mounted) {
- Navigator.pop(context);
- _loadData();
- }
- } catch (e) {
- if (context.mounted) {
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text('Error creating project: $e')),
- );
- }
- }
- }
- },
- child: const Text(
- "Create",
- style: TextStyle(fontFamily: 'GeneralSans'),
- ),
- ),
- ],
- ),
- );
+ ),
+ ).then((result) {
+ _loadData();
+ });
}
void _openProject(ProjectModel project) {
@@ -357,7 +288,7 @@ class _HomePageState extends State<HomePage> {
),
),
floatingActionButton: FloatingActionButton(
- onPressed: _createProjectDialog,
+ onPressed: _createNewProject,
backgroundColor: theme.colorScheme.primary,
foregroundColor: theme.colorScheme.onPrimary,
child: const Icon(Icons.add),
diff --git a/lib/ui/pages/project_board_page.dart b/lib/ui/pages/project_board_page.dart
@@ -3,10 +3,11 @@ import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:adobe/ui/styles/variables.dart';
import 'package:adobe/ui/widgets/top_bar.dart';
-import 'package:adobe/ui/widgets/bottom_bar.dart';
+import 'package:adobe/ui/widgets/bottom_bar.dart';
import '../../data/models/project_model.dart';
import '../../data/models/image_model.dart';
import '../../data/repos/project_repo.dart';
+import '../../data/repos/note_repo.dart';
import '../../data/repos/image_repo.dart';
import 'project_tag_page.dart';
import 'project_board_page_alternate.dart';
@@ -25,9 +26,11 @@ class ProjectBoardPage extends StatefulWidget {
class _ProjectBoardPageState extends State<ProjectBoardPage> {
final _projectRepo = ProjectRepo();
final _imageRepo = ImageRepo();
+ final _noteRepo = NoteRepo();
final ImagePicker _picker = ImagePicker();
- final GlobalKey<ProjectBoardPageAlternateState> _alternatePageKey = GlobalKey();
+ final GlobalKey<ProjectBoardPageAlternateState> _alternatePageKey =
+ GlobalKey();
ProjectModel? _currentProject;
Map<String, List<ImageModel>> _categorizedImages = {};
@@ -61,31 +64,59 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> {
if (_currentProject?.id == null) return;
if (!_showAlternateView) {
setState(() => _isLoading = true);
- final images = await _imageRepo.getImages(_currentProject!.id!);
- _categorizeImages(images);
- if (mounted) setState(() => _isLoading = false);
- }
- }
- void _categorizeImages(List<ImageModel> images) {
- _categorizedImages.clear();
- for (var img in images) {
- if (img.tags.isEmpty) {
- if (!_categorizedImages.containsKey('Uncategorized')) {
- _categorizedImages['Uncategorized'] = [];
- }
- _categorizedImages['Uncategorized']!.add(img);
- } else {
- for (var tag in img.tags) {
- if (!_categorizedImages.containsKey(tag)) {
- _categorizedImages[tag] = [];
- }
- _categorizedImages[tag]!.add(img);
- }
+ try {
+ final images = await _imageRepo.getImages(_currentProject!.id!);
+ final Map<String, List<ImageModel>> tempMap = {};
+ await Future.wait(
+ images.map((img) async {
+ final Set<String> distinctCategories = {...img.tags};
+ final notes = await _noteRepo.getNotesForImage(img.id);
+ for (var note in notes) {
+ if (note.category.isNotEmpty) {
+ distinctCategories.add(note.category);
+ }
+ }
+ if (distinctCategories.isEmpty) {
+ tempMap.putIfAbsent('Uncategorized', () => []).add(img);
+ } else {
+ for (var category in distinctCategories) {
+ final list = tempMap.putIfAbsent(category, () => []);
+ if (!list.any((i) => i.id == img.id)) {
+ list.add(img);
+ }
+ }
+ }
+ }),
+ );
+
+ _categorizedImages = tempMap;
+ } catch (e) {
+ debugPrint("Error loading images and note categories: $e");
}
+ if (mounted) setState(() => _isLoading = false);
}
}
+ // void _categorizeImages(List<ImageModel> images) {
+ // _categorizedImages.clear();
+ // for (var img in images) {
+ // if (img.tags.isEmpty) {
+ // if (!_categorizedImages.containsKey('Uncategorized')) {
+ // _categorizedImages['Uncategorized'] = [];
+ // }
+ // _categorizedImages['Uncategorized']!.add(img);
+ // } else {
+ // for (var tag in img.tags) {
+ // if (!_categorizedImages.containsKey(tag)) {
+ // _categorizedImages[tag] = [];
+ // }
+ // _categorizedImages[tag]!.add(img);
+ // }
+ // }
+ // }
+ // }
+
void _onProjectChanged(ProjectModel newProject) {
if (newProject.id != _currentProject?.id) {
setState(() {
@@ -95,7 +126,7 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> {
_loadImagesForSelected();
} else {
WidgetsBinding.instance.addPostFrameCallback((_) {
- _alternatePageKey.currentState?.refreshData();
+ _alternatePageKey.currentState?.refreshData();
});
}
}
@@ -104,19 +135,20 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> {
Future<void> _pickAndRedirect() async {
try {
final List<XFile> pickedFiles = await _picker.pickMultiImage();
-
+
if (pickedFiles.isNotEmpty && _currentProject != null) {
if (!mounted) return;
-
+
Navigator.push(
context,
MaterialPageRoute(
- builder: (_) => ImageSavePage(
- imagePaths: pickedFiles.map((e) => e.path).toList(),
- projectId: _currentProject!.id!,
- projectName: _currentProject!.title,
- isFromShare: false,
- ),
+ builder:
+ (_) => ImageSavePage(
+ imagePaths: pickedFiles.map((e) => e.path).toList(),
+ projectId: _currentProject!.id!,
+ projectName: _currentProject!.title,
+ isFromShare: false,
+ ),
),
).then((_) {
if (!_showAlternateView) {
@@ -161,7 +193,10 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> {
body: Column(
children: [
Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16.0,
+ vertical: 12.0,
+ ),
child: Row(
children: [
InkWell(
@@ -173,7 +208,10 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> {
},
borderRadius: BorderRadius.circular(20),
child: Container(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 8,
+ ),
decoration: BoxDecoration(
color: Variables.surfaceSubtle,
borderRadius: BorderRadius.circular(20),
@@ -183,7 +221,9 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> {
mainAxisSize: MainAxisSize.min,
children: [
Icon(
- _showAlternateView ? Icons.dashboard : Icons.view_agenda_outlined,
+ _showAlternateView
+ ? Icons.dashboard
+ : Icons.view_agenda_outlined,
size: 18,
color: Variables.textPrimary,
),
@@ -207,7 +247,10 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> {
},
borderRadius: BorderRadius.circular(20),
child: Container(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 8,
+ ),
decoration: BoxDecoration(
color: Variables.surfaceSubtle,
borderRadius: BorderRadius.circular(20),
@@ -237,12 +280,13 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> {
),
),
Expanded(
- child: _showAlternateView
- ? ProjectBoardPageAlternate(
- key: _alternatePageKey,
- projectId: _currentProject!.id!,
- )
- : _buildCategorizedView(),
+ child:
+ _showAlternateView
+ ? ProjectBoardPageAlternate(
+ key: _alternatePageKey,
+ projectId: _currentProject!.id!,
+ )
+ : _buildCategorizedView(),
),
],
),
@@ -270,7 +314,7 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> {
return Container(
margin: const EdgeInsets.only(bottom: 16),
// Keep this clipping so the scrolling list cuts off cleanly at the border
- clipBehavior: Clip.antiAlias,
+ clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Variables.borderSubtle),
@@ -285,10 +329,11 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> {
Navigator.push(
context,
MaterialPageRoute(
- builder: (_) => ProjectTagPage(
- projectId: _currentProject!.id!,
- tag: category,
- ),
+ builder:
+ (_) => ProjectTagPage(
+ projectId: _currentProject!.id!,
+ tag: category,
+ ),
),
);
}
@@ -330,11 +375,12 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> {
Navigator.push(
context,
MaterialPageRoute(
- builder: (_) => ImageDetailsPage(
- imagePath: image.filePath,
- imageId: image.id,
- projectId: _currentProject!.id!,
- ),
+ builder:
+ (_) => ImageDetailsPage(
+ imagePath: image.filePath,
+ imageId: image.id,
+ projectId: _currentProject!.id!,
+ ),
),
).then((_) => _loadImagesForSelected());
}
@@ -355,10 +401,14 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> {
Image.file(
File(image.filePath),
fit: BoxFit.cover,
- errorBuilder: (_, __, ___) => Container(
- color: Variables.surfaceSubtle,
- child: const Icon(Icons.broken_image, color: Variables.textDisabled),
- ),
+ errorBuilder:
+ (_, __, ___) => Container(
+ color: Variables.surfaceSubtle,
+ child: const Icon(
+ Icons.broken_image,
+ color: Variables.textDisabled,
+ ),
+ ),
),
],
),
@@ -374,4 +424,4 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> {
},
);
}
-}
-\ No newline at end of file
+}
diff --git a/lib/ui/pages/project_board_page_alternate.dart b/lib/ui/pages/project_board_page_alternate.dart
@@ -11,15 +11,16 @@ class ProjectBoardPageAlternate extends StatefulWidget {
const ProjectBoardPageAlternate({super.key, required this.projectId});
@override
- State<ProjectBoardPageAlternate> createState() => ProjectBoardPageAlternateState();
+ State<ProjectBoardPageAlternate> createState() =>
+ ProjectBoardPageAlternateState();
}
class ProjectBoardPageAlternateState extends State<ProjectBoardPageAlternate> {
final _imageRepo = ImageRepo();
-
+
List<ImageModel> _allImages = [];
List<ImageModel> _filteredImages = [];
-
+
List<String> _allTags = [];
Set<String> _selectedTags = {};
bool _isLoading = true;
@@ -46,7 +47,7 @@ class ProjectBoardPageAlternateState extends State<ProjectBoardPageAlternate> {
Future<void> _loadData() async {
setState(() => _isLoading = true);
final images = await _imageRepo.getImages(widget.projectId);
-
+
final Set<String> tags = {};
for (var img in images) {
tags.addAll(img.tags);
@@ -68,9 +69,10 @@ class ProjectBoardPageAlternateState extends State<ProjectBoardPageAlternate> {
setState(() => _filteredImages = _allImages);
} else {
setState(() {
- _filteredImages = _allImages.where((img) {
- return img.tags.toSet().intersection(_selectedTags).isNotEmpty;
- }).toList();
+ _filteredImages =
+ _allImages.where((img) {
+ return img.tags.toSet().intersection(_selectedTags).isNotEmpty;
+ }).toList();
});
}
}
@@ -103,49 +105,53 @@ class ProjectBoardPageAlternateState extends State<ProjectBoardPageAlternate> {
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
- )
+ ),
],
),
const SizedBox(height: 20),
if (_allTags.isEmpty)
Text("No tags available.", style: Variables.bodyStyle),
-
+
Expanded(
child: SingleChildScrollView(
child: Wrap(
spacing: 10,
runSpacing: 10,
- children: _allTags.map((tag) {
- final isSelected = _selectedTags.contains(tag);
- return FilterChip(
- label: Text(tag.toUpperCase()),
- selected: isSelected,
- onSelected: (selected) {
- setModalState(() {
- if (selected) {
- _selectedTags.add(tag);
- } else {
- _selectedTags.remove(tag);
- }
- });
- // Update main state
- this.setState(() {
- _applyFilter();
- });
- },
- labelStyle: Variables.captionStyle.copyWith(
- color: isSelected ? Colors.white : Variables.textPrimary,
- fontWeight: FontWeight.w600,
- ),
- backgroundColor: Variables.surfaceSubtle,
- selectedColor: Variables.textPrimary,
- checkmarkColor: Colors.white,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(20),
- side: BorderSide.none,
- ),
- );
- }).toList(),
+ children:
+ _allTags.map((tag) {
+ final isSelected = _selectedTags.contains(tag);
+ return FilterChip(
+ label: Text(tag.toUpperCase()),
+ selected: isSelected,
+ onSelected: (selected) {
+ setModalState(() {
+ if (selected) {
+ _selectedTags.add(tag);
+ } else {
+ _selectedTags.remove(tag);
+ }
+ });
+ // Update main state
+ this.setState(() {
+ _applyFilter();
+ });
+ },
+ labelStyle: Variables.captionStyle.copyWith(
+ color:
+ isSelected
+ ? Colors.white
+ : Variables.textPrimary,
+ fontWeight: FontWeight.w600,
+ ),
+ backgroundColor: Variables.surfaceSubtle,
+ selectedColor: Variables.textPrimary,
+ checkmarkColor: Colors.white,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(20),
+ side: BorderSide.none,
+ ),
+ );
+ }).toList(),
),
),
),
@@ -161,7 +167,10 @@ class ProjectBoardPageAlternateState extends State<ProjectBoardPageAlternate> {
});
Navigator.pop(context);
},
- child: const Text("Clear All", style: TextStyle(color: Colors.red)),
+ child: const Text(
+ "Clear All",
+ style: TextStyle(color: Colors.red),
+ ),
),
),
Expanded(
@@ -170,7 +179,9 @@ class ProjectBoardPageAlternateState extends State<ProjectBoardPageAlternate> {
style: ElevatedButton.styleFrom(
backgroundColor: Variables.textPrimary,
foregroundColor: Colors.white,
- shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
),
child: const Text("Done"),
),
@@ -180,7 +191,7 @@ class ProjectBoardPageAlternateState extends State<ProjectBoardPageAlternate> {
],
),
);
- }
+ },
);
},
);
@@ -220,44 +231,82 @@ class ProjectBoardPageAlternateState extends State<ProjectBoardPageAlternate> {
const Spacer(),
if (_selectedTags.isNotEmpty)
Container(
- padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 8,
+ vertical: 4,
+ ),
decoration: BoxDecoration(
color: Variables.textPrimary,
borderRadius: BorderRadius.circular(12),
),
child: Text(
"${_selectedTags.length} Active",
- style: Variables.captionStyle.copyWith(color: Colors.white, fontWeight: FontWeight.bold),
+ style: Variables.captionStyle.copyWith(
+ color: Colors.white,
+ fontWeight: FontWeight.bold,
+ ),
),
- )
+ ),
],
),
),
-
+
// Masonry Grid
Expanded(
- child: _filteredImages.isEmpty
- ? Center(
- child: Text(
- "No images found",
- style: Variables.bodyStyle.copyWith(color: Variables.textSecondary),
- ),
- )
- : SingleChildScrollView(
- padding: const EdgeInsets.fromLTRB(16, 0, 16, 80), // Bottom padding for FAB
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Expanded(
- child: Column(children: leftColumn.map((e) => Padding(padding: const EdgeInsets.only(bottom: 12), child: e)).toList()),
+ child:
+ _filteredImages.isEmpty
+ ? Center(
+ child: Text(
+ "No images found",
+ style: Variables.bodyStyle.copyWith(
+ color: Variables.textSecondary,
+ ),
),
- const SizedBox(width: 12),
- Expanded(
- child: Column(children: rightColumn.map((e) => Padding(padding: const EdgeInsets.only(bottom: 12), child: e)).toList()),
+ )
+ : SingleChildScrollView(
+ padding: const EdgeInsets.fromLTRB(
+ 16,
+ 0,
+ 16,
+ 80,
+ ), // Bottom padding for FAB
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Expanded(
+ child: Column(
+ children:
+ leftColumn
+ .map(
+ (e) => Padding(
+ padding: const EdgeInsets.only(
+ bottom: 12,
+ ),
+ child: e,
+ ),
+ )
+ .toList(),
+ ),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ children:
+ rightColumn
+ .map(
+ (e) => Padding(
+ padding: const EdgeInsets.only(
+ bottom: 12,
+ ),
+ child: e,
+ ),
+ )
+ .toList(),
+ ),
+ ),
+ ],
),
- ],
- ),
- ),
+ ),
),
],
);
@@ -269,16 +318,17 @@ class ProjectBoardPageAlternateState extends State<ProjectBoardPageAlternate> {
Navigator.push(
context,
MaterialPageRoute(
- builder: (_) => ImageDetailsPage(
- imagePath: image.filePath,
- imageId: image.id,
- projectId: widget.projectId,
- ),
+ builder:
+ (_) => ImageDetailsPage(
+ imagePath: image.filePath,
+ imageId: image.id,
+ projectId: widget.projectId,
+ ),
),
);
},
child: Container(
- height: (index % 3 == 0) ? 240 : 180,
+ height: (index % 3 == 0) ? 240 : 180,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: Variables.surfaceSubtle,
@@ -291,43 +341,59 @@ class ProjectBoardPageAlternateState extends State<ProjectBoardPageAlternate> {
File(image.filePath),
fit: BoxFit.cover,
width: double.infinity,
- errorBuilder: (_,__,___) => const Center(
- child: Icon(Icons.broken_image, color: Variables.textDisabled),
- ),
+ errorBuilder:
+ (_, __, ___) => const Center(
+ child: Icon(
+ Icons.broken_image,
+ color: Variables.textDisabled,
+ ),
+ ),
),
if (image.tags.isNotEmpty)
Positioned(
- bottom: 0, left: 0, right: 0,
+ bottom: 0,
+ left: 0,
+ right: 0,
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
- colors: [Colors.black.withOpacity(0.8), Colors.transparent],
+ colors: [
+ Colors.black.withOpacity(0.8),
+ Colors.transparent,
+ ],
),
),
child: Wrap(
- spacing: 4, runSpacing: 4,
- children: image.tags.take(3).map((tag) {
- return Container(
- padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
- decoration: BoxDecoration(
- color: Colors.white.withOpacity(0.2),
- borderRadius: BorderRadius.circular(4),
- border: Border.all(color: Colors.white.withOpacity(0.1)),
- ),
- child: Text(
- tag.toUpperCase(),
- style: const TextStyle(
- color: Colors.white,
- fontSize: 9,
- fontFamily: 'GeneralSans',
- fontWeight: FontWeight.w600
- ),
- ),
- );
- }).toList(),
+ spacing: 4,
+ runSpacing: 4,
+ children:
+ image.tags.take(3).map((tag) {
+ return Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 6,
+ vertical: 2,
+ ),
+ decoration: BoxDecoration(
+ color: Colors.white.withOpacity(0.2),
+ borderRadius: BorderRadius.circular(4),
+ border: Border.all(
+ color: Colors.white.withOpacity(0.1),
+ ),
+ ),
+ child: Text(
+ tag.toUpperCase(),
+ style: const TextStyle(
+ color: Colors.white,
+ fontSize: 9,
+ fontFamily: 'GeneralSans',
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ );
+ }).toList(),
),
),
),
@@ -336,4 +402,4 @@ class ProjectBoardPageAlternateState extends State<ProjectBoardPageAlternate> {
),
);
}
-}
-\ No newline at end of file
+}
diff --git a/lib/ui/pages/project_tag_page.dart b/lib/ui/pages/project_tag_page.dart
@@ -7,6 +7,7 @@ import 'package:adobe/ui/widgets/bottom_bar.dart';
import '../../data/models/image_model.dart';
import '../../data/repos/image_repo.dart';
import '../../data/repos/project_repo.dart';
+import '../../data/repos/note_repo.dart';
import 'image_save_page.dart';
import 'image_details_page.dart';
@@ -14,11 +15,7 @@ class ProjectTagPage extends StatefulWidget {
final int projectId;
final String tag;
- const ProjectTagPage({
- super.key,
- required this.projectId,
- required this.tag,
- });
+ const ProjectTagPage({super.key, required this.projectId, required this.tag});
@override
State<ProjectTagPage> createState() => _ProjectTagPageState();
@@ -27,8 +24,9 @@ class ProjectTagPage extends StatefulWidget {
class _ProjectTagPageState extends State<ProjectTagPage> {
final _imageRepo = ImageRepo();
final _projectRepo = ProjectRepo();
+ final _noteRepo = NoteRepo();
final ImagePicker _picker = ImagePicker();
-
+
List<ImageModel> _images = [];
String _projectName = "Project";
bool _isLoading = true;
@@ -44,12 +42,35 @@ class _ProjectTagPageState extends State<ProjectTagPage> {
if (project != null) _projectName = project.title;
final allImages = await _imageRepo.getImages(widget.projectId);
- final filtered = allImages.where((img) {
- if (widget.tag == 'Uncategorized') {
- return img.tags.isEmpty;
- }
- return img.tags.contains(widget.tag);
- }).toList();
+ final List<ImageModel> filtered = [];
+
+ // Process logic in parallel for speed
+ await Future.wait(
+ allImages.map((img) async {
+ bool matches = false;
+
+ if (widget.tag == 'Uncategorized') {
+ if (img.tags.isEmpty) {
+ final notes = await _noteRepo.getNotesForImage(img.id);
+ final hasCategorizedNote = notes.any((n) => n.category.isNotEmpty);
+ if (!hasCategorizedNote) matches = true;
+ }
+ } else {
+ if (img.tags.contains(widget.tag)) {
+ matches = true;
+ } else {
+ final notes = await _noteRepo.getNotesForImage(img.id);
+ if (notes.any((n) => n.category == widget.tag)) {
+ matches = true;
+ }
+ }
+ }
+
+ if (matches) {
+ filtered.add(img);
+ }
+ }),
+ );
if (mounted) {
setState(() {
@@ -67,12 +88,13 @@ class _ProjectTagPageState extends State<ProjectTagPage> {
Navigator.push(
context,
MaterialPageRoute(
- builder: (_) => ImageSavePage(
- imagePaths: pickedFiles.map((e) => e.path).toList(),
- projectId: widget.projectId,
- projectName: _projectName,
- isFromShare: false,
- ),
+ builder:
+ (_) => ImageSavePage(
+ imagePaths: pickedFiles.map((e) => e.path).toList(),
+ projectId: widget.projectId,
+ projectName: _projectName,
+ isFromShare: false,
+ ),
),
).then((_) => _loadData());
}
@@ -98,7 +120,7 @@ class _ProjectTagPageState extends State<ProjectTagPage> {
return Scaffold(
backgroundColor: Variables.background,
-
+
appBar: TopBar(
currentProjectId: widget.projectId,
titleOverride: widget.tag.toUpperCase(),
@@ -116,53 +138,65 @@ class _ProjectTagPageState extends State<ProjectTagPage> {
foregroundColor: Variables.background,
child: const Icon(Icons.add_photo_alternate_outlined),
),
-
- body: _isLoading
- ? const Center(child: CircularProgressIndicator())
- : _images.isEmpty
+
+ body:
+ _isLoading
+ ? const Center(child: CircularProgressIndicator())
+ : _images.isEmpty
? Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- const Icon(Icons.image_not_supported_outlined,
- size: 64, color: Variables.textDisabled),
- const SizedBox(height: 16),
- Text(
- "No images found for '${widget.tag}'",
- style: Variables.bodyStyle.copyWith(color: Variables.textSecondary),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Icon(
+ Icons.image_not_supported_outlined,
+ size: 64,
+ color: Variables.textDisabled,
+ ),
+ const SizedBox(height: 16),
+ Text(
+ "No images found for '${widget.tag}'",
+ style: Variables.bodyStyle.copyWith(
+ color: Variables.textSecondary,
),
- ],
- ),
- )
+ ),
+ ],
+ ),
+ )
: SingleChildScrollView(
- padding: const EdgeInsets.fromLTRB(16, 16, 16, 80),
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Expanded(
- child: Column(
- children: leftColumn
- .map((e) => Padding(
+ padding: const EdgeInsets.fromLTRB(16, 16, 16, 80),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Expanded(
+ child: Column(
+ children:
+ leftColumn
+ .map(
+ (e) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: e,
- ))
- .toList(),
- ),
+ ),
+ )
+ .toList(),
),
- const SizedBox(width: 12),
- Expanded(
- child: Column(
- children: rightColumn
- .map((e) => Padding(
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ children:
+ rightColumn
+ .map(
+ (e) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: e,
- ))
- .toList(),
- ),
+ ),
+ )
+ .toList(),
),
- ],
- ),
+ ),
+ ],
),
+ ),
);
}
@@ -172,11 +206,12 @@ class _ProjectTagPageState extends State<ProjectTagPage> {
Navigator.push(
context,
MaterialPageRoute(
- builder: (_) => ImageDetailsPage(
- imagePath: image.filePath,
- imageId: image.id,
- projectId: widget.projectId,
- ),
+ builder:
+ (_) => ImageDetailsPage(
+ imagePath: image.filePath,
+ imageId: image.id,
+ projectId: widget.projectId,
+ ),
),
).then((_) => _loadData());
},
@@ -198,12 +233,16 @@ class _ProjectTagPageState extends State<ProjectTagPage> {
File(image.filePath),
fit: BoxFit.cover,
width: double.infinity,
- errorBuilder: (_, __, ___) => Container(
- color: Variables.surfaceSubtle,
- child: const Center(
- child: Icon(Icons.broken_image, color: Variables.textDisabled),
- ),
- ),
+ errorBuilder:
+ (_, __, ___) => Container(
+ color: Variables.surfaceSubtle,
+ child: const Center(
+ child: Icon(
+ Icons.broken_image,
+ color: Variables.textDisabled,
+ ),
+ ),
+ ),
),
],
),
@@ -211,4 +250,4 @@ class _ProjectTagPageState extends State<ProjectTagPage> {
),
);
}
-}
-\ No newline at end of file
+}