creek

The AI Image Editor of 2030
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:
Mlib/data/database.dart | 2+-
Mlib/ui/pages/project_board_page.dart | 439++++++++++++++++++++++++++++++++++++++++---------------------------------------
Alib/ui/pages/project_board_page_alternate.dart | 315+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/ui/pages/project_tag_page.dart | 206+++++++++++++++++++++++++++++++++++++++++++------------------------------------
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