commit 90ffcf2dc055b6ca5c9c3da4cd9676fb88e8b704
parent bee4353a97b9bf209594e28cb9c66a423231a22c
Author: Abhinav Rai <69450646+AbhinavRai01@users.noreply.github.com>
Date: Sat, 29 Nov 2025 13:05:43 +0530
Merge pull request #17 from nilotpal-n7/abhinav
new board page implemented
upload image from gallery implemented
filters implemented
Diffstat:
4 files changed, 651 insertions(+), 311 deletions(-)
diff --git a/lib/data/database.dart b/lib/data/database.dart
@@ -9,7 +9,7 @@ import 'models/note_model.dart';
class AppDatabase {
static Database? _db;
- static const String _dbName = 'database_v2.db';
+ static const String _dbName = 'database_v3.db';
static Future<Database> get db async {
if (_db != null) return _db!;
diff --git a/lib/ui/pages/project_board_page.dart b/lib/ui/pages/project_board_page.dart
@@ -1,11 +1,13 @@
import 'dart:io';
import 'package:flutter/material.dart';
+import 'package:image_picker/image_picker.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 'image_details_page.dart';
+import 'project_board_page_alternate.dart';
+import 'image_save_page.dart'; // Import the Save Page
class ProjectBoardPage extends StatefulWidget {
final int projectId;
@@ -19,6 +21,9 @@ class ProjectBoardPage extends StatefulWidget {
class _ProjectBoardPageState extends State<ProjectBoardPage> {
final _projectRepo = ProjectRepo();
final _imageRepo = ImageRepo();
+ final ImagePicker _picker = ImagePicker();
+
+ final GlobalKey<ProjectBoardPageAlternateState> _alternatePageKey = GlobalKey();
ProjectModel? _mainProject;
List<ProjectModel> _events = [];
@@ -26,6 +31,8 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> {
Map<String, List<ImageModel>> _categorizedImages = {};
bool _isLoading = true;
+
+ bool _showAlternateView = false;
@override
void initState() {
@@ -34,35 +41,37 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> {
}
Future<void> _initData() async {
- final events = await _projectRepo.getEvents(widget.projectId);
-
- _mainProject = ProjectModel(
- id: widget.projectId,
- title: "Main Project",
- lastAccessedAt: DateTime.now(),
- createdAt: DateTime.now(),
- );
+ try {
+ final mainProject = await _projectRepo.getProjectById(widget.projectId);
+ final events = await _projectRepo.getEvents(widget.projectId);
- _events = events;
- _selectedProject = _mainProject;
-
- await _loadImagesForSelected();
+ if (mainProject != null) {
+ _mainProject = mainProject;
+ _events = events;
+ _selectedProject = _mainProject;
+ await _loadImagesForSelected();
+ } else {
+ if (mounted) Navigator.pop(context);
+ }
+ } catch (e) {
+ debugPrint("Error loading board data: $e");
+ setState(() => _isLoading = false);
+ }
}
Future<void> _loadImagesForSelected() async {
if (_selectedProject?.id == null) return;
-
- setState(() => _isLoading = true);
-
- final images = await _imageRepo.getImages(_selectedProject!.id!);
- _categorizeImages(images);
-
- setState(() => _isLoading = false);
+
+ if (!_showAlternateView) {
+ setState(() => _isLoading = true);
+ final images = await _imageRepo.getImages(_selectedProject!.id!);
+ _categorizeImages(images);
+ setState(() => _isLoading = false);
+ }
}
void _categorizeImages(List<ImageModel> images) {
_categorizedImages.clear();
-
for (var img in images) {
if (img.tags.isEmpty) {
if (!_categorizedImages.containsKey('Uncategorized')) {
@@ -85,36 +94,52 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> {
setState(() {
_selectedProject = newValue;
});
- _loadImagesForSelected();
+ if (!_showAlternateView) _loadImagesForSelected();
}
}
- // Navigate to image details and refresh on return
- Future<void> _navigateToImageDetails(ImageModel image) async {
- final result = await Navigator.push(
- context,
- MaterialPageRoute(
- builder:
- (_) => ImageDetailsPage(
- imagePath: image.filePath,
- imageId: image.id,
- projectId: widget.projectId,
+ // --- Image Picker Logic ---
+ Future<void> _pickAndRedirect() async {
+ try {
+ final XFile? pickedFile = await _picker.pickImage(source: ImageSource.gallery);
+ if (pickedFile != null && _selectedProject != null) {
+ if (!mounted) return;
+
+ // Navigate to ImageSavePage
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (_) => ImageSavePage(
+ imagePaths: [pickedFile.path],
+ projectId: _selectedProject!.id!,
+ projectName: _selectedProject!.title,
+ isFromShare: false,
),
- ),
- );
-
- // Refresh the board when returning from image details
- // The result can be anything or null - we always refresh
- await _loadImagesForSelected();
+ ),
+ ).then((_) {
+ // Refresh data upon return
+ if (!_showAlternateView) {
+ _loadImagesForSelected();
+ } else {
+ _alternatePageKey.currentState?.refreshData();
+ }
+ });
+ }
+ } catch (e) {
+ debugPrint("Error picking image: $e");
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text("Error picking image: $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 buttonColor = isDark ? Colors.white.withOpacity(0.1) : Colors.grey[100];
final List<DropdownMenuItem<ProjectModel>> dropdownItems = [];
if (_mainProject != null) {
@@ -146,7 +171,7 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> {
backgroundColor: theme.appBarTheme.backgroundColor,
centerTitle: false,
title: Text(
- _mainProject?.title ?? "Project",
+ _selectedProject?.title ?? "Loading...",
style: TextStyle(
fontFamily: 'GeneralSans',
fontWeight: FontWeight.w600,
@@ -161,14 +186,17 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> {
const SizedBox(width: 8),
],
),
+ // --- FAB for Adding Image ---
+ floatingActionButton: FloatingActionButton(
+ onPressed: _pickAndRedirect,
+ backgroundColor: isDark ? Colors.white : Colors.black,
+ foregroundColor: isDark ? Colors.black : Colors.white,
+ child: const Icon(Icons.add_photo_alternate_outlined),
+ ),
body: Column(
children: [
- // Controls Row
Padding(
- padding: const EdgeInsets.symmetric(
- horizontal: 16.0,
- vertical: 8.0,
- ),
+ padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Row(
children: [
Expanded(
@@ -191,11 +219,7 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> {
fontWeight: FontWeight.w500,
fontSize: 14,
),
- icon: Icon(
- Icons.keyboard_arrow_down,
- size: 18,
- color: theme.iconTheme.color,
- ),
+ icon: Icon(Icons.keyboard_arrow_down, size: 18, color: theme.iconTheme.color),
dropdownColor: theme.cardColor,
),
),
@@ -203,161 +227,173 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> {
),
const SizedBox(width: 8),
_buildControlIcon(
- theme,
- buttonColor,
- Icons.palette_outlined,
- "Stylesheet",
+ 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",
() {
- ScaffoldMessenger.of(
- context,
- ).showSnackBar(const SnackBar(content: Text("Stylesheet")));
- },
+ if (_showAlternateView) {
+ _alternatePageKey.currentState?.showFilterDialog();
+ } else {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text("Filter for categories not implemented")),
+ );
+ }
+ }
),
const SizedBox(width: 8),
+
_buildControlIcon(
- theme,
- buttonColor,
- Icons.tune_outlined,
- "Filter",
- () {},
+ 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,
),
],
),
),
- // Main Content
Expanded(
- child:
- _isLoading
- ? const Center(child: CircularProgressIndicator())
- : _categorizedImages.isEmpty
- ? Center(
- child: Text(
- "No images found",
+ child: _showAlternateView
+ ? ProjectBoardPageAlternate(
+ key: _alternatePageKey,
+ projectId: _selectedProject?.id ?? widget.projectId,
+ )
+ : _buildCategorizedView(theme, isDark),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildCategorizedView(ThemeData theme, bool isDark) {
+ 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),
+ ),
+ ),
+ );
+ }
+
+ return ListView.builder(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ 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(
+ padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ category.toUpperCase(),
style: TextStyle(
+ fontSize: 14,
+ fontWeight: FontWeight.bold,
+ letterSpacing: 1.0,
fontFamily: 'GeneralSans',
- color: theme.colorScheme.onSurface.withOpacity(0.6),
+ color: theme.colorScheme.onSurface,
),
),
- )
- : ListView.builder(
- padding: const EdgeInsets.symmetric(
- horizontal: 16,
- vertical: 8,
+ Icon(
+ Icons.arrow_forward,
+ size: 16,
+ color: theme.colorScheme.onSurface.withOpacity(0.4),
),
- 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,
- ),
- ),
- ).then((_) => _loadImagesForSelected());
- }
- },
- 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),
+ ],
+ ),
+ ),
+ 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(
+ 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)),
+ ),
+ 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),
),
- borderRadius: BorderRadius.circular(16),
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- // Category Header
- Padding(
- padding: const EdgeInsets.fromLTRB(
- 16,
- 12,
- 16,
- 8,
- ),
- child: Row(
- mainAxisAlignment:
- MainAxisAlignment.spaceBetween,
- children: [
- Text(
- category.toUpperCase(),
- style: TextStyle(
- fontSize: 14,
- fontWeight: FontWeight.bold,
- letterSpacing: 1.0,
- fontFamily: 'GeneralSans',
- color: theme.colorScheme.onSurface,
- ),
- ),
- Icon(
- Icons.arrow_forward,
- size: 16,
- color: theme.colorScheme.onSurface
- .withOpacity(0.4),
- ),
- ],
- ),
- ),
-
- // Image List
- 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 _buildImageCard(
- image,
- theme,
- isDark,
- );
- },
- ),
- ),
- ],
),
- ),
- );
- },
- ),
+ ],
+ ),
+ );
+ },
+ ),
+ ),
+ ],
+ ),
),
- ],
- ),
+ );
+ },
);
}
Widget _buildControlIcon(
- ThemeData theme,
- Color? bgColor,
- IconData icon,
+ ThemeData theme,
+ Color? bgColor,
+ IconData icon,
String tooltip,
VoidCallback onTap,
+ {Color? iconColor}
) {
return GestureDetector(
onTap: onTap,
@@ -368,40 +404,8 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> {
color: bgColor,
borderRadius: BorderRadius.circular(10),
),
- child: Icon(icon, size: 20, color: theme.iconTheme.color),
+ child: Icon(icon, size: 20, color: iconColor ?? theme.iconTheme.color),
),
);
}
-
- Widget _buildImageCard(ImageModel image, ThemeData theme, bool isDark) {
- return GestureDetector(
- onTap: () => _navigateToImageDetails(image),
- 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)),
- ),
- child: Stack(
- fit: StackFit.expand,
- children: [
- Hero(
- tag: 'image_${image.id}',
- child: Image.file(
- File(image.filePath),
- fit: BoxFit.cover,
- errorBuilder:
- (_, __, ___) => Container(
- color: Colors.grey[300],
- child: const Icon(Icons.broken_image, color: Colors.grey),
- ),
- ),
- ),
- ],
- ),
- ),
- );
- }
-}
+}
+\ 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
@@ -0,0 +1,314 @@
+import 'dart:io';
+import 'package:flutter/material.dart';
+import '../../data/models/image_model.dart';
+import '../../data/repos/image_repo.dart';
+import 'image_details_page.dart';
+
+class ProjectBoardPageAlternate extends StatefulWidget {
+ final int projectId;
+
+ const ProjectBoardPageAlternate({super.key, required this.projectId});
+
+ @override
+ 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;
+
+ @override
+ void initState() {
+ super.initState();
+ _loadData();
+ }
+
+ void refreshData() {
+ _loadData();
+ }
+
+ 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);
+ }
+
+ if (mounted) {
+ setState(() {
+ _allImages = images;
+ _filteredImages = images;
+ _allTags = tags.toList()..sort();
+ _isLoading = false;
+ });
+ if (_selectedTags.isNotEmpty) _applyFilter();
+ }
+ }
+
+ void _applyFilter() {
+ if (_selectedTags.isEmpty) {
+ setState(() => _filteredImages = _allImages);
+ } else {
+ setState(() {
+ _filteredImages = _allImages.where((img) {
+ return img.tags.toSet().intersection(_selectedTags).isNotEmpty;
+ }).toList();
+ });
+ }
+ }
+
+ void showFilterDialog() {
+ showModalBottomSheet(
+ context: context,
+ backgroundColor: Colors.transparent,
+ isScrollControlled: true,
+ builder: (context) {
+ return StatefulBuilder(
+ 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)),
+ ),
+ padding: const EdgeInsets.all(24),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ const Text(
+ "Filter by Tags",
+ style: TextStyle(fontFamily: 'GeneralSans', fontSize: 20, fontWeight: FontWeight.bold),
+ ),
+ IconButton(
+ icon: const Icon(Icons.close),
+ onPressed: () => Navigator.pop(context),
+ )
+ ],
+ ),
+ const SizedBox(height: 20),
+ if (_allTags.isEmpty)
+ const Text("No tags available."),
+
+ 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);
+ }
+ });
+ this.setState(() {
+ _applyFilter();
+ });
+ },
+ labelStyle: TextStyle(
+ fontFamily: 'GeneralSans',
+ color: isSelected ? Colors.white : null,
+ fontSize: 12,
+ fontWeight: FontWeight.w500,
+ ),
+ backgroundColor: Colors.grey[200],
+ selectedColor: Colors.black87,
+ checkmarkColor: Colors.white,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(20),
+ side: BorderSide.none,
+ ),
+ );
+ }).toList(),
+ ),
+ ),
+ ),
+ const SizedBox(height: 16),
+ Row(
+ children: [
+ Expanded(
+ child: TextButton(
+ onPressed: () {
+ this.setState(() {
+ _selectedTags.clear();
+ _applyFilter();
+ });
+ Navigator.pop(context);
+ },
+ child: const Text("Clear All", style: TextStyle(color: Colors.red)),
+ ),
+ ),
+ Expanded(
+ child: ElevatedButton(
+ onPressed: () => Navigator.pop(context),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.black,
+ foregroundColor: Colors.white,
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
+ ),
+ child: const Text("Done"),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+ );
+ },
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ if (_isLoading) return const Center(child: CircularProgressIndicator());
+
+ final leftColumn = <Widget>[];
+ final rightColumn = <Widget>[];
+
+ for (int i = 0; i < _filteredImages.length; i++) {
+ final item = _buildImageItem(_filteredImages[i], index: i);
+ if (i % 2 == 0) {
+ leftColumn.add(item);
+ } else {
+ rightColumn.add(item);
+ }
+ }
+
+ return Column(
+ children: [
+ Padding(
+ padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
+ child: Row(
+ children: [
+ Text(
+ "ALL IMAGES (${_filteredImages.length})",
+ style: TextStyle(
+ fontFamily: 'GeneralSans',
+ fontWeight: FontWeight.bold,
+ fontSize: 12,
+ letterSpacing: 1.2,
+ color: Colors.grey[600],
+ ),
+ ),
+ const Spacer(),
+ if (_selectedTags.isNotEmpty)
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
+ decoration: BoxDecoration(
+ color: Colors.black,
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Text(
+ "${_selectedTags.length} Filters",
+ style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold),
+ ),
+ )
+ ],
+ ),
+ ),
+ Expanded(
+ child: _filteredImages.isEmpty
+ ? Center(child: Text("No images found", style: TextStyle(color: Colors.grey[500])))
+ : SingleChildScrollView(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ 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()),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildImageItem(ImageModel image, {required int index}) {
+ return GestureDetector(
+ onTap: () {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (_) => ImageDetailsPage(
+ imagePath: image.filePath,
+ imageId: image.id,
+ projectId: widget.projectId,
+ ),
+ ),
+ );
+ },
+ child: Container(
+ height: (index % 3 == 0) ? 240 : 180,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(16),
+ color: Colors.grey[200],
+ ),
+ clipBehavior: Clip.antiAlias,
+ child: Stack(
+ fit: StackFit.expand,
+ children: [
+ Image.file(
+ File(image.filePath),
+ fit: BoxFit.cover,
+ width: double.infinity,
+ errorBuilder: (_,__,___) => const Center(child: Icon(Icons.broken_image, color: Colors.grey)),
+ ),
+ if (image.tags.isNotEmpty)
+ Positioned(
+ 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],
+ ),
+ ),
+ 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(),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+\ No newline at end of file
diff --git a/lib/ui/pages/project_tag_page.dart b/lib/ui/pages/project_tag_page.dart
@@ -1,49 +1,84 @@
import 'dart:io';
import 'package:flutter/material.dart';
+import 'package:image_picker/image_picker.dart';
import '../../data/models/image_model.dart';
-import '../../services/image_service.dart';
-import 'image_details_page.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
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();
}
class _ProjectTagPageState extends State<ProjectTagPage> {
- final ImageService _imageService = ImageService();
+ final _imageRepo = ImageRepo();
+ final _projectRepo = ProjectRepo();
+ final ImagePicker _picker = ImagePicker();
+
List<ImageModel> _images = [];
+ String _projectName = "Project"; // Default
bool _isLoading = true;
@override
void initState() {
super.initState();
- _loadImages();
+ _loadData();
}
- Future<void> _loadImages() async {
- final allImages = await _imageService.getImages(widget.projectId);
+ Future<void> _loadData() async {
+ // 1. Fetch Images
+ 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 filtered =
- allImages.where((img) {
- if (widget.tag == 'Uncategorized') {
- return img.tags.isEmpty;
- }
- 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;
});
}
}
+ Future<void> _pickAndRedirect() async {
+ try {
+ final XFile? pickedFile = await _picker.pickImage(source: ImageSource.gallery);
+ if (pickedFile != null) {
+ if (!mounted) return;
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (_) => ImageSavePage(
+ imagePaths: [pickedFile.path],
+ projectId: widget.projectId,
+ projectName: _projectName,
+ isFromShare: false,
+ ),
+ ),
+ ).then((_) => _loadData()); // Refresh upon return
+ }
+ } catch (e) {
+ debugPrint("Error picking image: $e");
+ }
+ }
+
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
@@ -64,93 +99,77 @@ class _ProjectTagPageState extends State<ProjectTagPage> {
backgroundColor: theme.appBarTheme.backgroundColor,
elevation: 0,
),
- body:
- _isLoading
- ? const Center(child: CircularProgressIndicator())
- : _images.isEmpty
+ floatingActionButton: FloatingActionButton(
+ onPressed: _pickAndRedirect,
+ backgroundColor: isDark ? Colors.white : Colors.black,
+ foregroundColor: isDark ? Colors.black : Colors.white,
+ child: const Icon(Icons.add_photo_alternate_outlined),
+ ),
+ body: _isLoading
+ ? const Center(child: CircularProgressIndicator())
+ : _images.isEmpty
? Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Icon(
- Icons.image_not_supported_outlined,
- size: 64,
- color: Colors.grey[400],
- ),
- const SizedBox(height: 16),
- Text(
- 'No images found for "${widget.tag}"',
- style: TextStyle(
- fontFamily: 'GeneralSans',
- fontSize: 16,
- color: Colors.grey[600],
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(Icons.image_not_supported_outlined,
+ size: 64, color: Colors.grey[400]),
+ const SizedBox(height: 16),
+ Text(
+ "No images found for '${widget.tag}'",
+ style: TextStyle(
+ fontFamily: 'GeneralSans',
+ fontSize: 16,
+ color: Colors.grey[600],
+ ),
),
- ),
- ],
- ),
- )
+ ],
+ ),
+ )
: 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: () {
- Navigator.push(
- context,
- MaterialPageRoute(
- builder:
- (_) => ImageDetailsPage(
- imagePath: image.filePath,
- imageId: image.id,
- projectId: widget.projectId,
- ),
- ),
- ).then((_) => _loadImages());
- },
- 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,
+ 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,
+ ),
),
- ),
- clipBehavior: Clip.antiAlias,
- child: Stack(
- fit: StackFit.expand,
- children: [
- Hero(
- tag: 'image_${image.id}',
- child: Image.file(
+ 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,
- ),
- ),
- ),
+ errorBuilder: (_, __, ___) => Container(
+ color: Colors.grey[300],
+ child: const Center(
+ child: Icon(Icons.broken_image, color: Colors.grey),
+ ),
+ ),
),
- ),
- ],
+ ],
+ ),
),
- ),
- );
- },
- ),
+ );
+ },
+ ),
);
}
-}
+}
+\ No newline at end of file