commit 8d56d21e4f205773b57c1a5f848906a4d8b21894
parent 5495fe41daaee683edf1b55682e2caf9e052b88f
Author: maydayv7 <maydayv7@gmail.com>
Date: Sat, 6 Dec 2025 04:06:15 +0530
Create project selector widget and settings page
Also modify test image analysis page and fix crash on back button
Diffstat:
9 files changed, 1094 insertions(+), 860 deletions(-)
diff --git a/lib/ui/pages/create_file_page.dart b/lib/ui/pages/create_file_page.dart
@@ -2,10 +2,9 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:creekui/services/project_service.dart';
-import 'package:creekui/services/image_service.dart';
-import 'package:creekui/data/models/project_model.dart';
import 'package:creekui/ui/styles/variables.dart';
import 'package:creekui/ui/widgets/search_bar.dart';
+import 'package:creekui/ui/widgets/project_selector.dart';
import 'canvas_page.dart';
import 'define_brand_page.dart';
@@ -137,7 +136,7 @@ class _CreateFilePageState extends State<CreateFilePage> {
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
- child: ProjectSelectionModal(
+ child: _ProjectSelectionModalContent(
scrollController: controller,
onProjectSelected: (id, title) {
setState(() {
@@ -179,18 +178,24 @@ class _CreateFilePageState extends State<CreateFilePage> {
@override
Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ final isDark = theme.brightness == Brightness.dark;
+
return Scaffold(
- backgroundColor: const Color(0xFFF5F5F7),
+ backgroundColor: theme.scaffoldBackgroundColor,
appBar: AppBar(
- backgroundColor: const Color(0xFFF5F5F7),
+ backgroundColor: theme.scaffoldBackgroundColor,
elevation: 0,
leading: IconButton(
- icon: const Icon(Icons.arrow_back, color: Colors.black),
+ icon: Icon(Icons.arrow_back, color: theme.colorScheme.onSurface),
onPressed: () => Navigator.pop(context),
),
title: Text(
'Create Files',
- style: Variables.headerStyle.copyWith(fontSize: 16),
+ style: Variables.headerStyle.copyWith(
+ fontSize: 16,
+ color: theme.colorScheme.onSurface,
+ ),
),
// Show project chooser only when ID not passed
@@ -201,7 +206,7 @@ class _CreateFilePageState extends State<CreateFilePage> {
child: TextButton.icon(
onPressed: _openProjectSelection,
style: TextButton.styleFrom(
- backgroundColor: Colors.white,
+ backgroundColor: isDark ? Colors.grey[800] : Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
@@ -215,11 +220,14 @@ class _CreateFilePageState extends State<CreateFilePage> {
? Icons.create_new_folder_outlined
: Icons.folder_open,
size: 18,
- color: Colors.black,
+ color: theme.colorScheme.onSurface,
),
label: Text(
_selectedProjectTitle,
- style: Variables.bodyStyle.copyWith(fontSize: 12),
+ style: Variables.bodyStyle.copyWith(
+ fontSize: 12,
+ color: theme.colorScheme.onSurface,
+ ),
),
),
),
@@ -243,7 +251,7 @@ class _CreateFilePageState extends State<CreateFilePage> {
child: Text(
'Canvas Sizes',
style: Variables.bodyStyle.copyWith(
- color: Colors.grey,
+ color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
fontSize: 15,
fontWeight: FontWeight.w600,
),
@@ -261,7 +269,8 @@ class _CreateFilePageState extends State<CreateFilePage> {
childAspectRatio: 0.75,
),
itemCount: _filteredPresets.length,
- itemBuilder: (_, i) => _buildPresetCard(_filteredPresets[i]),
+ itemBuilder:
+ (_, i) => _buildPresetCard(_filteredPresets[i], theme),
),
),
],
@@ -269,9 +278,10 @@ class _CreateFilePageState extends State<CreateFilePage> {
);
}
- // Card widget
- Widget _buildPresetCard(CanvasPreset preset) {
+ Widget _buildPresetCard(CanvasPreset preset, ThemeData theme) {
final bool isCustom = preset.name == 'Custom';
+ final isDark = theme.brightness == Brightness.dark;
+
return InkWell(
onTap:
() => _navigateToEditor(
@@ -281,8 +291,11 @@ class _CreateFilePageState extends State<CreateFilePage> {
borderRadius: BorderRadius.circular(12),
child: Container(
decoration: BoxDecoration(
- color: Colors.white,
+ color: theme.cardColor,
borderRadius: BorderRadius.circular(12),
+ border: Border.all(
+ color: isDark ? Variables.borderDark : Colors.transparent,
+ ),
),
padding: const EdgeInsets.all(8),
child: Column(
@@ -291,7 +304,7 @@ class _CreateFilePageState extends State<CreateFilePage> {
Expanded(
child: Container(
decoration: BoxDecoration(
- color: const Color(0xFFE0E7FF),
+ color: isDark ? Colors.grey[800] : const Color(0xFFE0E7FF),
borderRadius: BorderRadius.circular(8),
),
child: Center(
@@ -304,7 +317,7 @@ class _CreateFilePageState extends State<CreateFilePage> {
aspectRatio: preset.width / preset.height,
child: Container(
decoration: BoxDecoration(
- color: Colors.white,
+ color: theme.cardColor,
borderRadius: BorderRadius.circular(4),
boxShadow: [
BoxShadow(
@@ -335,11 +348,15 @@ class _CreateFilePageState extends State<CreateFilePage> {
style: Variables.bodyStyle.copyWith(
fontSize: 11,
fontWeight: FontWeight.w600,
+ color: theme.colorScheme.onSurface,
),
),
Text(
preset.displaySize,
- style: Variables.captionStyle.copyWith(fontSize: 9),
+ style: Variables.captionStyle.copyWith(
+ fontSize: 9,
+ color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
+ ),
),
],
),
@@ -364,167 +381,24 @@ class CanvasPreset {
});
}
-// --------------------------------------------------------------------------
-// --- PROJECT SELECTION MODAL ----------------------------------------------
-// --------------------------------------------------------------------------
-
-class ProjectItemViewModel {
- final ProjectModel item;
- final String? parentTitle;
- final String? coverPath;
-
- ProjectItemViewModel({required this.item, this.parentTitle, this.coverPath});
-
- String get title => item.title;
- bool get isEvent => item.isEvent;
- int get id => item.id!;
-}
-
-class ProjectGroup {
- final ProjectModel project;
- final List<ProjectItemViewModel> events;
- final String? coverPath;
- bool isExpanded;
-
- ProjectGroup({
- required this.project,
- this.events = const [],
- this.coverPath,
- this.isExpanded = false,
- });
-}
-
-class ProjectSelectionModal extends StatefulWidget {
+// Project Selection Modal
+class _ProjectSelectionModalContent extends StatefulWidget {
final Function(int id, String title) onProjectSelected;
final ScrollController scrollController;
- const ProjectSelectionModal({
- super.key,
+ const _ProjectSelectionModalContent({
required this.onProjectSelected,
required this.scrollController,
});
@override
- State<ProjectSelectionModal> createState() => _ProjectSelectionModalState();
+ State<_ProjectSelectionModalContent> createState() =>
+ _ProjectSelectionModalContentState();
}
-class _ProjectSelectionModalState extends State<ProjectSelectionModal> {
- final ProjectService _projectService = ProjectService();
- final ImageService _imageService = ImageService();
-
- List<ProjectItemViewModel> _recentViewModels = [];
- List<ProjectGroup> _groupedProjects = [];
- List<ProjectGroup> _filteredGroupedProjects = [];
-
- bool _isLoading = true;
- String _searchQuery = "";
-
- final TextEditingController _searchController = TextEditingController();
-
- @override
- void initState() {
- super.initState();
- _loadData();
- }
-
- @override
- void dispose() {
- _searchController.dispose();
- super.dispose();
- }
-
- Future<String?> _getProjectCover(int projectId) async {
- try {
- final images = await _imageService.getImages(projectId);
- if (images.isNotEmpty) {
- return images.first.filePath;
- }
- } catch (_) {}
- return null;
- }
-
- Future<void> _loadData() async {
- setState(() => _isLoading = true);
-
- final recent = await _projectService.getRecentProjectsAndEvents();
- final allProjects = await _projectService.getAllProjects();
-
- // Build Recent
- final List<ProjectItemViewModel> recents = [];
- for (var item in recent.take(3)) {
- String? parentTitle;
- if (item.parentId != null) {
- final parent = await _projectService.getProjectById(item.parentId!);
- parentTitle = parent?.title;
- }
- final cover = await _getProjectCover(item.id!);
- recents.add(
- ProjectItemViewModel(
- item: item,
- parentTitle: parentTitle,
- coverPath: cover,
- ),
- );
- }
- _recentViewModels = recents;
-
- // Build groups
- final List<ProjectGroup> groups = [];
- for (final p in allProjects) {
- final eventsRaw = await _projectService.getEvents(p.id!);
- final List<ProjectItemViewModel> events = [];
- for (final e in eventsRaw) {
- final cover = await _getProjectCover(e.id!);
- events.add(ProjectItemViewModel(item: e, coverPath: cover));
- }
- final cover = await _getProjectCover(p.id!);
- groups.add(ProjectGroup(project: p, events: events, coverPath: cover));
- }
-
- _groupedProjects = groups;
- _filteredGroupedProjects = groups;
-
- if (mounted) setState(() => _isLoading = false);
- }
-
- void _filterProjects(String query) {
- setState(() {
- _searchQuery = query;
- if (query.isEmpty) {
- _filteredGroupedProjects = _groupedProjects;
- return;
- }
- final q = query.toLowerCase();
- final List<ProjectGroup> filtered = [];
-
- for (final g in _groupedProjects) {
- final projectMatch = g.project.title.toLowerCase().contains(q);
- final matchingEvents =
- g.events.where((e) => e.title.toLowerCase().contains(q)).toList();
-
- if (projectMatch) {
- filtered.add(
- ProjectGroup(
- project: g.project,
- events: g.events,
- isExpanded: true,
- coverPath: g.coverPath,
- ),
- );
- } else if (matchingEvents.isNotEmpty) {
- filtered.add(
- ProjectGroup(
- project: g.project,
- events: matchingEvents,
- isExpanded: true,
- coverPath: g.coverPath,
- ),
- );
- }
- }
- _filteredGroupedProjects = filtered;
- });
- }
+class _ProjectSelectionModalContentState
+ extends State<_ProjectSelectionModalContent> {
+ Key _selectorKey = UniqueKey();
Future<void> _createNewProject() async {
final result = await Navigator.push(
@@ -534,6 +408,11 @@ class _ProjectSelectionModalState extends State<ProjectSelectionModal> {
if (result != null && result is Map) {
widget.onProjectSelected(result["id"], result["title"]);
+ } else {
+ // If project is created but not selected immediately, refreshing the list is a safe bet.
+ setState(() {
+ _selectorKey = UniqueKey();
+ });
}
}
@@ -559,135 +438,33 @@ class _ProjectSelectionModalState extends State<ProjectSelectionModal> {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- const Text(
+ Text(
"Select Destination",
- style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
+ style: Variables.headerStyle.copyWith(fontSize: 18),
),
IconButton(
icon: const Icon(Icons.add),
onPressed: _createNewProject,
+ tooltip: "Create New Project",
),
],
),
),
-
- // Search Bar
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
- child: CommonSearchBar(
- controller: _searchController,
- hintText: "Search Projects",
- onChanged: _filterProjects,
- ),
- ),
-
- Divider(color: Colors.grey[300]),
-
+ Divider(color: Colors.grey[300], height: 1),
+ // Selector
Expanded(
- child:
- _isLoading
- ? const Center(child: CircularProgressIndicator())
- : ListView(
- controller: widget.scrollController,
- children: [
- if (_searchQuery.isEmpty && _recentViewModels.isNotEmpty)
- const Padding(
- padding: EdgeInsets.only(left: 16, bottom: 8),
- child: Text(
- "Recent Projects/Events",
- style: TextStyle(fontSize: 14),
- ),
- ),
- if (_searchQuery.isEmpty)
- ..._recentViewModels.map(_buildRecentItem),
-
- const Padding(
- padding: EdgeInsets.only(left: 16, top: 8, bottom: 8),
- child: Text(
- "All Projects/Events",
- style: TextStyle(fontSize: 14),
- ),
- ),
- ..._filteredGroupedProjects.map(_buildProjectGroup),
- ],
- ),
- ),
- ],
- );
- }
-
- Widget _buildRecentItem(ProjectItemViewModel vm) {
- return InkWell(
- onTap: () => widget.onProjectSelected(vm.id, vm.title),
- child: ListTile(
- leading: _thumbnail(vm.coverPath),
- title: Text(vm.title),
- subtitle: vm.parentTitle != null ? Text(vm.parentTitle!) : null,
- ),
- );
- }
-
- Widget _thumbnail(String? path) {
- return Container(
- width: 48,
- height: 48,
- decoration: BoxDecoration(
- color: Colors.grey[200],
- borderRadius: BorderRadius.circular(8),
- image:
- path != null
- ? DecorationImage(
- image: FileImage(File(path)),
- fit: BoxFit.cover,
- )
- : null,
- ),
- child: path == null ? Icon(Icons.image, color: Colors.grey[400]) : null,
- );
- }
-
- Widget _buildProjectGroup(ProjectGroup g) {
- final hasEvents = g.events.isNotEmpty;
-
- return Container(
- margin: const EdgeInsets.only(bottom: 8),
- child: Column(
- children: [
- ListTile(
- onTap: () {
- if (hasEvents) {
- setState(() => g.isExpanded = !g.isExpanded);
- } else {
- widget.onProjectSelected(g.project.id!, g.project.title);
- }
+ child: ProjectSelector(
+ key: _selectorKey,
+ scrollController: widget.scrollController,
+ searchHint: "Search Projects",
+ onProjectSelected: (id, title, parentTitle) {
+ final displayTitle =
+ parentTitle != null ? "$parentTitle / $title" : title;
+ widget.onProjectSelected(id, displayTitle);
},
- leading: _thumbnail(g.coverPath),
- title: Text(g.project.title),
- trailing:
- hasEvents
- ? Icon(
- g.isExpanded
- ? Icons.keyboard_arrow_up
- : Icons.keyboard_arrow_down,
- )
- : null,
),
- if (hasEvents && g.isExpanded)
- Container(
- color: Colors.grey[100],
- child: Column(
- children:
- g.events.map((e) {
- return ListTile(
- onTap: () => widget.onProjectSelected(e.id, e.title),
- leading: _thumbnail(e.coverPath),
- title: Text(e.title),
- );
- }).toList(),
- ),
- ),
- ],
- ),
+ ),
+ ],
);
}
}
diff --git a/lib/ui/pages/image_analysis_page.dart b/lib/ui/pages/image_analysis_page.dart
@@ -5,7 +5,6 @@ import 'package:image_picker/image_picker.dart';
import 'package:path_provider/path_provider.dart';
import 'package:creekui/services/analyze/image_analyzer.dart';
import 'package:creekui/ui/styles/variables.dart';
-import 'package:creekui/ui/widgets/empty_state.dart';
class ImageAnalysisPage extends StatefulWidget {
const ImageAnalysisPage({super.key});
@@ -22,6 +21,19 @@ class _ImageAnalysisPageState extends State<ImageAnalysisPage> {
final ImagePicker _picker = ImagePicker();
+ final Set<String> _selectedTags = {};
+ final List<String> _availableTags = [
+ 'Compositions',
+ 'Colours',
+ 'Texture',
+ 'Style',
+ 'Emotion',
+ 'Lighting',
+ 'Era',
+ 'Fonts',
+ 'Subject',
+ ];
+
Future<void> _pickImage(ImageSource source) async {
try {
final XFile? image = await _picker.pickImage(source: source);
@@ -31,8 +43,6 @@ class _ImageAnalysisPageState extends State<ImageAnalysisPage> {
_analysisResult = null;
_errorMessage = null;
});
- // Auto-run analysis when a new image is picked
- _runAnalysis(image.path);
}
} catch (e) {
if (mounted) {
@@ -44,7 +54,9 @@ class _ImageAnalysisPageState extends State<ImageAnalysisPage> {
}
}
- Future<void> _runAnalysis(String sourcePath) async {
+ Future<void> _runAnalysis() async {
+ if (_selectedImage == null) return;
+
setState(() {
_isAnalyzing = true;
_errorMessage = null;
@@ -52,12 +64,21 @@ class _ImageAnalysisPageState extends State<ImageAnalysisPage> {
try {
final appDir = await getApplicationDocumentsDirectory();
- final fileName = sourcePath.split('/').last;
+ final fileName = _selectedImage!.path.split('/').last;
final targetPath = '${appDir.path}/$fileName';
- final file = File(sourcePath);
+
+ final file = File(_selectedImage!.path);
await file.copy(targetPath);
- final result = await ImageAnalyzerService.analyzeFullSuite(targetPath);
+ Map<String, dynamic> result;
+ if (_selectedTags.isEmpty) {
+ result = await ImageAnalyzerService.analyzeFullSuite(targetPath);
+ } else {
+ result = await ImageAnalyzerService.analyzeSelected(
+ targetPath,
+ _selectedTags.toList(),
+ );
+ }
if (mounted) {
setState(() {
@@ -126,6 +147,7 @@ class _ImageAnalysisPageState extends State<ImageAnalysisPage> {
@override
Widget build(BuildContext context) {
+ final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
@@ -135,16 +157,20 @@ class _ImageAnalysisPageState extends State<ImageAnalysisPage> {
icon: const Icon(Icons.arrow_back, color: Colors.black),
onPressed: () => Navigator.pop(context),
),
- title: Text(
+ title: const Text(
"Image Analysis",
- style: Variables.headerStyle.copyWith(fontSize: 20),
+ style: TextStyle(
+ color: Colors.black,
+ fontFamily: 'GeneralSans',
+ fontWeight: FontWeight.bold,
+ ),
),
actions: [
- if (_selectedImage != null)
+ if (_selectedImage != null && !_isAnalyzing)
IconButton(
- icon: const Icon(Icons.refresh, color: Colors.black),
- tooltip: "Re-analyze",
- onPressed: () => _runAnalysis(_selectedImage!.path),
+ icon: const Icon(Icons.play_arrow, color: Variables.textPrimary),
+ tooltip: "Run Analysis",
+ onPressed: _runAnalysis,
),
],
),
@@ -159,7 +185,7 @@ class _ImageAnalysisPageState extends State<ImageAnalysisPage> {
width: double.infinity,
height: 250,
decoration: BoxDecoration(
- color: Variables.borderSubtle,
+ color: Colors.grey[100],
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
@@ -177,10 +203,23 @@ class _ImageAnalysisPageState extends State<ImageAnalysisPage> {
if (_selectedImage != null)
Image.file(_selectedImage!, fit: BoxFit.contain)
else
- const EmptyState(
- icon: Icons.bug_report_outlined,
- title: "No image selected",
- subtitle: "Select an image to test full suite",
+ Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(
+ Icons.bug_report_outlined,
+ size: 48,
+ color: Colors.grey[400],
+ ),
+ const SizedBox(height: 12),
+ Text(
+ "Select image to analyze",
+ style: TextStyle(
+ fontFamily: 'GeneralSans',
+ color: Colors.grey[500],
+ ),
+ ),
+ ],
),
if (_isAnalyzing)
Container(
@@ -198,6 +237,80 @@ class _ImageAnalysisPageState extends State<ImageAnalysisPage> {
),
const SizedBox(height: 24),
+ // Tag Selector
+ Text(
+ "Select Analysis Modules",
+ style: Variables.headerStyle.copyWith(fontSize: 16),
+ ),
+ const SizedBox(height: 12),
+ Wrap(
+ spacing: 8,
+ runSpacing: 8,
+ children:
+ _availableTags.map((tag) {
+ final isSelected = _selectedTags.contains(tag);
+ return GestureDetector(
+ onTap: () {
+ setState(() {
+ if (isSelected) {
+ _selectedTags.remove(tag);
+ } else {
+ _selectedTags.add(tag);
+ }
+ });
+ },
+ child: Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 12,
+ vertical: 8,
+ ),
+ decoration: BoxDecoration(
+ color:
+ isSelected
+ ? const Color(0xFFEEF0FF)
+ : Colors.white,
+ borderRadius: BorderRadius.circular(20),
+ border: Border.all(
+ color:
+ isSelected
+ ? const Color(0xFF7C4DFF)
+ : Colors.grey[300]!,
+ width: 1.0,
+ ),
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ if (isSelected) ...[
+ const Icon(
+ Icons.check,
+ size: 14,
+ color: Color(0xFF7C4DFF),
+ ),
+ const SizedBox(width: 4),
+ ],
+ Text(
+ tag,
+ style: TextStyle(
+ fontSize: 13,
+ fontWeight:
+ isSelected
+ ? FontWeight.w600
+ : FontWeight.normal,
+ color:
+ isSelected
+ ? const Color(0xFF7C4DFF)
+ : Colors.black87,
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }).toList(),
+ ),
+ const SizedBox(height: 24),
+
// Error Message
if (_errorMessage != null)
Container(
@@ -219,9 +332,13 @@ class _ImageAnalysisPageState extends State<ImageAnalysisPage> {
// Raw JSON Result
if (_analysisResult != null) ...[
- Text(
+ const Text(
"Results",
- style: Variables.headerStyle.copyWith(fontSize: 18),
+ style: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.bold,
+ fontFamily: 'GeneralSans',
+ ),
),
const SizedBox(height: 12),
_buildJsonViewer(_analysisResult!),
@@ -232,8 +349,8 @@ class _ImageAnalysisPageState extends State<ImageAnalysisPage> {
),
floatingActionButton: FloatingActionButton.extended(
onPressed: _showSourceSelector,
- backgroundColor: Variables.textPrimary,
- foregroundColor: Colors.white,
+ backgroundColor: colorScheme.primary,
+ foregroundColor: colorScheme.onPrimary,
icon: const Icon(Icons.add_photo_alternate),
label: Text(
_selectedImage == null ? "Select Image" : "Change Image",
diff --git a/lib/ui/pages/project_detail_page.dart b/lib/ui/pages/project_detail_page.dart
@@ -9,6 +9,8 @@ import 'package:creekui/services/project_service.dart';
import 'package:creekui/ui/styles/variables.dart';
import 'package:creekui/ui/widgets/empty_state.dart';
import 'package:creekui/ui/widgets/section_header.dart';
+import 'package:creekui/ui/pages/settings_page.dart';
+import 'package:creekui/ui/pages/home_page.dart';
import 'project_board_page.dart';
import 'stylesheet_page.dart';
import 'project_file_page.dart';
@@ -305,8 +307,26 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> {
title: Text(_project!.title, style: Variables.headerStyle),
backgroundColor: const Color(0xFFFAFAFA),
elevation: 0,
- leadingWidth: 40,
- titleSpacing: 4,
+ leadingWidth: 50,
+ titleSpacing: 0,
+ automaticallyImplyLeading: false,
+ leading: IconButton(
+ icon: const Icon(
+ Icons.arrow_back,
+ size: 20,
+ color: Variables.textPrimary,
+ ),
+ onPressed: () {
+ if (Navigator.canPop(context)) {
+ Navigator.pop(context);
+ } else {
+ Navigator.pushReplacement(
+ context,
+ MaterialPageRoute(builder: (_) => const HomePage()),
+ );
+ }
+ },
+ ),
actions: [
IconButton(
icon: SvgPicture.asset(
@@ -318,7 +338,12 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> {
BlendMode.srcIn,
),
),
- onPressed: () {},
+ onPressed: () {
+ Navigator.push(
+ context,
+ MaterialPageRoute(builder: (_) => const SettingsPage()),
+ );
+ },
),
],
),
diff --git a/lib/ui/pages/settings_page.dart b/lib/ui/pages/settings_page.dart
@@ -0,0 +1,49 @@
+import 'package:flutter/material.dart';
+import 'package:creekui/ui/styles/variables.dart';
+import 'package:creekui/ui/pages/image_analysis_page.dart';
+
+class SettingsPage extends StatelessWidget {
+ const SettingsPage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: Colors.white,
+ appBar: AppBar(
+ title: const Text(
+ "Settings",
+ style: TextStyle(
+ fontFamily: 'GeneralSans',
+ color: Variables.textPrimary,
+ ),
+ ),
+ backgroundColor: Colors.white,
+ elevation: 0,
+ iconTheme: const IconThemeData(color: Variables.textPrimary),
+ ),
+ body: ListView(
+ children: [
+ ListTile(
+ title: const Text(
+ "Test Image Analysis",
+ style: TextStyle(
+ fontFamily: 'GeneralSans',
+ color: Variables.textPrimary,
+ ),
+ ),
+ trailing: const Icon(
+ Icons.chevron_right,
+ color: Variables.textSecondary,
+ ),
+ onTap: () {
+ Navigator.push(
+ context,
+ MaterialPageRoute(builder: (_) => const ImageAnalysisPage()),
+ );
+ },
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/ui/pages/share_to_file_page.dart b/lib/ui/pages/share_to_file_page.dart
@@ -9,8 +9,8 @@ import 'package:creekui/data/models/project_model.dart';
import 'package:creekui/ui/styles/variables.dart';
import 'package:creekui/ui/widgets/search_bar.dart';
import 'package:creekui/ui/widgets/file_card.dart';
-import 'package:creekui/ui/widgets/empty_state.dart';
import 'package:creekui/ui/widgets/section_header.dart';
+import 'package:creekui/ui/widgets/empty_state.dart';
import 'create_file_page.dart';
import 'canvas_page.dart';
@@ -30,6 +30,8 @@ class _ShareToFilePageState extends State<ShareToFilePage> {
List<FileModel> _allFiles = [];
List<FileModel> _filteredFiles = [];
List<FileModel> _recentFiles = [];
+
+ // file.id -> { 'preview': path, 'dimensions': str }
Map<String, Map<String, String>> _fileMetadata = {};
// Avoid repeated fetches
@@ -69,7 +71,7 @@ class _ShareToFilePageState extends State<ShareToFilePage> {
await _loadFileMetadata(files);
setState(() => _isLoading = false);
} catch (e) {
- debugPrint('Error fetching files: $e');
+ debugPrint('Error fetching files in ShareToFilePage: $e');
setState(() => _isLoading = false);
}
}
@@ -90,7 +92,8 @@ class _ShareToFilePageState extends State<ShareToFilePage> {
String dims = 'Unknown';
if (data is Map) {
- if (data['preview_path'] != null) {
+ if (data['preview_path'] != null &&
+ data['preview_path'].toString().isNotEmpty) {
preview = data['preview_path'].toString();
// If preview is relative, try to resolve relative to JSON file
if (!File(preview).existsSync()) {
@@ -100,11 +103,15 @@ class _ShareToFilePageState extends State<ShareToFilePage> {
}
}
if (data['width'] != null && data['height'] != null) {
- dims = '${data['width']} x ${data['height']} px';
+ final w = (data['width'] as num).toInt();
+ final h = (data['height'] as num).toInt();
+ dims = '$w x $h px';
}
}
meta[fmodel.id] = {'preview': preview, 'dimensions': dims};
- } catch (_) {}
+ } catch (e) {
+ debugPrint('Error parsing canvas json for ${fmodel.id}: $e');
+ }
} else {
// Regular image file - assign path and try
String dims = 'Unknown';
@@ -112,10 +119,14 @@ class _ShareToFilePageState extends State<ShareToFilePage> {
final bytes = await f.readAsBytes();
final image = img.decodeImage(bytes);
if (image != null) dims = '${image.width} x ${image.height} px';
- } catch (_) {}
+ } catch (_) {
+ // ignore
+ }
meta[fmodel.id] = {'preview': fmodel.filePath, 'dimensions': dims};
}
- } catch (_) {}
+ } catch (e) {
+ debugPrint('Error while loading metadata for ${fmodel.id}: $e');
+ }
}
_fileMetadata = meta;
}
@@ -131,7 +142,8 @@ class _ShareToFilePageState extends State<ShareToFilePage> {
_allFiles.where((file) {
final nameMatch = file.name.toLowerCase().contains(q);
final breadcrumb = _getProjectBreadcrumbSync(file).toLowerCase();
- return nameMatch || breadcrumb.contains(q);
+ final projectMatch = breadcrumb.contains(q);
+ return nameMatch || projectMatch;
}).toList();
}
});
@@ -154,36 +166,63 @@ class _ShareToFilePageState extends State<ShareToFilePage> {
try {
final p = await _projectService.getProjectById(file.projectId);
if (p != null) _projectCache[file.projectId] = p;
- } catch (_) {}
+ } catch (e) {
+ debugPrint('Project load failed for ${file.projectId}: $e');
+ }
}
final project = _projectCache[file.projectId];
if (project == null) return "Unknown";
- if (project.parentId == null) return project.title;
- if (!_projectCache.containsKey(project.parentId!)) {
+ if (project.parentId == null) {
+ return project.title;
+ }
+
+ final parentId = project.parentId!;
+ if (!_projectCache.containsKey(parentId)) {
try {
- final parent = await _projectService.getProjectById(project.parentId!);
- if (parent != null) _projectCache[project.parentId!] = parent;
- } catch (_) {}
+ final parent = await _projectService.getProjectById(parentId);
+ if (parent != null) _projectCache[parentId] = parent;
+ } catch (e) {
+ debugPrint('Parent project load failed for $parentId: $e');
+ }
}
- final parent = _projectCache[project.parentId];
- return parent == null
- ? project.title
- : "${parent.title} / ${project.title}";
+
+ final parentProject = _projectCache[parentId];
+ if (parentProject == null) return project.title;
+ return "${parentProject.title} / ${project.title}";
+ }
+
+ void _onAddPressed() {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (_) => CreateFilePage(file: widget.sharedImage),
+ ),
+ );
}
void _onFileSelected(FileModel file) async {
try {
final f = File(file.filePath);
- if (!await f.exists()) return;
+ if (!await f.exists()) {
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(const SnackBar(content: Text("File not found")));
+ return;
+ }
+
+ double width = 1080;
+ double height = 1080;
- double width = 1080, height = 1080;
if (file.filePath.toLowerCase().endsWith('.json')) {
final content = await f.readAsString();
final data = jsonDecode(content);
- if (data is Map && data['width'] != null) {
- width = (data['width'] as num).toDouble();
- height = (data['height'] as num).toDouble();
+
+ if (data is Map) {
+ if (data['width'] != null && data['height'] != null) {
+ width = (data['width'] as num).toDouble();
+ height = (data['height'] as num).toDouble();
+ }
}
}
@@ -213,31 +252,54 @@ class _ShareToFilePageState extends State<ShareToFilePage> {
return '${date.day}/${date.month}/${date.year}';
}
+ // Fallback to original filePath if preview path empty/missing
+ String _resolvePreviewPath(FileModel file) {
+ final meta = _fileMetadata[file.id];
+ if (meta == null) return file.filePath;
+ final preview = meta['preview'] ?? '';
+ if (preview.isNotEmpty && File(preview).existsSync()) return preview;
+
+ if (file.filePath.toLowerCase().endsWith('.json')) {
+ final f = File(file.filePath);
+ final base = f.uri.pathSegments.last;
+ final nameWithoutExt = base.split('.').first;
+ final parent = f.parent;
+ final candidates = [
+ '${parent.path}/$nameWithoutExt.png',
+ '${parent.path}/$nameWithoutExt.jpg',
+ '${parent.path}/preview_$nameWithoutExt.png',
+ ];
+ for (final c in candidates) {
+ if (File(c).existsSync()) return c;
+ }
+ }
+ if (File(file.filePath).existsSync()) return file.filePath;
+ return '';
+ }
+
@override
Widget build(BuildContext context) {
+ final theme = Theme.of(context);
return Scaffold(
- backgroundColor: Variables.background,
+ backgroundColor: theme.scaffoldBackgroundColor,
appBar: AppBar(
- backgroundColor: Variables.background,
+ backgroundColor: theme.scaffoldBackgroundColor,
elevation: 0,
leading: IconButton(
- icon: const Icon(Icons.arrow_back, color: Colors.black),
+ icon: Icon(Icons.arrow_back, color: theme.colorScheme.onSurface),
onPressed: () => Navigator.pop(context),
),
title: Text(
'Files',
- style: Variables.headerStyle.copyWith(fontSize: 20),
+ style: Variables.headerStyle.copyWith(
+ color: theme.colorScheme.onSurface,
+ ),
),
actions: [
IconButton(
- icon: const Icon(Icons.add, color: Colors.black, size: 28),
- onPressed:
- () => Navigator.push(
- context,
- MaterialPageRoute(
- builder: (_) => CreateFilePage(file: widget.sharedImage),
- ),
- ),
+ icon: Icon(Icons.add, color: theme.colorScheme.onSurface, size: 28),
+ onPressed: _onAddPressed,
+ tooltip: "Create New File",
),
],
),
@@ -245,13 +307,10 @@ class _ShareToFilePageState extends State<ShareToFilePage> {
_isLoading
? const Center(child: CircularProgressIndicator())
: _allFiles.isEmpty
- ? const EmptyState(
- icon: Icons.folder_open,
- title: "No files yet",
- subtitle: "Tap + to create your first file",
- )
+ ? _buildEmptyState()
: Column(
children: [
+ // Search bar
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: CommonSearchBar(
@@ -264,15 +323,24 @@ class _ShareToFilePageState extends State<ShareToFilePage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
+ if (_filteredFiles.isEmpty && _searchQuery.isNotEmpty)
+ const Padding(
+ padding: EdgeInsets.all(32.0),
+ child: EmptyState(
+ icon: Icons.search_off,
+ title: "No results found",
+ subtitle: "Try adjusting your search",
+ ),
+ ),
+
+ // Recent Files
if (_recentFiles.isNotEmpty &&
_searchQuery.isEmpty) ...[
const Padding(
- padding: EdgeInsets.symmetric(
- horizontal: 16,
- vertical: 8,
- ),
+ padding: EdgeInsets.symmetric(horizontal: 16),
child: SectionHeader(title: "Recent Files"),
),
+ const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
@@ -286,29 +354,35 @@ class _ShareToFilePageState extends State<ShareToFilePage> {
),
const SizedBox(height: 24),
],
- Padding(
- padding: const EdgeInsets.symmetric(
- horizontal: 16,
- vertical: 8,
- ),
- child: SectionHeader(
- title:
- _searchQuery.isEmpty
- ? "All Files"
- : "Search Results",
+
+ // All files header
+ if (_filteredFiles.isNotEmpty) ...[
+ Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ ),
+ child: SectionHeader(
+ title:
+ _searchQuery.isEmpty
+ ? "All Files"
+ : "Search Results",
+ ),
),
- ),
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: ListView.builder(
- shrinkWrap: true,
- physics: const NeverScrollableScrollPhysics(),
- itemCount: _filteredFiles.length,
- itemBuilder:
- (context, index) =>
- _buildFileItem(_filteredFiles[index]),
+ const SizedBox(height: 12),
+ Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ ),
+ child: ListView.builder(
+ shrinkWrap: true,
+ physics: const NeverScrollableScrollPhysics(),
+ itemCount: _filteredFiles.length,
+ itemBuilder:
+ (context, index) =>
+ _buildFileCard(_filteredFiles[index]),
+ ),
),
- ),
+ ],
const SizedBox(height: 40),
],
),
@@ -319,18 +393,42 @@ class _ShareToFilePageState extends State<ShareToFilePage> {
);
}
+ Widget _buildEmptyState() {
+ return Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(Icons.folder_open, size: 80, color: Colors.grey[300]),
+ const SizedBox(height: 16),
+ Text(
+ "No files yet",
+ style: TextStyle(color: Colors.grey[500], fontSize: 16),
+ ),
+ const SizedBox(height: 8),
+ TextButton(
+ onPressed: _onAddPressed,
+ child: const Text("Create your first file"),
+ ),
+ ],
+ ),
+ );
+ }
+
Widget _buildFileCard(FileModel file) {
+ final preview = _resolvePreviewPath(file);
+ final meta = _fileMetadata[file.id] ?? {};
+ final dimensions = meta['dimensions'] ?? 'Unknown';
+
return FutureBuilder<String>(
future: _getProjectEventLabel(file),
builder: (context, snapshot) {
- final meta = _fileMetadata[file.id] ?? {};
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: FileCard(
file: file,
breadcrumb: snapshot.data ?? "",
- dimensions: meta['dimensions'] ?? "Unknown",
- previewPath: meta['preview'] ?? "",
+ dimensions: dimensions,
+ previewPath: preview,
timeAgo: _formatDate(file.lastUpdated),
onTap: () => _onFileSelected(file),
onMenuAction: null,
@@ -339,6 +437,4 @@ class _ShareToFilePageState extends State<ShareToFilePage> {
},
);
}
-
- Widget _buildFileItem(FileModel file) => _buildFileCard(file);
}
diff --git a/lib/ui/pages/share_to_moodboard_page.dart b/lib/ui/pages/share_to_moodboard_page.dart
@@ -1,38 +1,11 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
-import 'package:creekui/data/models/project_model.dart';
-import 'package:creekui/data/repos/project_repo.dart';
-import 'package:creekui/data/repos/image_repo.dart';
import 'package:creekui/services/project_service.dart';
import 'package:creekui/ui/styles/variables.dart';
-import 'package:creekui/ui/widgets/search_bar.dart';
-import 'package:creekui/ui/widgets/section_header.dart';
+import 'package:creekui/ui/widgets/project_selector.dart';
import 'image_save_page.dart';
-class ProjectItemViewModel {
- final ProjectModel item;
- final String? parentTitle;
- final String? coverPath;
- ProjectItemViewModel({required this.item, this.parentTitle, this.coverPath});
- String get title => item.title;
- bool get isEvent => item.isEvent;
- int get id => item.id!;
-}
-
-class ProjectGroup {
- final ProjectModel project;
- final List<ProjectItemViewModel> events;
- final String? coverPath;
- bool isExpanded;
- ProjectGroup({
- required this.project,
- this.events = const [],
- this.coverPath,
- this.isExpanded = false,
- });
-}
-
class ShareToMoodboardPage extends StatefulWidget {
final List<File> imageFiles;
const ShareToMoodboardPage({super.key, required this.imageFiles});
@@ -42,118 +15,8 @@ class ShareToMoodboardPage extends StatefulWidget {
}
class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> {
- final ProjectRepo _projectRepo = ProjectRepo();
- final ImageRepo _imageRepo = ImageRepo();
final ProjectService _projectService = ProjectService();
-
- List<ProjectItemViewModel> _recentViewModels = [];
- List<ProjectGroup> _groupedProjects = [];
- List<ProjectGroup> _filteredGroupedProjects = [];
-
- bool _isLoading = true;
- String _searchQuery = "";
- final TextEditingController _searchController = TextEditingController();
-
- @override
- void initState() {
- super.initState();
- _loadData();
- }
-
- @override
- void dispose() {
- _searchController.dispose();
- super.dispose();
- }
-
- Future<String?> _getProjectCover(int projectId) async {
- try {
- final images = await _imageRepo.getImages(projectId);
- if (images.isNotEmpty) return images.first.filePath;
- } catch (_) {}
- return null;
- }
-
- Future<void> _loadData() async {
- setState(() => _isLoading = true);
-
- // 1. Fetch Raw Data
- final recentItems = await _projectRepo.getRecentProjectsAndEvents();
- final allProjects = await _projectRepo.getAllProjects();
-
- // 2. Build Recent View Models
- final List<ProjectItemViewModel> recents = [];
- for (var item in recentItems.take(3)) {
- String? parentTitle;
- if (item.parentId != null) {
- final parent = await _projectRepo.getProjectById(item.parentId!);
- parentTitle = parent?.title;
- }
- final cover = await _getProjectCover(item.id!);
- recents.add(
- ProjectItemViewModel(
- item: item,
- parentTitle: parentTitle,
- coverPath: cover,
- ),
- );
- }
- _recentViewModels = recents;
-
- // 3. Build Grouped Projects
- final List<ProjectGroup> groups = [];
- for (final p in allProjects) {
- final rawEvents = await _projectRepo.getEvents(p.id!);
- final List<ProjectItemViewModel> eventVMs = [];
- for (final e in rawEvents) {
- final eCover = await _getProjectCover(e.id!);
- eventVMs.add(ProjectItemViewModel(item: e, coverPath: eCover));
- }
- final pCover = await _getProjectCover(p.id!);
- groups.add(ProjectGroup(project: p, events: eventVMs, coverPath: pCover));
- }
-
- _groupedProjects = groups;
- _filteredGroupedProjects = groups;
- if (mounted) setState(() => _isLoading = false);
- }
-
- void _filterProjects(String query) {
- setState(() {
- _searchQuery = query;
- if (query.isEmpty) {
- _filteredGroupedProjects = _groupedProjects;
- } else {
- final q = query.toLowerCase();
- final List<ProjectGroup> filtered = [];
- for (final g in _groupedProjects) {
- final projectMatch = g.project.title.toLowerCase().contains(q);
- final matchingEvents =
- g.events.where((e) => e.title.toLowerCase().contains(q)).toList();
- if (projectMatch) {
- filtered.add(
- ProjectGroup(
- project: g.project,
- events: g.events,
- isExpanded: true,
- coverPath: g.coverPath,
- ),
- );
- } else if (matchingEvents.isNotEmpty) {
- filtered.add(
- ProjectGroup(
- project: g.project,
- events: matchingEvents,
- isExpanded: true,
- coverPath: g.coverPath,
- ),
- );
- }
- }
- _filteredGroupedProjects = filtered;
- }
- });
- }
+ Key _selectorKey = UniqueKey(); // Used to refresh list after creation
Future<void> _createNewProject() async {
final controller = TextEditingController();
@@ -161,7 +24,10 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> {
context: context,
builder:
(context) => AlertDialog(
- title: const Text("New Project"),
+ title: const Text(
+ "New Project",
+ style: TextStyle(fontFamily: 'GeneralSans'),
+ ),
content: TextField(
controller: controller,
decoration: const InputDecoration(hintText: "Project Title"),
@@ -183,7 +49,9 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> {
if (title != null && title.isNotEmpty) {
final newId = await _projectService.createProject(title);
- await _loadData();
+ setState(() {
+ _selectorKey = UniqueKey();
+ });
_navigateToSavePage(newId, title);
}
}
@@ -212,301 +80,36 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> {
@override
Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+
return Scaffold(
- backgroundColor: Colors.white,
+ backgroundColor: theme.scaffoldBackgroundColor,
appBar: AppBar(
- backgroundColor: Colors.white,
+ backgroundColor: theme.scaffoldBackgroundColor,
elevation: 0,
leading: IconButton(
- icon: const Icon(Icons.arrow_back, color: Colors.black),
+ icon: Icon(Icons.arrow_back, color: theme.colorScheme.onSurface),
onPressed: () => Navigator.pop(context),
),
title: Text(
"MoodBoards",
- style: Variables.headerStyle.copyWith(fontSize: 20),
+ style: Variables.headerStyle.copyWith(
+ color: theme.colorScheme.onSurface,
+ ),
),
actions: [
IconButton(
- icon: const Icon(Icons.add, color: Colors.black, size: 28),
+ icon: Icon(Icons.add, color: theme.colorScheme.onSurface, size: 28),
onPressed: _createNewProject,
tooltip: "Create New Project",
),
],
),
- body:
- _isLoading
- ? const Center(child: CircularProgressIndicator())
- : Column(
- children: [
- Padding(
- padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
- child: CommonSearchBar(
- controller: _searchController,
- onChanged: _filterProjects,
- ),
- ),
- Expanded(
- child: SingleChildScrollView(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- if (_searchQuery.isEmpty &&
- _recentViewModels.isNotEmpty) ...[
- const Padding(
- padding: EdgeInsets.symmetric(
- horizontal: 20,
- vertical: 8,
- ),
- child: SectionHeader(
- title: "Recent Projects/Events",
- ),
- ),
- Padding(
- padding: const EdgeInsets.symmetric(
- horizontal: 20,
- ),
- child: Column(
- children:
- _recentViewModels
- .map((vm) => _buildRecentItem(vm))
- .toList(),
- ),
- ),
- const SizedBox(height: 24),
- ],
- Padding(
- padding: const EdgeInsets.symmetric(
- horizontal: 20,
- vertical: 8,
- ),
- child: SectionHeader(
- title:
- _searchQuery.isEmpty
- ? "All Projects/Events"
- : "Search Results",
- ),
- ),
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 20),
- child: ListView.builder(
- shrinkWrap: true,
- physics: const NeverScrollableScrollPhysics(),
- itemCount: _filteredGroupedProjects.length,
- itemBuilder:
- (context, index) => _buildProjectGroup(
- _filteredGroupedProjects[index],
- ),
- ),
- ),
- const SizedBox(height: 40),
- ],
- ),
- ),
- ),
- ],
- ),
- );
- }
-
- // Wrappers for consistent UI
- Widget _buildRecentItem(ProjectItemViewModel vm) {
- return Container(
- margin: const EdgeInsets.only(bottom: 8),
- child: InkWell(
- onTap:
- () => _navigateToSavePage(
- vm.id,
- vm.title,
- parentProjectName: vm.parentTitle,
- ),
- borderRadius: BorderRadius.circular(16),
- child: Container(
- padding: const EdgeInsets.fromLTRB(4, 4, 0, 4),
- decoration: BoxDecoration(
- color: Colors.white,
- borderRadius: BorderRadius.circular(12),
- border: Border.all(color: Variables.borderSubtle, width: 1),
- ),
- child: Row(
- children: [
- // Cover Image
- Container(
- width: 56,
- height: 56,
- decoration: BoxDecoration(
- color: Variables.surfaceSubtle,
- borderRadius: BorderRadius.circular(8),
- image:
- vm.coverPath != null
- ? DecorationImage(
- image: FileImage(File(vm.coverPath!)),
- fit: BoxFit.cover,
- )
- : null,
- ),
- child:
- vm.coverPath == null
- ? Icon(Icons.image, color: Colors.grey[400], size: 28)
- : null,
- ),
- const SizedBox(width: 10),
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- if (vm.isEvent && vm.parentTitle != null)
- Text(
- vm.parentTitle!,
- style: Variables.captionStyle.copyWith(fontSize: 12),
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- ),
- Text(
- vm.title,
- style: Variables.bodyStyle.copyWith(
- fontWeight: FontWeight.w500,
- fontSize: 16,
- ),
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- ),
- ],
- ),
- ),
- ],
- ),
- ),
- ),
- );
- }
-
- Widget _buildProjectGroup(ProjectGroup g) {
- final project = g.project;
- final hasEvents = g.events.isNotEmpty;
-
- return Container(
- margin: const EdgeInsets.only(bottom: 8),
- clipBehavior: Clip.antiAlias,
- decoration: BoxDecoration(
- color: Colors.white,
- borderRadius: BorderRadius.circular(12),
- border: Border.all(color: Variables.borderSubtle),
- ),
- child: Column(
- children: [
- // Parent Project
- ListTile(
- onTap:
- () =>
- hasEvents
- ? setState(() => g.isExpanded = !g.isExpanded)
- : _navigateToSavePage(project.id!, project.title),
- contentPadding: const EdgeInsets.symmetric(
- horizontal: 16,
- vertical: 4,
- ),
- visualDensity: VisualDensity.compact,
- leading: Container(
- width: 48,
- height: 48,
- decoration: BoxDecoration(
- color: Colors.grey[100],
- borderRadius: BorderRadius.circular(12),
- image:
- g.coverPath != null
- ? DecorationImage(
- image: FileImage(File(g.coverPath!)),
- fit: BoxFit.cover,
- )
- : null,
- ),
- child:
- g.coverPath == null
- ? Icon(Icons.folder, color: Colors.grey[500])
- : null,
- ),
- title: Text(
- project.title,
- style: Variables.bodyStyle.copyWith(
- fontWeight: FontWeight.w500,
- fontSize: 16,
- ),
- ),
- trailing:
- hasEvents
- ? IconButton(
- icon: Icon(
- g.isExpanded
- ? Icons.keyboard_arrow_up
- : Icons.keyboard_arrow_down,
- color: Colors.grey[600],
- ),
- onPressed:
- () => setState(() => g.isExpanded = !g.isExpanded),
- )
- : null,
- ),
-
- // Children (Events)
- if (hasEvents)
- AnimatedCrossFade(
- firstChild: const SizedBox.shrink(),
- secondChild: Container(
- width: double.infinity,
- color: Variables.surfaceSubtle,
- child: Column(
- children:
- g.events.map((e) {
- return ListTile(
- onTap: () => _navigateToSavePage(e.id, e.title),
- contentPadding: const EdgeInsets.symmetric(
- horizontal: 24,
- vertical: 2,
- ),
- visualDensity: VisualDensity.compact,
- leading: Container(
- width: 40,
- height: 40,
- decoration: BoxDecoration(
- color: Colors.white,
- borderRadius: BorderRadius.circular(10),
- border: Border.all(color: Colors.grey.shade200),
- image:
- e.coverPath != null
- ? DecorationImage(
- image: FileImage(File(e.coverPath!)),
- fit: BoxFit.cover,
- )
- : null,
- ),
- child:
- e.coverPath == null
- ? const Icon(
- Icons.event,
- size: 20,
- color: Colors.grey,
- )
- : null,
- ),
- title: Text(
- e.title,
- style: Variables.bodyStyle.copyWith(
- fontSize: 15,
- fontWeight: FontWeight.w500,
- ),
- ),
- );
- }).toList(),
- ),
- ),
- crossFadeState:
- g.isExpanded
- ? CrossFadeState.showSecond
- : CrossFadeState.showFirst,
- duration: const Duration(milliseconds: 200),
- ),
- ],
+ body: ProjectSelector(
+ key: _selectorKey,
+ onProjectSelected: (id, title, parentTitle) {
+ _navigateToSavePage(id, title, parentProjectName: parentTitle);
+ },
),
);
}
diff --git a/lib/ui/widgets/project_selector.dart b/lib/ui/widgets/project_selector.dart
@@ -0,0 +1,525 @@
+import 'dart:io';
+import 'package:flutter/material.dart';
+import 'package:creekui/data/models/project_model.dart';
+import 'package:creekui/data/repos/project_repo.dart';
+import 'package:creekui/data/repos/image_repo.dart';
+import 'package:creekui/ui/styles/variables.dart';
+import 'package:creekui/ui/widgets/search_bar.dart';
+import 'package:creekui/ui/widgets/section_header.dart';
+import 'package:creekui/ui/widgets/empty_state.dart';
+
+class ProjectItemViewModel {
+ final ProjectModel item;
+ final String? parentTitle;
+ final String? coverPath;
+
+ ProjectItemViewModel({required this.item, this.parentTitle, this.coverPath});
+
+ String get title => item.title;
+ bool get isEvent => item.isEvent;
+ int get id => item.id!;
+}
+
+class ProjectGroup {
+ final ProjectModel project;
+ final List<ProjectItemViewModel> events;
+ final String? coverPath;
+ bool isExpanded;
+
+ ProjectGroup({
+ required this.project,
+ this.events = const [],
+ this.coverPath,
+ this.isExpanded = false,
+ });
+}
+
+class ProjectSelector extends StatefulWidget {
+ final Function(int id, String title, String? parentTitle) onProjectSelected;
+ final String searchHint;
+ final ScrollController? scrollController;
+
+ const ProjectSelector({
+ super.key,
+ required this.onProjectSelected,
+ this.searchHint = "Search",
+ this.scrollController,
+ });
+
+ @override
+ State<ProjectSelector> createState() => _ProjectSelectorState();
+}
+
+class _ProjectSelectorState extends State<ProjectSelector> {
+ final ProjectRepo _projectRepo = ProjectRepo();
+ final ImageRepo _imageRepo = ImageRepo();
+
+ final TextEditingController _searchController = TextEditingController();
+
+ List<ProjectItemViewModel> _recentViewModels = [];
+ List<ProjectGroup> _groupedProjects = [];
+ List<ProjectGroup> _filteredGroupedProjects = [];
+
+ bool _isLoading = true;
+ String _searchQuery = "";
+
+ @override
+ void initState() {
+ super.initState();
+ _loadData();
+ }
+
+ @override
+ void dispose() {
+ _searchController.dispose();
+ super.dispose();
+ }
+
+ Future<String?> _getProjectCover(int projectId) async {
+ try {
+ final images = await _imageRepo.getImages(projectId);
+ if (images.isNotEmpty) {
+ return images.first.filePath;
+ }
+ } catch (e) {
+ debugPrint("Error fetching cover for project $projectId: $e");
+ }
+ return null;
+ }
+
+ Future<void> _loadData() async {
+ if (!mounted) return;
+ setState(() => _isLoading = true);
+
+ // 1. Fetch Raw Data
+ final recentItems = await _projectRepo.getRecentProjectsAndEvents();
+ final allProjects = await _projectRepo.getAllProjects();
+
+ // 2. Build Recent View Models
+ final List<ProjectItemViewModel> recents = [];
+ for (var item in recentItems.take(3)) {
+ String? parentTitle;
+ if (item.parentId != null) {
+ final parent = await _projectRepo.getProjectById(item.parentId!);
+ parentTitle = parent?.title;
+ }
+
+ final cover = await _getProjectCover(item.id!);
+
+ recents.add(
+ ProjectItemViewModel(
+ item: item,
+ parentTitle: parentTitle,
+ coverPath: cover,
+ ),
+ );
+ }
+
+ // 3. Build Grouped Projects
+ final List<ProjectGroup> groups = [];
+ for (final p in allProjects) {
+ final rawEvents = await _projectRepo.getEvents(p.id!);
+ final List<ProjectItemViewModel> eventVMs = [];
+ for (final e in rawEvents) {
+ final eCover = await _getProjectCover(e.id!);
+ eventVMs.add(ProjectItemViewModel(item: e, coverPath: eCover));
+ }
+
+ final pCover = await _getProjectCover(p.id!);
+
+ groups.add(ProjectGroup(project: p, events: eventVMs, coverPath: pCover));
+ }
+
+ if (mounted) {
+ setState(() {
+ _recentViewModels = recents;
+ _groupedProjects = groups;
+ _filterProjects(_searchQuery); // Re-apply filter if any
+ _isLoading = false;
+ });
+ }
+ }
+
+ void _filterProjects(String query) {
+ setState(() {
+ _searchQuery = query;
+ if (query.isEmpty) {
+ _filteredGroupedProjects = _groupedProjects;
+ } else {
+ final q = query.toLowerCase();
+ final List<ProjectGroup> filtered = [];
+ for (final g in _groupedProjects) {
+ final projectMatch = g.project.title.toLowerCase().contains(q);
+ final matchingEvents =
+ g.events.where((e) => e.title.toLowerCase().contains(q)).toList();
+
+ if (projectMatch) {
+ filtered.add(
+ ProjectGroup(
+ project: g.project,
+ events: g.events,
+ isExpanded: true,
+ coverPath: g.coverPath,
+ ),
+ );
+ } else if (matchingEvents.isNotEmpty) {
+ filtered.add(
+ ProjectGroup(
+ project: g.project,
+ events: matchingEvents,
+ isExpanded: true,
+ coverPath: g.coverPath,
+ ),
+ );
+ }
+ }
+ _filteredGroupedProjects = filtered;
+ }
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ final isDark = theme.brightness == Brightness.dark;
+
+ if (_isLoading) {
+ return const Center(child: CircularProgressIndicator());
+ }
+
+ return Column(
+ children: [
+ // Search Bar
+ Padding(
+ padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
+ child: CommonSearchBar(
+ controller: _searchController,
+ onChanged: _filterProjects,
+ hintText: widget.searchHint,
+ ),
+ ),
+
+ Expanded(
+ child: SingleChildScrollView(
+ controller: widget.scrollController,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Empty State
+ if (_filteredGroupedProjects.isEmpty && _searchQuery.isNotEmpty)
+ const Padding(
+ padding: EdgeInsets.all(32.0),
+ child: EmptyState(
+ icon: Icons.search_off,
+ title: "No results found",
+ subtitle: "Try adjusting your search",
+ ),
+ ),
+
+ // Recents Section
+ if (_searchQuery.isEmpty && _recentViewModels.isNotEmpty) ...[
+ const Padding(
+ padding: EdgeInsets.symmetric(horizontal: 16),
+ child: SectionHeader(title: "Recent Projects/Events"),
+ ),
+ const SizedBox(height: 12),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ child: Column(
+ children:
+ _recentViewModels
+ .map((vm) => _buildRecentItem(vm, theme, isDark))
+ .toList(),
+ ),
+ ),
+ const SizedBox(height: 24),
+ ],
+
+ // All Projects Section
+ if (_filteredGroupedProjects.isNotEmpty) ...[
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ child: SectionHeader(
+ title:
+ _searchQuery.isEmpty
+ ? "All Projects/Events"
+ : "Search Results",
+ ),
+ ),
+ const SizedBox(height: 12),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ child: ListView.builder(
+ shrinkWrap: true,
+ physics: const NeverScrollableScrollPhysics(),
+ itemCount: _filteredGroupedProjects.length,
+ itemBuilder:
+ (context, index) => _buildProjectGroup(
+ _filteredGroupedProjects[index],
+ theme,
+ isDark,
+ ),
+ ),
+ ),
+ ],
+ const SizedBox(height: 40),
+ ],
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+
+ // Widgets
+ Widget _buildRecentItem(
+ ProjectItemViewModel vm,
+ ThemeData theme,
+ bool isDark,
+ ) {
+ return Container(
+ margin: const EdgeInsets.only(bottom: 8),
+ child: InkWell(
+ onTap: () => widget.onProjectSelected(vm.id, vm.title, vm.parentTitle),
+ borderRadius: BorderRadius.circular(Variables.radiusMedium),
+ child: Container(
+ padding: const EdgeInsets.fromLTRB(4, 4, 0, 4),
+ decoration: BoxDecoration(
+ color: theme.scaffoldBackgroundColor,
+ borderRadius: BorderRadius.circular(Variables.radiusMedium),
+ border: Border.all(
+ color: isDark ? Variables.borderDark : Variables.borderSubtle,
+ width: 1,
+ ),
+ ),
+ child: Row(
+ children: [
+ // Cover Image
+ Container(
+ width: 56,
+ height: 56,
+ decoration: BoxDecoration(
+ color:
+ isDark ? Variables.surfaceDark : Variables.surfaceSubtle,
+ borderRadius: BorderRadius.circular(Variables.radiusSmall),
+ image:
+ vm.coverPath != null
+ ? DecorationImage(
+ image: FileImage(File(vm.coverPath!)),
+ fit: BoxFit.cover,
+ )
+ : null,
+ ),
+ child:
+ vm.coverPath == null
+ ? Icon(
+ Icons.image,
+ color: theme.colorScheme.onSurface.withValues(
+ alpha: 0.3,
+ ),
+ size: 28,
+ )
+ : null,
+ ),
+ const SizedBox(width: 12),
+ // Text Content
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ if (vm.isEvent && vm.parentTitle != null)
+ Padding(
+ padding: const EdgeInsets.only(bottom: 2),
+ child: Text(
+ vm.parentTitle!,
+ style: Variables.captionStyle.copyWith(
+ color: theme.colorScheme.onSurface.withValues(
+ alpha: 0.6,
+ ),
+ ),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ Text(
+ vm.title,
+ style: Variables.bodyStyle.copyWith(
+ fontWeight: FontWeight.w600,
+ color: theme.colorScheme.onSurface,
+ ),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildProjectGroup(ProjectGroup g, ThemeData theme, bool isDark) {
+ final project = g.project;
+ final hasEvents = g.events.isNotEmpty;
+
+ return Container(
+ margin: const EdgeInsets.only(bottom: 8),
+ clipBehavior: Clip.antiAlias,
+ decoration: BoxDecoration(
+ color: theme.scaffoldBackgroundColor,
+ borderRadius: BorderRadius.circular(Variables.radiusMedium),
+ border: Border.all(
+ color: isDark ? Variables.borderDark : Variables.borderSubtle,
+ ),
+ ),
+ child: Column(
+ children: [
+ // Parent Project
+ ListTile(
+ onTap:
+ () =>
+ hasEvents
+ ? setState(() => g.isExpanded = !g.isExpanded)
+ : widget.onProjectSelected(
+ project.id!,
+ project.title,
+ null,
+ ),
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 4,
+ ),
+ visualDensity: VisualDensity.compact,
+ leading: Container(
+ width: 48,
+ height: 48,
+ decoration: BoxDecoration(
+ color: isDark ? Variables.surfaceDark : Variables.surfaceSubtle,
+ borderRadius: BorderRadius.circular(Variables.radiusMedium),
+ image:
+ g.coverPath != null
+ ? DecorationImage(
+ image: FileImage(File(g.coverPath!)),
+ fit: BoxFit.cover,
+ )
+ : null,
+ ),
+ child:
+ g.coverPath == null
+ ? Icon(
+ Icons.folder,
+ color: theme.colorScheme.onSurface.withValues(
+ alpha: 0.4,
+ ),
+ )
+ : null,
+ ),
+ title: Text(
+ project.title,
+ style: Variables.bodyStyle.copyWith(
+ fontWeight: FontWeight.w600,
+ fontSize: 16,
+ color: theme.colorScheme.onSurface,
+ ),
+ ),
+ trailing:
+ hasEvents
+ ? IconButton(
+ icon: Icon(
+ g.isExpanded
+ ? Icons.keyboard_arrow_up
+ : Icons.keyboard_arrow_down,
+ color: theme.colorScheme.onSurface.withValues(
+ alpha: 0.6,
+ ),
+ ),
+ onPressed: () {
+ setState(() => g.isExpanded = !g.isExpanded);
+ },
+ )
+ : null,
+ ),
+
+ // Children (Events)
+ if (hasEvents)
+ AnimatedCrossFade(
+ firstChild: const SizedBox.shrink(),
+ secondChild: Container(
+ width: double.infinity,
+ color:
+ isDark
+ ? Colors.black26
+ : Variables.surfaceSubtle.withValues(alpha: 0.5),
+ child: Column(
+ children:
+ g.events.map((e) {
+ return ListTile(
+ onTap:
+ () => widget.onProjectSelected(
+ e.id,
+ e.title,
+ project.title,
+ ),
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 24,
+ vertical: 2,
+ ),
+ visualDensity: VisualDensity.compact,
+ leading: Container(
+ width: 40,
+ height: 40,
+ decoration: BoxDecoration(
+ color: theme.cardColor,
+ borderRadius: BorderRadius.circular(
+ Variables.radiusSmall,
+ ),
+ border: Border.all(
+ color:
+ isDark
+ ? Variables.borderDark
+ : Variables.borderSubtle,
+ ),
+ image:
+ e.coverPath != null
+ ? DecorationImage(
+ image: FileImage(File(e.coverPath!)),
+ fit: BoxFit.cover,
+ )
+ : null,
+ ),
+ child:
+ e.coverPath == null
+ ? Icon(
+ Icons.event,
+ size: 20,
+ color: theme.colorScheme.onSurface
+ .withValues(alpha: 0.4),
+ )
+ : null,
+ ),
+ title: Text(
+ e.title,
+ style: Variables.bodyStyle.copyWith(
+ fontSize: 15,
+ fontWeight: FontWeight.w500,
+ color: theme.colorScheme.onSurface,
+ ),
+ ),
+ );
+ }).toList(),
+ ),
+ ),
+ crossFadeState:
+ g.isExpanded
+ ? CrossFadeState.showSecond
+ : CrossFadeState.showFirst,
+ duration: const Duration(milliseconds: 200),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/ui/widgets/section_header.dart b/lib/ui/widgets/section_header.dart
@@ -1,51 +1,67 @@
import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
import 'package:creekui/ui/styles/variables.dart';
class SectionHeader extends StatelessWidget {
final String title;
final VoidCallback? onTap;
final Widget? trailing;
+ final bool showArrow;
const SectionHeader({
super.key,
required this.title,
this.onTap,
this.trailing,
+ this.showArrow = true,
});
@override
Widget build(BuildContext context) {
- final theme = Theme.of(context);
-
- return Row(
- children: [
- Text(
- title,
- style: Variables.headerStyle.copyWith(
- fontSize: 16,
- color: theme.colorScheme.onSurface,
+ Widget content = Padding(
+ padding: const EdgeInsets.only(bottom: 12),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Expanded(
+ child: Text(
+ title,
+ style: const TextStyle(
+ fontFamily: 'GeneralSans',
+ fontSize: 16,
+ fontWeight: FontWeight.w500,
+ height: 24 / 16,
+ color: Variables.textPrimary,
+ ),
+ ),
),
- ),
- const Spacer(),
- if (trailing != null)
- trailing!
- else if (onTap != null)
- Material(
- color: Colors.transparent,
- child: InkWell(
- borderRadius: BorderRadius.circular(20),
- onTap: onTap,
- child: Padding(
- padding: const EdgeInsets.all(8.0),
- child: Icon(
- Icons.chevron_right,
- size: 24,
- color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
+ if (trailing != null)
+ trailing!
+ else if (showArrow)
+ Transform.rotate(
+ angle: 3.14159,
+ child: SvgPicture.asset(
+ 'assets/icons/arrow-left-s-line.svg',
+ width: 24,
+ height: 24,
+ colorFilter: const ColorFilter.mode(
+ Variables.textPrimary,
+ BlendMode.srcIn,
),
),
),
- ),
- ],
+ ],
+ ),
);
+
+ if (onTap != null) {
+ return GestureDetector(
+ onTap: onTap,
+ behavior: HitTestBehavior.opaque,
+ child: content,
+ );
+ }
+
+ return content;
}
}
diff --git a/lib/ui/widgets/top_bar.dart b/lib/ui/widgets/top_bar.dart
@@ -3,6 +3,8 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:creekui/data/models/project_model.dart';
import 'package:creekui/data/repos/project_repo.dart';
import 'package:creekui/ui/styles/variables.dart';
+import 'package:creekui/ui/pages/settings_page.dart';
+import 'package:creekui/ui/pages/home_page.dart';
class TopBar extends StatefulWidget implements PreferredSizeWidget {
final int currentProjectId;
@@ -70,7 +72,7 @@ class _TopBarState extends State<TopBar> {
widget.currentProjectId,
);
if (current == null) {
- setState(() => _isLoading = false);
+ if (mounted) setState(() => _isLoading = false);
return;
}
@@ -98,43 +100,69 @@ class _TopBarState extends State<TopBar> {
}
} catch (e) {
debugPrint("TopBar Error: $e");
- setState(() => _isLoading = false);
+ if (mounted) setState(() => _isLoading = false);
+ }
+ }
+
+ void _handleSafeBack() {
+ if (widget.onBack != null) {
+ widget.onBack!();
+ return;
+ }
+
+ if (Navigator.canPop(context)) {
+ Navigator.pop(context);
+ } else {
+ // Fallback: Go to HomePage to prevent app exit/crash
+ Navigator.pushReplacement(
+ context,
+ MaterialPageRoute(builder: (_) => const HomePage()),
+ );
+ }
+ }
+
+ void _openSettings() {
+ if (widget.onSettingsPressed != null) {
+ widget.onSettingsPressed!();
+ } else {
+ Navigator.push(
+ context,
+ MaterialPageRoute(builder: (_) => const SettingsPage()),
+ );
}
}
@override
Widget build(BuildContext context) {
- final textScaler = MediaQuery.of(context).textScaler;
return Container(
- color: Colors.white, // White background
+ color: Colors.white,
child: SafeArea(
bottom: false,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
- // First Row: Title and Settings
+ // Row 1: Title and Settings
Container(
- height: 48.0, // py-[12px] = 24px + 24px content
+ height: 48.0,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: [
// Back Button
- if (widget.onBack != null) ...[
- GestureDetector(
- onTap: widget.onBack,
- child: Container(
- width: 24,
- height: 24,
- alignment: Alignment.center,
- child: const Icon(
- Icons.arrow_back,
- size: 20,
- color: Variables.textPrimary,
- ),
+ GestureDetector(
+ onTap: _handleSafeBack,
+ child: Container(
+ width: 24,
+ height: 24,
+ alignment: Alignment.center,
+ child: const Icon(
+ Icons.arrow_back,
+ size: 20,
+ color: Variables.textPrimary,
),
),
- const SizedBox(width: 8),
- ],
+ ),
+ const SizedBox(width: 10),
+
// Title
Expanded(
child:
@@ -145,7 +173,7 @@ class _TopBarState extends State<TopBar> {
fontFamily: 'GeneralSans',
fontSize: 20,
fontWeight: FontWeight.w500,
- height: 24 / 20, // line-height: 24px
+ height: 24 / 20,
color: Variables.textPrimary,
),
overflow: TextOverflow.ellipsis,
@@ -166,10 +194,10 @@ class _TopBarState extends State<TopBar> {
)
: const SizedBox(),
),
- // Settings Icon (only show if second row is visible)
+ // Settings Icon
if (!widget.hideSecondRow)
GestureDetector(
- onTap: widget.onSettingsPressed,
+ onTap: _openSettings,
child: SvgPicture.asset(
'assets/icons/settings-line.svg',
width: 24,
@@ -183,10 +211,10 @@ class _TopBarState extends State<TopBar> {
],
),
),
- // Second Row: Global Dropdown and Action Buttons
+ // Row 2: Global Dropdown and Action Buttons
if (!widget.hideSecondRow)
Container(
- height: 40.0, // py-[8px] = 16px + 24px content
+ height: 40.0,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: [
@@ -213,7 +241,7 @@ class _TopBarState extends State<TopBar> {
vertical: 10,
),
decoration: BoxDecoration(
- color: Variables.borderSubtle, // #e4e4e7
+ color: Variables.borderSubtle,
borderRadius: BorderRadius.circular(1000),
),
child: Row(
@@ -265,7 +293,6 @@ class _TopBarState extends State<TopBar> {
}).toList();
},
),
- // Layout Icon Button (Toggle between All Images/Categorized) or Edit Icon
if (widget.onLayoutToggle != null ||
widget.onLayoutPressed != null) ...[
const SizedBox(width: 8),
@@ -286,8 +313,7 @@ class _TopBarState extends State<TopBar> {
? (widget.isAlternateView == true
? Icons.dashboard
: Icons.view_agenda_outlined)
- : Icons
- .edit, // Use edit icon when only onLayoutPressed is provided
+ : Icons.edit,
size: 20,
color: Variables.textPrimary,
),
@@ -297,12 +323,12 @@ class _TopBarState extends State<TopBar> {
],
),
const Spacer(),
- // Right group: Filter and AI/Sparkle Buttons
+ // Right group: Filter and AI Buttons
if (widget.onFilterPressed != null ||
widget.onAIPressed != null)
Row(
children: [
- // Filter Icon Button
+ // Filter Button
if (widget.onFilterPressed != null)
GestureDetector(
onTap: widget.onFilterPressed,
@@ -325,7 +351,7 @@ class _TopBarState extends State<TopBar> {
if (widget.onFilterPressed != null &&
widget.onAIPressed != null)
const SizedBox(width: 8),
- // AI/Sparkle Icon Button
+ // AI Button
if (widget.onAIPressed != null)
GestureDetector(
onTap: widget.onAIPressed,