commit 8adada1000fb2a389a32ef7cd473df5c8dfaa2c2
parent f2fdfa1b34ded69320e81c20521382c253d3838e
Author: Abhinav Rai <69450646+AbhinavRai01@users.noreply.github.com>
Date: Sat, 29 Nov 2025 15:03:40 +0530
Merge pull request #19 from nilotpal-n7/abhinav
ui fixes
image clicking now works
header fixed
Diffstat:
4 files changed, 477 insertions(+), 426 deletions(-)
diff --git a/lib/ui/pages/project_board_page.dart b/lib/ui/pages/project_board_page.dart
@@ -1,13 +1,17 @@
import 'dart:io';
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 '../../data/models/project_model.dart';
import '../../data/models/image_model.dart';
import '../../data/repos/project_repo.dart';
import '../../data/repos/image_repo.dart';
import 'project_tag_page.dart';
import 'project_board_page_alternate.dart';
-import 'image_save_page.dart'; // Import the Save Page
+import 'image_save_page.dart';
+import 'image_details_page.dart';
class ProjectBoardPage extends StatefulWidget {
final int projectId;
@@ -25,13 +29,9 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> {
final GlobalKey<ProjectBoardPageAlternateState> _alternatePageKey = GlobalKey();
- ProjectModel? _mainProject;
- List<ProjectModel> _events = [];
- ProjectModel? _selectedProject;
-
+ ProjectModel? _currentProject;
Map<String, List<ImageModel>> _categorizedImages = {};
bool _isLoading = true;
-
bool _showAlternateView = false;
@override
@@ -42,31 +42,28 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> {
Future<void> _initData() async {
try {
- final mainProject = await _projectRepo.getProjectById(widget.projectId);
- final events = await _projectRepo.getEvents(widget.projectId);
-
- if (mainProject != null) {
- _mainProject = mainProject;
- _events = events;
- _selectedProject = _mainProject;
- await _loadImagesForSelected();
- } else {
- if (mounted) Navigator.pop(context);
+ final project = await _projectRepo.getProjectById(widget.projectId);
+ if (project != null) {
+ if (mounted) {
+ setState(() {
+ _currentProject = project;
+ });
+ await _loadImagesForSelected();
+ }
}
} catch (e) {
- debugPrint("Error loading board data: $e");
- setState(() => _isLoading = false);
+ debugPrint("Error loading board: $e");
+ if (mounted) setState(() => _isLoading = false);
}
}
Future<void> _loadImagesForSelected() async {
- if (_selectedProject?.id == null) return;
-
+ if (_currentProject?.id == null) return;
if (!_showAlternateView) {
setState(() => _isLoading = true);
- final images = await _imageRepo.getImages(_selectedProject!.id!);
+ final images = await _imageRepo.getImages(_currentProject!.id!);
_categorizeImages(images);
- setState(() => _isLoading = false);
+ if (mounted) setState(() => _isLoading = false);
}
}
@@ -89,35 +86,39 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> {
}
}
- void _onProjectChanged(ProjectModel? newValue) {
- if (newValue != null && newValue != _selectedProject) {
+ void _onProjectChanged(ProjectModel newProject) {
+ if (newProject.id != _currentProject?.id) {
setState(() {
- _selectedProject = newValue;
+ _currentProject = newProject;
});
- if (!_showAlternateView) _loadImagesForSelected();
+ if (!_showAlternateView) {
+ _loadImagesForSelected();
+ } else {
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ _alternatePageKey.currentState?.refreshData();
+ });
+ }
}
}
- // --- Image Picker Logic ---
Future<void> _pickAndRedirect() async {
try {
- final XFile? pickedFile = await _picker.pickImage(source: ImageSource.gallery);
- if (pickedFile != null && _selectedProject != null) {
+ final List<XFile> pickedFiles = await _picker.pickMultiImage();
+
+ if (pickedFiles.isNotEmpty && _currentProject != null) {
if (!mounted) return;
- // Navigate to ImageSavePage
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ImageSavePage(
- imagePaths: [pickedFile.path],
- projectId: _selectedProject!.id!,
- projectName: _selectedProject!.title,
+ imagePaths: pickedFiles.map((e) => e.path).toList(),
+ projectId: _currentProject!.id!,
+ projectName: _currentProject!.title,
isFromShare: false,
),
),
).then((_) {
- // Refresh data upon return
if (!_showAlternateView) {
_loadImagesForSelected();
} else {
@@ -126,286 +127,251 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> {
});
}
} catch (e) {
- debugPrint("Error picking image: $e");
- if (mounted) {
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text("Error picking image: $e")),
- );
- }
+ debugPrint("Error picking images: $e");
}
}
@override
Widget build(BuildContext context) {
- final theme = Theme.of(context);
- final isDark = theme.brightness == Brightness.dark;
- final buttonColor = isDark ? Colors.white.withOpacity(0.1) : Colors.grey[100];
-
- final List<DropdownMenuItem<ProjectModel>> dropdownItems = [];
- if (_mainProject != null) {
- dropdownItems.add(
- DropdownMenuItem(
- value: _mainProject,
- child: Text(
- _mainProject!.title,
- style: const TextStyle(fontFamily: 'GeneralSans'),
- ),
- ),
- );
- }
- for (var event in _events) {
- dropdownItems.add(
- DropdownMenuItem(
- value: event,
- child: Text(
- event.title,
- style: const TextStyle(fontFamily: 'GeneralSans'),
- ),
- ),
+ if (_currentProject == null) {
+ return const Scaffold(
+ backgroundColor: Variables.background,
+ body: Center(child: CircularProgressIndicator()),
);
}
return Scaffold(
- backgroundColor: theme.scaffoldBackgroundColor,
- appBar: AppBar(
- backgroundColor: theme.appBarTheme.backgroundColor,
- centerTitle: false,
- title: Text(
- _selectedProject?.title ?? "Loading...",
- style: TextStyle(
- fontFamily: 'GeneralSans',
- fontWeight: FontWeight.w600,
- color: theme.colorScheme.onSurface,
- ),
- ),
- actions: [
- IconButton(
- icon: Icon(Icons.settings_outlined, color: theme.iconTheme.color),
- onPressed: () {},
- ),
- const SizedBox(width: 8),
- ],
+ backgroundColor: Variables.background,
+ appBar: TopBar(
+ currentProjectId: _currentProject!.id!,
+ onBack: () => Navigator.pop(context),
+ onProjectChanged: _onProjectChanged,
+ onSettingsPressed: () {},
+ ),
+ bottomNavigationBar: BottomBar(
+ currentTab: BottomBarItem.moodboard,
+ projectId: _currentProject!.id!,
),
- // --- FAB for Adding Image ---
floatingActionButton: FloatingActionButton(
onPressed: _pickAndRedirect,
- backgroundColor: isDark ? Colors.white : Colors.black,
- foregroundColor: isDark ? Colors.black : Colors.white,
+ backgroundColor: Variables.textPrimary,
+ foregroundColor: Variables.background,
child: const Icon(Icons.add_photo_alternate_outlined),
),
body: Column(
children: [
Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
+ padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
child: Row(
children: [
- Expanded(
+ InkWell(
+ onTap: () {
+ setState(() {
+ _showAlternateView = !_showAlternateView;
+ if (!_showAlternateView) _loadImagesForSelected();
+ });
+ },
+ borderRadius: BorderRadius.circular(20),
child: Container(
- height: 40,
- padding: const EdgeInsets.symmetric(horizontal: 12),
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
- color: buttonColor,
- borderRadius: BorderRadius.circular(10),
+ color: Variables.surfaceSubtle,
+ borderRadius: BorderRadius.circular(20),
+ border: Border.all(color: Variables.borderSubtle),
),
- child: DropdownButtonHideUnderline(
- child: DropdownButton<ProjectModel>(
- value: _selectedProject,
- isExpanded: true,
- items: dropdownItems,
- onChanged: _onProjectChanged,
- style: TextStyle(
- color: theme.colorScheme.onSurface,
- fontFamily: 'GeneralSans',
- fontWeight: FontWeight.w500,
- fontSize: 14,
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(
+ _showAlternateView ? Icons.dashboard : Icons.view_agenda_outlined,
+ size: 18,
+ color: Variables.textPrimary,
),
- icon: Icon(Icons.keyboard_arrow_down, size: 18, color: theme.iconTheme.color),
- dropdownColor: theme.cardColor,
- ),
+ const SizedBox(width: 8),
+ Text(
+ _showAlternateView ? "Categorized" : "All Images",
+ style: Variables.bodyStyle.copyWith(
+ fontWeight: FontWeight.w600,
+ fontSize: 13,
+ ),
+ ),
+ ],
),
),
),
- const SizedBox(width: 8),
- _buildControlIcon(
- theme, buttonColor, Icons.palette_outlined, "Stylesheet",
- () => ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Stylesheet")))
- ),
- const SizedBox(width: 8),
- _buildControlIcon(
- theme, buttonColor, Icons.tune_outlined, "Filter",
- () {
- if (_showAlternateView) {
+ const Spacer(),
+ if (_showAlternateView)
+ InkWell(
+ onTap: () {
_alternatePageKey.currentState?.showFilterDialog();
- } else {
- ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(content: Text("Filter for categories not implemented")),
- );
- }
- }
- ),
- const SizedBox(width: 8),
-
- _buildControlIcon(
- theme,
- _showAlternateView ? Colors.black : buttonColor,
- _showAlternateView ? Icons.dashboard : Icons.view_agenda_outlined,
- "Switch View",
- () {
- setState(() {
- _showAlternateView = !_showAlternateView;
- if (!_showAlternateView) _loadImagesForSelected();
- });
- },
- iconColor: _showAlternateView ? Colors.white : theme.iconTheme.color,
- ),
+ },
+ borderRadius: BorderRadius.circular(20),
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ decoration: BoxDecoration(
+ color: Variables.surfaceSubtle,
+ borderRadius: BorderRadius.circular(20),
+ border: Border.all(color: Variables.borderSubtle),
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const Icon(
+ Icons.tune_outlined,
+ size: 18,
+ color: Variables.textPrimary,
+ ),
+ const SizedBox(width: 8),
+ Text(
+ "Filter",
+ style: Variables.bodyStyle.copyWith(
+ fontWeight: FontWeight.w600,
+ fontSize: 13,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
],
),
),
-
Expanded(
child: _showAlternateView
? ProjectBoardPageAlternate(
key: _alternatePageKey,
- projectId: _selectedProject?.id ?? widget.projectId,
+ projectId: _currentProject!.id!,
)
- : _buildCategorizedView(theme, isDark),
+ : _buildCategorizedView(),
),
],
),
);
}
- Widget _buildCategorizedView(ThemeData theme, bool isDark) {
+ Widget _buildCategorizedView() {
if (_isLoading) return const Center(child: CircularProgressIndicator());
if (_categorizedImages.isEmpty) {
return Center(
child: Text(
"No images found",
- style: TextStyle(
- fontFamily: 'GeneralSans',
- color: theme.colorScheme.onSurface.withOpacity(0.6),
- ),
+ style: Variables.bodyStyle.copyWith(color: Variables.textSecondary),
),
);
}
return ListView.builder(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ padding: const EdgeInsets.fromLTRB(16, 0, 16, 80),
itemCount: _categorizedImages.keys.length,
itemBuilder: (context, index) {
final category = _categorizedImages.keys.elementAt(index);
final images = _categorizedImages[category]!;
- return GestureDetector(
- onTap: () {
- if (_selectedProject?.id != null) {
- Navigator.push(
- context,
- MaterialPageRoute(
- builder: (_) => ProjectTagPage(
- projectId: _selectedProject!.id!,
- tag: category,
- ),
- ),
- );
- }
- },
- child: Container(
- margin: const EdgeInsets.only(bottom: 16),
- clipBehavior: Clip.antiAlias,
- decoration: BoxDecoration(
- color: isDark ? const Color(0xFF1E1E1E) : Colors.white,
- border: Border.all(color: Colors.grey.withOpacity(0.3)),
- borderRadius: BorderRadius.circular(16),
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Padding(
+ return Container(
+ margin: const EdgeInsets.only(bottom: 16),
+ // Keep this clipping so the scrolling list cuts off cleanly at the border
+ clipBehavior: Clip.antiAlias,
+ decoration: BoxDecoration(
+ color: Colors.white,
+ border: Border.all(color: Variables.borderSubtle),
+ borderRadius: BorderRadius.circular(16),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ InkWell(
+ onTap: () {
+ if (_currentProject?.id != null) {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (_) => ProjectTagPage(
+ projectId: _currentProject!.id!,
+ tag: category,
+ ),
+ ),
+ );
+ }
+ },
+ child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
category.toUpperCase(),
- style: TextStyle(
+ style: Variables.headerStyle.copyWith(
fontSize: 14,
- fontWeight: FontWeight.bold,
letterSpacing: 1.0,
- fontFamily: 'GeneralSans',
- color: theme.colorScheme.onSurface,
),
),
- Icon(
+ const Icon(
Icons.arrow_forward,
size: 16,
- color: theme.colorScheme.onSurface.withOpacity(0.4),
+ color: Variables.textSecondary,
),
],
),
),
- SizedBox(
- height: 140,
- child: ListView.separated(
- clipBehavior: Clip.none,
- padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
- scrollDirection: Axis.horizontal,
- itemCount: images.length,
- separatorBuilder: (_, __) => const SizedBox(width: 8),
- itemBuilder: (context, imgIndex) {
- final image = images[imgIndex];
- return Container(
+ ),
+ SizedBox(
+ height: 140,
+ child: ListView.separated(
+ clipBehavior: Clip.none,
+ padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
+ scrollDirection: Axis.horizontal,
+ itemCount: images.length,
+ separatorBuilder: (_, __) => const SizedBox(width: 8),
+ itemBuilder: (context, imgIndex) {
+ final image = images[imgIndex];
+ return GestureDetector(
+ onTap: () {
+ if (_currentProject?.id != null) {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (_) => ImageDetailsPage(
+ imagePath: image.filePath,
+ imageId: image.id,
+ projectId: _currentProject!.id!,
+ ),
+ ),
+ ).then((_) => _loadImagesForSelected());
+ }
+ },
+ child: Container(
width: 120,
- clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
- color: isDark ? Colors.black26 : Colors.grey[200],
- border: Border.all(color: theme.dividerColor.withOpacity(0.1)),
+ color: Variables.surfaceSubtle,
+ border: Border.all(color: Variables.borderSubtle),
),
- child: Stack(
- fit: StackFit.expand,
- children: [
- Image.file(
- File(image.filePath),
- fit: BoxFit.cover,
- errorBuilder: (_, __, ___) => Container(
- color: Colors.grey[300],
- child: const Icon(Icons.broken_image, color: Colors.grey),
+ // FIX: Explicitly clip image to border radius
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(12),
+ child: Stack(
+ fit: StackFit.expand,
+ children: [
+ Image.file(
+ File(image.filePath),
+ fit: BoxFit.cover,
+ errorBuilder: (_, __, ___) => Container(
+ color: Variables.surfaceSubtle,
+ child: const Icon(Icons.broken_image, color: Variables.textDisabled),
+ ),
),
- ),
- ],
+ ],
+ ),
),
- );
- },
- ),
+ ),
+ );
+ },
),
- ],
- ),
+ ),
+ ],
),
);
},
);
}
-
- Widget _buildControlIcon(
- ThemeData theme,
- Color? bgColor,
- IconData icon,
- String tooltip,
- VoidCallback onTap,
- {Color? iconColor}
- ) {
- return GestureDetector(
- onTap: onTap,
- child: Container(
- width: 40,
- height: 40,
- decoration: BoxDecoration(
- color: bgColor,
- borderRadius: BorderRadius.circular(10),
- ),
- child: Icon(icon, size: 20, color: iconColor ?? theme.iconTheme.color),
- ),
- );
- }
}
\ 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
@@ -1,5 +1,6 @@
import 'dart:io';
import 'package:flutter/material.dart';
+import 'package:adobe/ui/styles/variables.dart';
import '../../data/models/image_model.dart';
import '../../data/repos/image_repo.dart';
import 'image_details_page.dart';
@@ -29,6 +30,15 @@ class ProjectBoardPageAlternateState extends State<ProjectBoardPageAlternate> {
_loadData();
}
+ // Called when project changes in parent
+ @override
+ void didUpdateWidget(covariant ProjectBoardPageAlternate oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (oldWidget.projectId != widget.projectId) {
+ _loadData();
+ }
+ }
+
void refreshData() {
_loadData();
}
@@ -75,9 +85,9 @@ class ProjectBoardPageAlternateState extends State<ProjectBoardPageAlternate> {
builder: (context, setModalState) {
return Container(
height: MediaQuery.of(context).size.height * 0.6,
- decoration: BoxDecoration(
- color: Theme.of(context).scaffoldBackgroundColor,
- borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
+ decoration: const BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
padding: const EdgeInsets.all(24),
child: Column(
@@ -86,9 +96,9 @@ class ProjectBoardPageAlternateState extends State<ProjectBoardPageAlternate> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- const Text(
+ Text(
"Filter by Tags",
- style: TextStyle(fontFamily: 'GeneralSans', fontSize: 20, fontWeight: FontWeight.bold),
+ style: Variables.headerStyle.copyWith(fontSize: 20),
),
IconButton(
icon: const Icon(Icons.close),
@@ -98,7 +108,7 @@ class ProjectBoardPageAlternateState extends State<ProjectBoardPageAlternate> {
),
const SizedBox(height: 20),
if (_allTags.isEmpty)
- const Text("No tags available."),
+ Text("No tags available.", style: Variables.bodyStyle),
Expanded(
child: SingleChildScrollView(
@@ -118,18 +128,17 @@ class ProjectBoardPageAlternateState extends State<ProjectBoardPageAlternate> {
_selectedTags.remove(tag);
}
});
+ // Update main state
this.setState(() {
_applyFilter();
});
},
- labelStyle: TextStyle(
- fontFamily: 'GeneralSans',
- color: isSelected ? Colors.white : null,
- fontSize: 12,
- fontWeight: FontWeight.w500,
+ labelStyle: Variables.captionStyle.copyWith(
+ color: isSelected ? Colors.white : Variables.textPrimary,
+ fontWeight: FontWeight.w600,
),
- backgroundColor: Colors.grey[200],
- selectedColor: Colors.black87,
+ backgroundColor: Variables.surfaceSubtle,
+ selectedColor: Variables.textPrimary,
checkmarkColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
@@ -159,7 +168,7 @@ class ProjectBoardPageAlternateState extends State<ProjectBoardPageAlternate> {
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
- backgroundColor: Colors.black,
+ backgroundColor: Variables.textPrimary,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
@@ -195,18 +204,17 @@ class ProjectBoardPageAlternateState extends State<ProjectBoardPageAlternate> {
return Column(
children: [
+ // Context Info Row (Image Count + Active Filters)
Padding(
- padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
+ padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: Row(
children: [
Text(
"ALL IMAGES (${_filteredImages.length})",
- style: TextStyle(
- fontFamily: 'GeneralSans',
+ style: Variables.captionStyle.copyWith(
fontWeight: FontWeight.bold,
- fontSize: 12,
letterSpacing: 1.2,
- color: Colors.grey[600],
+ color: Variables.textSecondary,
),
),
const Spacer(),
@@ -214,22 +222,29 @@ class ProjectBoardPageAlternateState extends State<ProjectBoardPageAlternate> {
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
- color: Colors.black,
+ color: Variables.textPrimary,
borderRadius: BorderRadius.circular(12),
),
child: Text(
- "${_selectedTags.length} Filters",
- style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold),
+ "${_selectedTags.length} Active",
+ style: Variables.captionStyle.copyWith(color: Colors.white, fontWeight: FontWeight.bold),
),
)
],
),
),
+
+ // Masonry Grid
Expanded(
child: _filteredImages.isEmpty
- ? Center(child: Text("No images found", style: TextStyle(color: Colors.grey[500])))
+ ? Center(
+ child: Text(
+ "No images found",
+ style: Variables.bodyStyle.copyWith(color: Variables.textSecondary),
+ ),
+ )
: SingleChildScrollView(
- padding: const EdgeInsets.symmetric(horizontal: 16),
+ padding: const EdgeInsets.fromLTRB(16, 0, 16, 80), // Bottom padding for FAB
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -266,7 +281,7 @@ class ProjectBoardPageAlternateState extends State<ProjectBoardPageAlternate> {
height: (index % 3 == 0) ? 240 : 180,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
- color: Colors.grey[200],
+ color: Variables.surfaceSubtle,
),
clipBehavior: Clip.antiAlias,
child: Stack(
@@ -276,7 +291,9 @@ class ProjectBoardPageAlternateState extends State<ProjectBoardPageAlternate> {
File(image.filePath),
fit: BoxFit.cover,
width: double.infinity,
- errorBuilder: (_,__,___) => const Center(child: Icon(Icons.broken_image, color: Colors.grey)),
+ errorBuilder: (_,__,___) => const Center(
+ child: Icon(Icons.broken_image, color: Variables.textDisabled),
+ ),
),
if (image.tags.isNotEmpty)
Positioned(
@@ -300,7 +317,15 @@ class ProjectBoardPageAlternateState extends State<ProjectBoardPageAlternate> {
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)),
+ child: Text(
+ tag.toUpperCase(),
+ style: const TextStyle(
+ color: Colors.white,
+ fontSize: 9,
+ fontFamily: 'GeneralSans',
+ fontWeight: FontWeight.w600
+ ),
+ ),
);
}).toList(),
),
diff --git a/lib/ui/pages/project_tag_page.dart b/lib/ui/pages/project_tag_page.dart
@@ -1,10 +1,14 @@
import 'dart:io';
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 '../../data/models/image_model.dart';
import '../../data/repos/image_repo.dart';
-import '../../data/repos/project_repo.dart'; // Added to fetch project name
-import 'image_save_page.dart'; // Import Save Page
+import '../../data/repos/project_repo.dart';
+import 'image_save_page.dart';
+import 'image_details_page.dart';
class ProjectTagPage extends StatefulWidget {
final int projectId;
@@ -26,7 +30,7 @@ class _ProjectTagPageState extends State<ProjectTagPage> {
final ImagePicker _picker = ImagePicker();
List<ImageModel> _images = [];
- String _projectName = "Project"; // Default
+ String _projectName = "Project";
bool _isLoading = true;
@override
@@ -36,7 +40,9 @@ class _ProjectTagPageState extends State<ProjectTagPage> {
}
Future<void> _loadData() async {
- // 1. Fetch Images
+ final project = await _projectRepo.getProjectById(widget.projectId);
+ if (project != null) _projectName = project.title;
+
final allImages = await _imageRepo.getImages(widget.projectId);
final filtered = allImages.where((img) {
if (widget.tag == 'Uncategorized') {
@@ -45,13 +51,9 @@ class _ProjectTagPageState extends State<ProjectTagPage> {
return img.tags.contains(widget.tag);
}).toList();
- // 2. Fetch Project Name (for the Save Page)
- final project = await _projectRepo.getProjectById(widget.projectId);
-
if (mounted) {
setState(() {
_images = filtered;
- if (project != null) _projectName = project.title;
_isLoading = false;
});
}
@@ -59,52 +61,62 @@ class _ProjectTagPageState extends State<ProjectTagPage> {
Future<void> _pickAndRedirect() async {
try {
- final XFile? pickedFile = await _picker.pickImage(source: ImageSource.gallery);
- if (pickedFile != null) {
+ final List<XFile> pickedFiles = await _picker.pickMultiImage();
+ if (pickedFiles.isNotEmpty) {
if (!mounted) return;
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ImageSavePage(
- imagePaths: [pickedFile.path],
+ imagePaths: pickedFiles.map((e) => e.path).toList(),
projectId: widget.projectId,
projectName: _projectName,
isFromShare: false,
),
),
- ).then((_) => _loadData()); // Refresh upon return
+ ).then((_) => _loadData());
}
} catch (e) {
- debugPrint("Error picking image: $e");
+ debugPrint("Error picking images: $e");
}
}
@override
Widget build(BuildContext context) {
- final theme = Theme.of(context);
- final isDark = theme.brightness == Brightness.dark;
+ // 1. Prepare Columns for Masonry Layout
+ final leftColumn = <Widget>[];
+ final rightColumn = <Widget>[];
+
+ for (int i = 0; i < _images.length; i++) {
+ final item = _buildStaggeredImageItem(_images[i], index: i);
+ if (i % 2 == 0) {
+ leftColumn.add(item);
+ } else {
+ rightColumn.add(item);
+ }
+ }
return Scaffold(
- backgroundColor: theme.scaffoldBackgroundColor,
- appBar: AppBar(
- title: Text(
- widget.tag.toUpperCase(),
- style: const TextStyle(
- fontFamily: 'GeneralSans',
- fontWeight: FontWeight.w600,
- letterSpacing: 1.0,
- ),
- ),
- centerTitle: false,
- backgroundColor: theme.appBarTheme.backgroundColor,
- elevation: 0,
+ backgroundColor: Variables.background,
+
+ appBar: TopBar(
+ currentProjectId: widget.projectId,
+ titleOverride: widget.tag.toUpperCase(),
+ onBack: () => Navigator.pop(context),
),
+
+ bottomNavigationBar: BottomBar(
+ currentTab: BottomBarItem.moodboard,
+ projectId: widget.projectId,
+ ),
+
floatingActionButton: FloatingActionButton(
onPressed: _pickAndRedirect,
- backgroundColor: isDark ? Colors.white : Colors.black,
- foregroundColor: isDark ? Colors.black : Colors.white,
+ backgroundColor: Variables.textPrimary,
+ foregroundColor: Variables.background,
child: const Icon(Icons.add_photo_alternate_outlined),
),
+
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _images.isEmpty
@@ -112,64 +124,91 @@ class _ProjectTagPageState extends State<ProjectTagPage> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
- Icon(Icons.image_not_supported_outlined,
- size: 64, color: Colors.grey[400]),
+ const Icon(Icons.image_not_supported_outlined,
+ size: 64, color: Variables.textDisabled),
const SizedBox(height: 16),
Text(
"No images found for '${widget.tag}'",
- style: TextStyle(
- fontFamily: 'GeneralSans',
- fontSize: 16,
- color: Colors.grey[600],
- ),
+ style: Variables.bodyStyle.copyWith(color: Variables.textSecondary),
),
],
),
)
- : GridView.builder(
- padding: const EdgeInsets.all(16),
- gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
- crossAxisCount: 2,
- crossAxisSpacing: 12,
- mainAxisSpacing: 12,
- childAspectRatio: 0.8,
- ),
- itemCount: _images.length,
- itemBuilder: (context, index) {
- final image = _images[index];
- return GestureDetector(
- onTap: () {
- // Details logic
- },
- child: Container(
- decoration: BoxDecoration(
- color: isDark ? const Color(0xFF1E1E1E) : Colors.grey[200],
- borderRadius: BorderRadius.circular(16),
- border: Border.all(
- color: isDark ? Colors.white10 : Colors.transparent,
- ),
+ : 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.only(bottom: 12),
+ child: e,
+ ))
+ .toList(),
),
- clipBehavior: Clip.antiAlias,
- child: Stack(
- fit: StackFit.expand,
- children: [
- Image.file(
- File(image.filePath),
- fit: BoxFit.cover,
- width: double.infinity,
- errorBuilder: (_, __, ___) => Container(
- color: Colors.grey[300],
- child: const Center(
- child: Icon(Icons.broken_image, color: Colors.grey),
- ),
- ),
- ),
- ],
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ children: rightColumn
+ .map((e) => Padding(
+ padding: const EdgeInsets.only(bottom: 12),
+ child: e,
+ ))
+ .toList(),
),
),
- );
- },
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildStaggeredImageItem(ImageModel image, {required int index}) {
+ return GestureDetector(
+ onTap: () {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (_) => ImageDetailsPage(
+ imagePath: image.filePath,
+ imageId: image.id,
+ projectId: widget.projectId,
+ ),
+ ),
+ ).then((_) => _loadData());
+ },
+ child: Container(
+ // Simulate staggered heights similar to Alternate Page
+ height: (index % 3 == 0) ? 240 : 180,
+ decoration: BoxDecoration(
+ color: Variables.surfaceSubtle,
+ borderRadius: BorderRadius.circular(16),
+ border: Border.all(color: Variables.borderSubtle),
+ ),
+ // Explicitly clip content to border radius
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(16),
+ child: Stack(
+ fit: StackFit.expand,
+ children: [
+ Image.file(
+ 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),
+ ),
),
+ ),
+ ],
+ ),
+ ),
+ ),
);
}
}
\ No newline at end of file
diff --git a/lib/ui/widgets/top_bar.dart b/lib/ui/widgets/top_bar.dart
@@ -1,13 +1,14 @@
import 'package:flutter/material.dart';
-import 'package:adobe/data/models/project_model.dart';
-import 'package:adobe/data/repos/project_repo.dart';
-import 'package:adobe/ui/styles/variables.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
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
const TopBar({
super.key,
@@ -15,13 +16,15 @@ class TopBar extends StatefulWidget implements PreferredSizeWidget {
this.onBack,
this.onProjectChanged,
this.onSettingsPressed,
+ this.titleOverride,
});
@override
State<TopBar> createState() => _TopBarState();
+ // FIX: Reduced from 80 to 56 (Standard Toolbar Height)
@override
- Size get preferredSize => const Size.fromHeight(80);
+ Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}
class _TopBarState extends State<TopBar> {
@@ -55,11 +58,9 @@ class _TopBarState extends State<TopBar> {
List<ProjectModel> events = [];
if (current.parentId == null) {
- // Current IS Project
root = current;
events = await _projectRepo.getEvents(current.id!);
} else {
- // Current IS Event
root = await _projectRepo.getProjectById(current.parentId!);
if (root != null) {
events = await _projectRepo.getEvents(root.id!);
@@ -86,103 +87,122 @@ class _TopBarState extends State<TopBar> {
color: Variables.background,
child: SafeArea(
bottom: false,
- child: Container(
- height: 80,
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: Row(
- children: [
- // 1. Back Button
- if (widget.onBack != null)
- IconButton(
- icon: const Icon(Icons.arrow_back, color: Variables.textPrimary),
- onPressed: widget.onBack,
- ),
- if (widget.onBack != null) const SizedBox(width: 8),
+ child: SizedBox(
+ height: kToolbarHeight, // FIX: Use standard height
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ child: Row(
+ children: [
+ // 1. Back Button
+ if (widget.onBack != null) ...[
+ IconButton(
+ icon: const Icon(Icons.arrow_back, color: Variables.textPrimary),
+ onPressed: widget.onBack,
+ padding: EdgeInsets.zero,
+ constraints: const BoxConstraints(),
+ ),
+ const SizedBox(width: 12),
+ ],
- // 2. Title & Selector
- if (!_isLoading && _currentProject != null && _rootProject != null)
+ // 2. Title & Selector
Expanded(
- child: Row(
- children: [
- Expanded(
- child: Text(
- _rootProject!.title,
+ child: widget.titleOverride != null
+ ? Text(
+ widget.titleOverride!,
style: const TextStyle(
- fontSize: 26,
+ fontSize: 20, // Slightly smaller for tags
fontWeight: FontWeight.w600,
color: Variables.textPrimary,
),
overflow: TextOverflow.ellipsis,
- ),
- ),
- const SizedBox(width: 12),
- PopupMenuButton<ProjectModel>(
- 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: 14, vertical: 10),
- decoration: BoxDecoration(
- color: Variables.surfaceSubtle,
- borderRadius: BorderRadius.circular(12),
- ),
- child: Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- Text(
- _currentProject!.id == _rootProject!.id
- ? "Main Project"
- : _currentProject!.title,
- style: const TextStyle(
- fontSize: 16,
- fontWeight: FontWeight.w500,
- color: Variables.textPrimary,
+ )
+ : (!_isLoading && _currentProject != null && _rootProject != null)
+ ? Row(
+ children: [
+ Flexible(
+ child: Text(
+ _rootProject!.title,
+ style: const TextStyle(
+ fontSize: 22, // Adjusted font size
+ fontWeight: FontWeight.w600,
+ color: Variables.textPrimary,
+ ),
+ overflow: TextOverflow.ellipsis,
+ ),
),
- ),
- const SizedBox(width: 4),
- const Icon(
- Icons.keyboard_arrow_down_rounded,
- color: Variables.textPrimary,
- )
- ],
- ),
- ),
- 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,
+ 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();
+ },
),
- ),
- );
- }).toList();
- },
- ),
- ],
- ),
+ ],
+ )
+ : const SizedBox(),
),
- // 3. Settings Icon
- IconButton(
- icon: const Icon(Icons.settings_outlined, color: Variables.textPrimary),
- onPressed: widget.onSettingsPressed,
- ),
- ],
+ // 3. Settings Icon
+ IconButton(
+ icon: const Icon(Icons.settings_outlined, color: Variables.textPrimary),
+ onPressed: widget.onSettingsPressed,
+ padding: EdgeInsets.zero,
+ constraints: const BoxConstraints(),
+ ),
+ ],
+ ),
),
),
),
);
}
-}
+}
+\ No newline at end of file