commit 5495fe41daaee683edf1b55682e2caf9e052b88f
parent dfe1a3d56e61f957881f08376be5847b3660091c
Author: maydayv7 <maydayv7@gmail.com>
Date: Sat, 6 Dec 2025 03:14:14 +0530
Refactor image_* pages and add moodboard image card
Diffstat:
16 files changed, 1473 insertions(+), 3008 deletions(-)
diff --git a/lib/ui/pages/define_brand_page.dart b/lib/ui/pages/define_brand_page.dart
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:creekui/services/project_service.dart';
import 'package:creekui/ui/styles/variables.dart';
+import 'package:creekui/ui/widgets/text_field.dart';
+import 'package:creekui/ui/widgets/primary_button.dart';
import 'project_detail_page.dart';
class DefineBrandPage extends StatefulWidget {
@@ -18,10 +20,7 @@ class DefineBrandPage extends StatefulWidget {
}
class _DefineBrandPageState extends State<DefineBrandPage> {
- // Service
final ProjectService _projectService = ProjectService();
-
- // Controllers
final _projectNameController = TextEditingController();
final _descriptionController = TextEditingController();
final _problemController = TextEditingController();
@@ -29,7 +28,6 @@ class _DefineBrandPageState extends State<DefineBrandPage> {
final _whereWillAppearController = TextEditingController();
final _competitorInputController = TextEditingController();
- // State
bool _isLoading = false;
final List<String> _keywords = ['Colour', 'Fonts', 'Composition'];
final List<Map<String, String>> _competitorBrands = [
@@ -68,16 +66,14 @@ class _DefineBrandPageState extends State<DefineBrandPage> {
return;
}
- setState(() {
- _isLoading = true;
- });
+ setState(() => _isLoading = true);
try {
// 2. Prepare data
final String title = _projectNameController.text.trim();
final String description = _descriptionController.text.trim();
- // 3. Call the Service and capture the NEW ID
+ // 3. Call service and capture new ID
final int newId = await _projectService.createProject(
title,
description: description.isEmpty ? null : description,
@@ -103,11 +99,7 @@ class _DefineBrandPageState extends State<DefineBrandPage> {
);
}
} finally {
- if (mounted) {
- setState(() {
- _isLoading = false;
- });
- }
+ if (mounted) setState(() => _isLoading = false);
}
}
@@ -125,7 +117,7 @@ class _DefineBrandPageState extends State<DefineBrandPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
- backgroundColor: const Color(0xFFFAFAFA),
+ backgroundColor: Variables.background,
body: SafeArea(
child: Column(
children: [
@@ -142,8 +134,6 @@ class _DefineBrandPageState extends State<DefineBrandPage> {
color: Variables.textSecondary,
),
onPressed: () => Navigator.pop(context),
- padding: EdgeInsets.zero,
- constraints: const BoxConstraints(),
),
],
),
@@ -178,87 +168,51 @@ class _DefineBrandPageState extends State<DefineBrandPage> {
),
),
),
-
const SizedBox(height: 16),
-
- // Title and Subtitle
- Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- 'Define Your Brand',
- style: TextStyle(
- fontFamily: 'GeneralSans',
- fontSize: 20,
- fontWeight: FontWeight.w500,
- color: Variables.textPrimary,
- height: 24 / 20,
- ),
- ),
- const SizedBox(height: 4),
- Text(
- 'Answer a few quick questions to help us craft your unique style guide.',
- style: TextStyle(
- fontFamily: 'GeneralSans',
- fontSize: 14,
- fontWeight: FontWeight.w400,
- color: Variables.textSecondary,
- height: 20 / 14,
- ),
- ),
- ],
+ Text('Define Your Brand', style: Variables.headerStyle),
+ const SizedBox(height: 4),
+ Text(
+ 'Answer a few quick questions to help us craft your unique style guide.',
+ style: Variables.captionStyle.copyWith(fontSize: 14),
),
-
const SizedBox(height: 32),
-
- // Form Fields
- Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- _buildFormField(
- label: 'Project Name',
- hintText: 'Enter project name',
- controller: _projectNameController,
- required: true,
- ),
- const SizedBox(height: 16),
- _buildFormField(
- label: 'What do you want & who is it for.',
- hintText: 'Describe your work and your audience.',
- controller: _descriptionController,
- maxLines: 3,
- required: false,
- ),
- const SizedBox(height: 16),
- _buildFormField(
- label: 'What problem you solve.',
- hintText:
- 'Explain the main issue your brand addresses.',
- controller: _problemController,
- maxLines: 3,
- required: false,
- ),
- const SizedBox(height: 16),
- _buildFormField(
- label: 'Long-term goal for the brand.',
- hintText: 'E.g. - Improving food availability...',
- controller: _goalController,
- required: false,
- ),
- const SizedBox(height: 32),
- _buildKeywordsSection(),
- const SizedBox(height: 32),
- _buildCompetitorBrandsSection(),
- const SizedBox(height: 32),
- _buildFormField(
- label: 'Where will the brand appear',
- hintText: 'Banners, Posters, Instagram..',
- controller: _whereWillAppearController,
- required: false,
- ),
- ],
+ CommonTextField(
+ label: 'Project Name',
+ hintText: 'Enter project name',
+ controller: _projectNameController,
+ isRequired: true,
),
- const SizedBox(height: 100), // Space for bottom button
+ const SizedBox(height: 16),
+ CommonTextField(
+ label: 'What do you want & who is it for.',
+ hintText: 'Describe your work and your audience.',
+ controller: _descriptionController,
+ maxLines: 3,
+ ),
+ const SizedBox(height: 16),
+ CommonTextField(
+ label: 'What problem you solve.',
+ hintText: 'Explain the main issue your brand addresses.',
+ controller: _problemController,
+ maxLines: 3,
+ ),
+ const SizedBox(height: 16),
+ CommonTextField(
+ label: 'Long-term goal for the brand.',
+ hintText: 'E.g. - Improving food availability...',
+ controller: _goalController,
+ ),
+ const SizedBox(height: 32),
+ _buildKeywordsSection(),
+ const SizedBox(height: 32),
+ _buildCompetitorBrandsSection(),
+ const SizedBox(height: 32),
+ CommonTextField(
+ label: 'Where will the brand appear',
+ hintText: 'Banners, Posters, Instagram..',
+ controller: _whereWillAppearController,
+ ),
+ const SizedBox(height: 100),
],
),
),
@@ -271,144 +225,27 @@ class _DefineBrandPageState extends State<DefineBrandPage> {
bottomNavigationBar: Container(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 24),
decoration: BoxDecoration(
- color: const Color(0xFFFAFAFA),
+ color: Variables.background,
boxShadow: [
BoxShadow(
- color: Colors.black.withValues(alpha: 0.05),
+ color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, -2),
),
],
),
child: SafeArea(
- child: SizedBox(
- width: double.infinity,
- child: ElevatedButton(
- onPressed: _isLoading ? null : _handleFinish,
- style: ElevatedButton.styleFrom(
- backgroundColor: Variables.textPrimary,
- foregroundColor: Colors.white,
- padding: const EdgeInsets.symmetric(vertical: 12),
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(112),
- ),
- elevation: 0,
- ),
- child:
- _isLoading
- ? const SizedBox(
- height: 20,
- width: 20,
- child: CircularProgressIndicator(
- color: Colors.white,
- strokeWidth: 2,
- ),
- )
- : Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Text(
- 'Create Project',
- style: TextStyle(
- fontFamily: 'GeneralSans',
- fontSize: 14,
- fontWeight: FontWeight.w500,
- color: Colors.white,
- ),
- ),
- const SizedBox(width: 12),
- SvgPicture.asset(
- 'assets/icons/generate_icon.svg',
- width: 18,
- height: 18,
- colorFilter: const ColorFilter.mode(
- Colors.white,
- BlendMode.srcIn,
- ),
- ),
- ],
- ),
- ),
+ child: PrimaryButton(
+ text: 'Create Project',
+ isLoading: _isLoading,
+ onPressed: _handleFinish,
+ iconPath: 'assets/icons/generate_icon.svg',
),
),
),
);
}
- Widget _buildFormField({
- required String label,
- required String hintText,
- required TextEditingController controller,
- int maxLines = 1,
- bool required = false,
- }) {
- return Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Row(
- children: [
- Text(
- label,
- style: TextStyle(
- fontFamily: 'GeneralSans',
- fontSize: 14,
- fontWeight: FontWeight.w500,
- color: Variables.textPrimary,
- height: 20 / 14,
- ),
- ),
- if (required)
- Text(
- '*',
- style: TextStyle(
- fontFamily: 'GeneralSans',
- fontSize: 12,
- fontWeight: FontWeight.w500,
- color: const Color(0xFF4F39F6),
- height: 16 / 12,
- ),
- ),
- ],
- ),
- const SizedBox(height: 6),
- Container(
- decoration: BoxDecoration(
- color: const Color(0xFFE4E4E7),
- borderRadius: BorderRadius.circular(8),
- ),
- child: TextField(
- controller: controller,
- maxLines: maxLines,
- style: TextStyle(
- fontFamily: 'GeneralSans',
- fontSize: 14,
- fontWeight: FontWeight.w400,
- color: Variables.textPrimary,
- height: 20 / 14,
- ),
- decoration: InputDecoration(
- hintText: hintText,
- hintStyle: TextStyle(
- fontFamily: 'GeneralSans',
- fontSize: 14,
- fontWeight: FontWeight.w400,
- color: Variables.textSecondary,
- height: 20 / 14,
- ),
- border: InputBorder.none,
- enabledBorder: InputBorder.none,
- focusedBorder: InputBorder.none,
- contentPadding: const EdgeInsets.symmetric(
- horizontal: 16,
- vertical: 12,
- ),
- ),
- ),
- ),
- ],
- );
- }
-
Widget _buildKeywordsSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -417,22 +254,13 @@ class _DefineBrandPageState extends State<DefineBrandPage> {
children: [
Text(
'2-3 vibe keywords',
- style: TextStyle(
- fontFamily: 'GeneralSans',
- fontSize: 14,
- fontWeight: FontWeight.w500,
- color: Variables.textPrimary,
- height: 20 / 14,
- ),
+ style: Variables.bodyStyle.copyWith(fontWeight: FontWeight.w500),
),
Text(
'*',
- style: TextStyle(
- fontFamily: 'GeneralSans',
- fontSize: 12,
- fontWeight: FontWeight.w500,
+ style: Variables.bodyStyle.copyWith(
color: const Color(0xFF4F39F6),
- height: 16 / 12,
+ fontSize: 12,
),
),
],
@@ -457,21 +285,14 @@ class _DefineBrandPageState extends State<DefineBrandPage> {
children: [
Text(
keyword,
- style: TextStyle(
- fontFamily: 'GeneralSans',
+ style: Variables.captionStyle.copyWith(
fontSize: 12,
- fontWeight: FontWeight.w400,
color: Variables.textPrimary,
- height: 16 / 12,
),
),
const SizedBox(width: 8),
GestureDetector(
- onTap: () {
- setState(() {
- _keywords.remove(keyword);
- });
- },
+ onTap: () => setState(() => _keywords.remove(keyword)),
child: Icon(
Icons.close,
size: 16,
@@ -489,7 +310,7 @@ class _DefineBrandPageState extends State<DefineBrandPage> {
vertical: 6,
),
decoration: BoxDecoration(
- border: Border.all(color: const Color(0xFFE4E4E7), width: 1),
+ border: Border.all(color: Variables.borderSubtle),
borderRadius: BorderRadius.circular(48),
),
child: Row(
@@ -497,12 +318,9 @@ class _DefineBrandPageState extends State<DefineBrandPage> {
children: [
Text(
'Add More',
- style: TextStyle(
- fontFamily: 'GeneralSans',
+ style: Variables.captionStyle.copyWith(
fontSize: 12,
- fontWeight: FontWeight.w400,
color: Variables.textPrimary,
- height: 16 / 12,
),
),
const SizedBox(width: 4),
@@ -525,60 +343,23 @@ class _DefineBrandPageState extends State<DefineBrandPage> {
children: [
Text(
'2-3 reference/competitor brands.',
- style: TextStyle(
- fontFamily: 'GeneralSans',
- fontSize: 14,
- fontWeight: FontWeight.w500,
- color: Variables.textPrimary,
- height: 20 / 14,
- ),
+ style: Variables.bodyStyle.copyWith(fontWeight: FontWeight.w500),
),
Text(
'*',
- style: TextStyle(
- fontFamily: 'GeneralSans',
- fontSize: 12,
- fontWeight: FontWeight.w500,
+ style: Variables.bodyStyle.copyWith(
color: const Color(0xFF4F39F6),
- height: 16 / 12,
+ fontSize: 12,
),
),
],
),
const SizedBox(height: 6),
- Container(
- decoration: BoxDecoration(
- color: const Color(0xFFE4E4E7),
- borderRadius: BorderRadius.circular(8),
- ),
- child: TextField(
- controller: _competitorInputController,
- onSubmitted: (_) => _addCompetitorBrand(),
- style: TextStyle(
- fontFamily: 'GeneralSans',
- fontSize: 14,
- fontWeight: FontWeight.w400,
- color: Variables.textPrimary,
- height: 20 / 14,
- ),
- decoration: InputDecoration(
- hintText: 'Type the name of brands here...',
- hintStyle: TextStyle(
- fontFamily: 'GeneralSans',
- fontSize: 14,
- fontWeight: FontWeight.w400,
- color: Variables.textSecondary,
- height: 20 / 14,
- ),
- border: InputBorder.none,
- enabledBorder: InputBorder.none,
- focusedBorder: InputBorder.none,
- contentPadding: const EdgeInsets.symmetric(
- horizontal: 16,
- vertical: 12,
- ),
- ),
- ),
+ CommonTextField(
+ label: '',
+ hintText: 'Type the name of brands here...',
+ controller: _competitorInputController,
+ onSubmitted: (_) => _addCompetitorBrand(),
),
if (_competitorBrands.isNotEmpty) ...[
const SizedBox(height: 8),
@@ -596,11 +377,8 @@ class _DefineBrandPageState extends State<DefineBrandPage> {
vertical: 8,
),
decoration: BoxDecoration(
- color: const Color(0xFFFAFAFA),
- border: Border.all(
- color: const Color(0xFFE4E4E7),
- width: 1,
- ),
+ color: Variables.surfaceSubtle,
+ border: Border.all(color: Variables.borderSubtle),
borderRadius: BorderRadius.circular(64),
),
child: Row(
@@ -609,40 +387,28 @@ class _DefineBrandPageState extends State<DefineBrandPage> {
Container(
width: 30,
height: 30,
- decoration: BoxDecoration(
+ decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: Center(
child: Text(
brand['initial'] ?? '',
- style: TextStyle(
- fontFamily: 'GeneralSans',
+ style: Variables.captionStyle.copyWith(
+ fontWeight: FontWeight.bold,
fontSize: 12,
- fontWeight: FontWeight.w500,
- color: Variables.textPrimary,
),
),
),
),
const SizedBox(width: 8),
- Text(
- brand['name'] ?? '',
- style: TextStyle(
- fontFamily: 'GeneralSans',
- fontSize: 14,
- fontWeight: FontWeight.w400,
- color: Variables.textSecondary,
- height: 20 / 14,
- ),
- ),
+ Text(brand['name'] ?? '', style: Variables.bodyStyle),
const SizedBox(width: 8),
GestureDetector(
- onTap: () {
- setState(() {
- _competitorBrands.removeAt(index);
- });
- },
+ onTap:
+ () => setState(
+ () => _competitorBrands.removeAt(index),
+ ),
child: Icon(
Icons.close,
size: 16,
@@ -680,11 +446,8 @@ class _DefineBrandPageState extends State<DefineBrandPage> {
ElevatedButton(
onPressed: () {
final val = controller.text.trim();
- if (val.isNotEmpty && !_keywords.contains(val)) {
- setState(() {
- _keywords.add(val);
- });
- }
+ if (val.isNotEmpty && !_keywords.contains(val))
+ setState(() => _keywords.add(val));
Navigator.pop(context);
},
child: const Text('Add'),
diff --git a/lib/ui/pages/home_page.dart b/lib/ui/pages/home_page.dart
@@ -40,7 +40,7 @@ class _HomePageState extends State<HomePage> {
final Map<int, List<String>> _projectPreviews = {};
Map<String, Map<String, String>> _fileMetadata = {};
bool _isLoading = true;
- final String _userName = "Alex";
+ final String _userName = "Alex"; // TODO
@override
void initState() {
@@ -619,7 +619,7 @@ class _HomePageState extends State<HomePage> {
),
);
}).toList()),
- const SizedBox(height: 24),
+ const SizedBox(height: 12),
],
SectionHeader(
title: 'Projects',
diff --git a/lib/ui/pages/image_analysis_page.dart b/lib/ui/pages/image_analysis_page.dart
@@ -4,7 +4,8 @@ import 'package:flutter/material.dart';
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/styles/variables.dart';
+import 'package:creekui/ui/styles/variables.dart';
+import 'package:creekui/ui/widgets/empty_state.dart';
class ImageAnalysisPage extends StatefulWidget {
const ImageAnalysisPage({super.key});
@@ -53,7 +54,6 @@ class _ImageAnalysisPageState extends State<ImageAnalysisPage> {
final appDir = await getApplicationDocumentsDirectory();
final fileName = sourcePath.split('/').last;
final targetPath = '${appDir.path}/$fileName';
-
final file = File(sourcePath);
await file.copy(targetPath);
@@ -126,7 +126,6 @@ class _ImageAnalysisPageState extends State<ImageAnalysisPage> {
@override
Widget build(BuildContext context) {
- final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
@@ -136,13 +135,9 @@ class _ImageAnalysisPageState extends State<ImageAnalysisPage> {
icon: const Icon(Icons.arrow_back, color: Colors.black),
onPressed: () => Navigator.pop(context),
),
- title: const Text(
+ title: Text(
"Image Analysis",
- style: TextStyle(
- color: Colors.black,
- fontFamily: 'GeneralSans',
- fontWeight: FontWeight.bold,
- ),
+ style: Variables.headerStyle.copyWith(fontSize: 20),
),
actions: [
if (_selectedImage != null)
@@ -164,11 +159,11 @@ class _ImageAnalysisPageState extends State<ImageAnalysisPage> {
width: double.infinity,
height: 250,
decoration: BoxDecoration(
- color: Colors.grey[100],
+ color: Variables.borderSubtle,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
- color: Colors.black.withValues(alpha: 0.05),
+ color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
@@ -182,23 +177,10 @@ class _ImageAnalysisPageState extends State<ImageAnalysisPage> {
if (_selectedImage != null)
Image.file(_selectedImage!, fit: BoxFit.contain)
else
- Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Icon(
- Icons.bug_report_outlined,
- size: 48,
- color: Colors.grey[400],
- ),
- const SizedBox(height: 12),
- Text(
- "Select image to test full suite",
- style: TextStyle(
- fontFamily: 'GeneralSans',
- color: Colors.grey[500],
- ),
- ),
- ],
+ const EmptyState(
+ icon: Icons.bug_report_outlined,
+ title: "No image selected",
+ subtitle: "Select an image to test full suite",
),
if (_isAnalyzing)
Container(
@@ -237,13 +219,9 @@ class _ImageAnalysisPageState extends State<ImageAnalysisPage> {
// Raw JSON Result
if (_analysisResult != null) ...[
- const Text(
+ Text(
"Results",
- style: TextStyle(
- fontSize: 18,
- fontWeight: FontWeight.bold,
- fontFamily: 'GeneralSans',
- ),
+ style: Variables.headerStyle.copyWith(fontSize: 18),
),
const SizedBox(height: 12),
_buildJsonViewer(_analysisResult!),
@@ -254,8 +232,8 @@ class _ImageAnalysisPageState extends State<ImageAnalysisPage> {
),
floatingActionButton: FloatingActionButton.extended(
onPressed: _showSourceSelector,
- backgroundColor: colorScheme.primary,
- foregroundColor: colorScheme.onPrimary,
+ backgroundColor: Variables.textPrimary,
+ foregroundColor: Colors.white,
icon: const Icon(Icons.add_photo_alternate),
label: Text(
_selectedImage == null ? "Select Image" : "Change Image",
@@ -268,7 +246,6 @@ class _ImageAnalysisPageState extends State<ImageAnalysisPage> {
Widget _buildJsonViewer(Map<String, dynamic> data) {
const encoder = JsonEncoder.withIndent(' ');
final String prettyJson = encoder.convert(data);
-
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
diff --git a/lib/ui/pages/image_details_page.dart b/lib/ui/pages/image_details_page.dart
@@ -1,5 +1,4 @@
import 'dart:io';
-import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:creekui/services/image_service.dart';
@@ -7,47 +6,11 @@ import 'package:creekui/services/note_service.dart';
import 'package:creekui/data/models/note_model.dart';
import 'package:creekui/data/models/image_model.dart';
import 'package:creekui/utils/image_actions_helper.dart';
-
-// --- STATE MACHINE FOR SELECTION MODE ---
-enum DragHandle {
- none,
- topLeft,
- topRight,
- bottomLeft,
- bottomRight,
- center, // For dragging the entire box
-}
-
-// --- HELPER CLASS FOR TEMPORARY NOTES (Needed for consistency) ---
-class TempNote {
- final double normX;
- final double normY;
- final double normWidth;
- final double normHeight;
- final String content;
- final String category;
-
- TempNote({
- required this.normX,
- required this.normY,
- required this.normWidth,
- required this.normHeight,
- required this.content,
- required this.category,
- });
-}
-
-// --- EXTENSION TO NORMALIZE RECT ---
-extension on Rect {
- Rect normalize() {
- return Rect.fromLTRB(
- left < right ? left : right,
- top < bottom ? top : bottom,
- left > right ? left : right,
- top > bottom ? top : bottom,
- );
- }
-}
+import 'package:creekui/ui/styles/variables.dart';
+import 'package:creekui/ui/widgets/primary_button.dart';
+import 'package:creekui/ui/widgets/empty_state.dart';
+import 'package:creekui/ui/widgets/selection_overlay_painter.dart';
+import 'package:creekui/ui/widgets/note_input_sheet.dart';
class ImageDetailsPage extends StatefulWidget {
final String imagePath;
@@ -66,18 +29,16 @@ class ImageDetailsPage extends StatefulWidget {
}
class _ImageDetailsPageState extends State<ImageDetailsPage> {
- // Services
final NoteService _noteService = NoteService();
final ImageService _imageService = ImageService();
- // State
ImageModel? _imageModel;
List<NoteModel> _notes = [];
List<String> _currentTags = [];
bool _isLoading = true;
int? _activeNoteId;
- // -- DRAWING/RESIZING STATE --
+ // Drawing/Resizing
bool _isDrawMode = false;
bool _isResizing = false;
final GlobalKey _imageKey = GlobalKey();
@@ -87,13 +48,10 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> {
Rect? _finalSelectionRect;
Size? _imageRenderSize;
- // Resizing state
DragHandle _activeHandle = DragHandle.none;
Offset? _startDragLocalOffset;
+ final double _handleSize = 25.0;
- final double _handleSize = 25.0; // Resizing constant
-
- // Master List of Tags
final List<String> _allAvailableTags = [
'Compositions',
'Subject',
@@ -134,8 +92,7 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> {
}
}
- // --- ACTIONS ---
-
+ // Actions
void _resetSelectionMode() {
setState(() {
_isDrawMode = false;
@@ -149,15 +106,12 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> {
}
void _activateDrawMode() {
- _resetSelectionMode(); // Reset any previous selection
- setState(() {
- _isDrawMode = true; // Start initial drawing mode
- });
+ _resetSelectionMode();
+ setState(() => _isDrawMode = true);
}
void _confirmSelectionAndShowModal() {
if (_finalSelectionRect != null) {
- // Exit resizing mode before showing the modal
setState(() {
_isResizing = false;
_activeHandle = DragHandle.none;
@@ -168,7 +122,6 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> {
void _openNotesSheet({int? highlightId}) {
setState(() => _activeNoteId = highlightId);
-
showModalBottomSheet(
context: context,
isScrollControlled: true,
@@ -182,13 +135,10 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> {
_activateDrawMode();
},
),
- ).whenComplete(() {
- setState(() => _activeNoteId = null);
- });
+ ).whenComplete(() => setState(() => _activeNoteId = null));
}
- // --- DRAWING/RESIZING GESTURES ---
-
+ // Gestures
Offset? _getLocalPosition(Offset globalPosition) {
final RenderBox? box =
_imageKey.currentContext?.findRenderObject() as RenderBox?;
@@ -203,14 +153,41 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> {
// Clamp coordinates to ensure we don't draw/drag outside the image
final dx = local.dx.clamp(0.0, box.size.width);
final dy = local.dy.clamp(0.0, box.size.height);
-
return Offset(dx, dy);
}
- // --- DRAWING HANDLERS ---
+ // Resizing Handlers
+ DragHandle _getDragHandle(Offset pos) {
+ if (_finalSelectionRect == null) return DragHandle.none;
+ final rect = _finalSelectionRect!;
+
+ // Check corners first
+ if (Rect.fromCircle(
+ center: rect.topLeft,
+ radius: _handleSize,
+ ).contains(pos))
+ return DragHandle.topLeft;
+ if (Rect.fromCircle(
+ center: rect.topRight,
+ radius: _handleSize,
+ ).contains(pos))
+ return DragHandle.topRight;
+ if (Rect.fromCircle(
+ center: rect.bottomLeft,
+ radius: _handleSize,
+ ).contains(pos))
+ return DragHandle.bottomLeft;
+ if (Rect.fromCircle(
+ center: rect.bottomRight,
+ radius: _handleSize,
+ ).contains(pos))
+ return DragHandle.bottomRight;
+ if (rect.contains(pos)) return DragHandle.center;
+ return DragHandle.none;
+ }
+
void _onPanStart(DragStartDetails details) {
if (_isDrawMode) {
- // START DRAWING
final pos = _getLocalPosition(details.globalPosition);
if (pos == null) return;
setState(() {
@@ -218,409 +195,166 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> {
_currentPos = pos;
});
} else if (_isResizing && _finalSelectionRect != null) {
- // START RESIZING/MOVING
- _onResizeStart(details);
+ final pos = _getLocalPosition(details.globalPosition);
+ if (pos == null) return;
+ final handle = _getDragHandle(pos);
+ if (handle != DragHandle.none) {
+ setState(() {
+ _activeHandle = handle;
+ if (handle == DragHandle.center) {
+ _startDragLocalOffset = pos - _finalSelectionRect!.topLeft;
+ }
+ });
+ }
}
}
void _onPanUpdate(DragUpdateDetails details) {
if (_isDrawMode) {
- // DRAWING
final pos = _getLocalPosition(details.globalPosition);
if (pos == null) return;
+ setState(() => _currentPos = pos);
+ } else if (_isResizing && _finalSelectionRect != null) {
+ final pos = _getLocalPosition(details.globalPosition);
+ if (pos == null || _activeHandle == DragHandle.none) return;
+
setState(() {
- _currentPos = pos;
+ Rect newRect = _finalSelectionRect!;
+ final newPoint = pos;
+
+ switch (_activeHandle) {
+ case DragHandle.topLeft:
+ newRect = Rect.fromLTRB(
+ newPoint.dx,
+ newPoint.dy,
+ newRect.right,
+ newRect.bottom,
+ );
+ break;
+ case DragHandle.topRight:
+ newRect = Rect.fromLTRB(
+ newRect.left,
+ newPoint.dy,
+ newPoint.dx,
+ newRect.bottom,
+ );
+ break;
+ case DragHandle.bottomLeft:
+ newRect = Rect.fromLTRB(
+ newPoint.dx,
+ newRect.top,
+ newRect.right,
+ newPoint.dy,
+ );
+ break;
+ case DragHandle.bottomRight:
+ newRect = Rect.fromLTRB(
+ newRect.left,
+ newRect.top,
+ newPoint.dx,
+ newPoint.dy,
+ );
+ break;
+ case DragHandle.center:
+ if (_startDragLocalOffset != null) {
+ final newTopLeft = newPoint - _startDragLocalOffset!;
+ newRect = Rect.fromLTWH(
+ newTopLeft.dx,
+ newTopLeft.dy,
+ newRect.width,
+ newRect.height,
+ );
+ }
+ break;
+ case DragHandle.none:
+ return;
+ }
+
+ if (_imageRenderSize != null) {
+ final cl = newRect.left.clamp(0.0, _imageRenderSize!.width);
+ final ct = newRect.top.clamp(0.0, _imageRenderSize!.height);
+ final cr = newRect.right.clamp(0.0, _imageRenderSize!.width);
+ final cb = newRect.bottom.clamp(0.0, _imageRenderSize!.height);
+ newRect = Rect.fromLTRB(cl, ct, cr, cb).normalize();
+ } else {
+ newRect = newRect.normalize();
+ }
+
+ if (newRect.width > 10 && newRect.height > 10)
+ _finalSelectionRect = newRect;
});
- } else if (_isResizing && _finalSelectionRect != null) {
- // RESIZING/MOVING
- _onResizeUpdate(details);
}
}
void _onPanEnd(DragEndDetails details) {
if (_isDrawMode && _startPos != null && _currentPos != null) {
- // END DRAWING, TRANSITION TO RESIZING MODE
final rect = Rect.fromPoints(_startPos!, _currentPos!).normalize();
-
if (rect.width < 10 || rect.height < 10) {
_resetSelectionMode();
return;
}
-
setState(() {
_isDrawMode = false;
- _isResizing = true; // Enter resizing/confirming mode
+ _isResizing = true;
_finalSelectionRect = rect;
_startPos = null;
_currentPos = null;
});
} else if (_isResizing) {
- // END RESIZING/MOVING
- _onResizeEnd(details);
- }
- }
-
- // --- RESIZING HANDLERS ---
- DragHandle _getDragHandle(Offset pos) {
- if (_finalSelectionRect == null) return DragHandle.none;
-
- final rect = _finalSelectionRect!;
-
- // Check corners
- if (Rect.fromCircle(
- center: rect.topLeft,
- radius: _handleSize,
- ).contains(pos)) {
- return DragHandle.topLeft;
- } else if (Rect.fromCircle(
- center: rect.topRight,
- radius: _handleSize,
- ).contains(pos)) {
- return DragHandle.topRight;
- } else if (Rect.fromCircle(
- center: rect.bottomLeft,
- radius: _handleSize,
- ).contains(pos)) {
- return DragHandle.bottomLeft;
- } else if (Rect.fromCircle(
- center: rect.bottomRight,
- radius: _handleSize,
- ).contains(pos)) {
- return DragHandle.bottomRight;
- }
- // Check if dragging the whole box (center)
- else if (rect.contains(pos)) {
- return DragHandle.center;
- }
-
- return DragHandle.none;
- }
-
- void _onResizeStart(DragStartDetails details) {
- if (!_isResizing || _finalSelectionRect == null) return;
-
- final pos = _getLocalPosition(details.globalPosition);
- if (pos == null) return;
-
- final handle = _getDragHandle(pos);
- if (handle != DragHandle.none) {
setState(() {
- _activeHandle = handle;
- // Calculate offset for moving the entire rect
- if (handle == DragHandle.center) {
- _startDragLocalOffset = pos - _finalSelectionRect!.topLeft;
- }
+ _activeHandle = DragHandle.none;
+ _startDragLocalOffset = null;
});
}
}
- void _onResizeUpdate(DragUpdateDetails details) {
- if (!_isResizing ||
- _finalSelectionRect == null ||
- _activeHandle == DragHandle.none) {
- return;
- }
-
- final pos = _getLocalPosition(details.globalPosition);
- if (pos == null) return;
-
- setState(() {
- Rect newRect = _finalSelectionRect!;
- final newPoint = pos;
-
- switch (_activeHandle) {
- case DragHandle.topLeft:
- newRect = Rect.fromLTRB(
- newPoint.dx,
- newPoint.dy,
- newRect.right,
- newRect.bottom,
- );
- break;
- case DragHandle.topRight:
- newRect = Rect.fromLTRB(
- newRect.left,
- newPoint.dy,
- newPoint.dx,
- newRect.bottom,
- );
- break;
- case DragHandle.bottomLeft:
- newRect = Rect.fromLTRB(
- newPoint.dx,
- newRect.top,
- newRect.right,
- newPoint.dy,
- );
- break;
- case DragHandle.bottomRight:
- newRect = Rect.fromLTRB(
- newRect.left,
- newRect.top,
- newPoint.dx,
- newPoint.dy,
- );
- break;
- case DragHandle.center:
- if (_startDragLocalOffset != null) {
- final newTopLeft = newPoint - _startDragLocalOffset!;
- newRect = Rect.fromLTWH(
- newTopLeft.dx,
- newTopLeft.dy,
- newRect.width,
- newRect.height,
- );
- }
- break;
- case DragHandle.none:
- return;
- }
-
- // Clamp the final rectangle to the image boundaries (0,0 to width, height)
- final imageSize = _imageRenderSize;
- if (imageSize != null) {
- final clampedLeft = newRect.left.clamp(0.0, imageSize.width);
- final clampedTop = newRect.top.clamp(0.0, imageSize.height);
- final clampedRight = newRect.right.clamp(0.0, imageSize.width);
- final clampedBottom = newRect.bottom.clamp(0.0, imageSize.height);
-
- newRect =
- Rect.fromLTRB(
- clampedLeft,
- clampedTop,
- clampedRight,
- clampedBottom,
- ).normalize();
- } else {
- newRect = newRect.normalize();
- }
-
- // Ensure min size
- if (newRect.width > 10 && newRect.height > 10) {
- _finalSelectionRect = newRect;
- }
- });
- }
-
- void _onResizeEnd(DragEndDetails details) {
- if (!_isResizing) return;
- setState(() {
- _activeHandle = DragHandle.none;
- _startDragLocalOffset = null;
- });
- }
-
- // --- ADD NOTE INPUT DIALOG ---
+ // Show modal
void _showAddNoteInputDialog() {
- final TextEditingController newNoteController = TextEditingController();
- // Default to the first tag or 'Compositions', ensure it exists in the list to prevent crashes
- String newCategory =
+ String initialCategory =
_allAvailableTags.contains('Compositions')
? 'Compositions'
- : (_allAvailableTags.isNotEmpty
- ? _allAvailableTags.first
- : 'General');
+ : (_allAvailableTags.firstOrNull ?? 'General');
showModalBottomSheet(
context: context,
isScrollControlled: true,
- backgroundColor: Colors.transparent, // Crucial for the overlay wrapper
+ backgroundColor: Colors.transparent,
builder: (context) {
- final mediaQuery = MediaQuery.of(context);
-
- // 1. Wrap content in StatefulBuilder for Dropdown updates
- final modalContent = StatefulBuilder(
- builder: (BuildContext context, StateSetter setModalState) {
- return Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- Padding(
- padding: const EdgeInsets.symmetric(
- horizontal: 20,
- vertical: 20,
- ),
- child: Column(
- children: [
- // --- ROW 1: Header (Avatar, Name, Pill Dropdown) ---
- Row(
- children: [
- // Avatar
- const CircleAvatar(
- radius: 18,
- backgroundColor: Colors.grey,
- child: Icon(
- Icons.person,
- color: Colors.white,
- size: 20,
- ),
- ),
- const SizedBox(width: 12),
-
- // Name
- const Text(
- "User",
- style: TextStyle(
- fontWeight: FontWeight.bold,
- fontSize: 16,
- color: Colors.black87,
- ),
- ),
-
- const Spacer(),
-
- // Dropdown
- Container(
- height: 40,
- padding: const EdgeInsets.symmetric(horizontal: 16),
- decoration: BoxDecoration(
- color: const Color(0xFFE0E5FF),
- borderRadius: BorderRadius.circular(20),
- ),
- child: DropdownButtonHideUnderline(
- child: DropdownButton<String>(
- value:
- _allAvailableTags.contains(newCategory)
- ? newCategory
- : null,
- hint: const Text("Type"),
- isDense: true,
- icon: const Icon(
- Icons.keyboard_arrow_down,
- size: 20,
- color: Colors.black54,
- ),
- style: const TextStyle(
- fontSize: 14,
- fontWeight: FontWeight.w600,
- color: Colors.black87,
- ),
- focusColor: Colors.transparent,
- dropdownColor: Colors.white,
- items:
- _allAvailableTags
- .map(
- (c) => DropdownMenuItem(
- value: c,
- child: Text(c),
- ),
- )
- .toList(),
- onChanged: (v) {
- // Update local state for the modal
- setModalState(() {
- newCategory = v!;
- });
- },
- ),
- ),
- ),
- ],
- ),
-
- const SizedBox(height: 20),
-
- // --- ROW 2: Input Field & Send Button ---
- Row(
- crossAxisAlignment: CrossAxisAlignment.end,
- children: [
- Expanded(
- child: TextField(
- controller: newNoteController,
- autofocus: true,
- maxLines: 1,
- style: const TextStyle(fontSize: 14),
- decoration: InputDecoration(
- filled: true,
- fillColor: const Color(0xFFF3F4F6),
- hintText: "Enter note details...",
- hintStyle: TextStyle(color: Colors.grey[600]),
- contentPadding: const EdgeInsets.symmetric(
- horizontal: 16,
- vertical: 14,
- ),
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(12),
- borderSide: BorderSide.none,
- ),
- ),
- ),
- ),
- const SizedBox(width: 12),
-
- // Send Button
- Container(
- margin: const EdgeInsets.only(bottom: 2),
- child: IconButton(
- icon: const Icon(Icons.send_outlined),
- color: Colors.black87,
- iconSize: 28,
- onPressed: () async {
- if (newNoteController.text.isNotEmpty &&
- _finalSelectionRect != null &&
- _imageRenderSize != null) {
- // Normalization logic
- final normalizedRect =
- _finalSelectionRect!.normalize();
- final nX =
- normalizedRect.center.dx /
- _imageRenderSize!.width;
- final nY =
- normalizedRect.center.dy /
- _imageRenderSize!.height;
- final nW =
- normalizedRect.width /
- _imageRenderSize!.width;
- final nH =
- normalizedRect.height /
- _imageRenderSize!.height;
-
- await _noteService.addNote(
- widget.imageId,
- newNoteController.text.trim(),
- newCategory,
- normX: nX,
- normY: nY,
- normWidth: nW,
- normHeight: nH,
- );
-
- final updatedNotes = await _noteService
- .getNotesForImage(widget.imageId);
-
- // Update the main state of ImageDetailsPage
- if (mounted) {
- setState(() {
- _notes = updatedNotes;
- _finalSelectionRect =
- null; // Clear selection
- });
- }
-
- if (context.mounted) Navigator.pop(context);
- }
- },
- ),
- ),
- ],
- ),
- ],
- ),
- ),
- const SizedBox(height: 10),
- ],
- );
- },
- );
-
return NoteModalOverlay(
- modalContent: modalContent,
- screenSize: mediaQuery.size,
+ screenSize: MediaQuery.of(context).size,
+ modalContent: NoteInputSheet(
+ categories: _allAvailableTags,
+ initialCategory: initialCategory,
+ onSubmit: (content, category) async {
+ if (_finalSelectionRect != null && _imageRenderSize != null) {
+ final normRect = _finalSelectionRect!.normalize();
+ await _noteService.addNote(
+ widget.imageId,
+ content,
+ category,
+ normX: normRect.center.dx / _imageRenderSize!.width,
+ normY: normRect.center.dy / _imageRenderSize!.height,
+ normWidth: normRect.width / _imageRenderSize!.width,
+ normHeight: normRect.height / _imageRenderSize!.height,
+ );
+ final updatedNotes = await _noteService.getNotesForImage(
+ widget.imageId,
+ );
+ if (mounted)
+ setState(() {
+ _notes = updatedNotes;
+ _finalSelectionRect = null;
+ });
+ if (context.mounted) Navigator.pop(context);
+ }
+ },
+ ),
);
},
);
}
- // --- EDIT TAGS DIALOG ---
+ // Edit Tags Dialog
void _openEditTagsDialog() {
showDialog(
context: context,
@@ -641,15 +375,13 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> {
_allAvailableTags.map((tag) {
final isSelected = tempTags.contains(tag);
return GestureDetector(
- onTap: () {
- setState(() {
- if (isSelected) {
- tempTags.remove(tag);
- } else {
- tempTags.add(tag);
- }
- });
- },
+ onTap:
+ () => setState(
+ () =>
+ isSelected
+ ? tempTags.remove(tag)
+ : tempTags.add(tag),
+ ),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
@@ -666,35 +398,21 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> {
isSelected
? const Color(0xFF7C4DFF)
: Colors.grey[300]!,
- width: 1.0,
),
),
- child: Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- if (isSelected) ...[
- const Icon(
- Icons.close,
- 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,
- ),
- ),
- ],
+ child: Text(
+ tag,
+ style: TextStyle(
+ fontSize: 13,
+ fontWeight:
+ isSelected
+ ? FontWeight.w600
+ : FontWeight.normal,
+ color:
+ isSelected
+ ? const Color(0xFF7C4DFF)
+ : Colors.black87,
+ ),
),
),
);
@@ -797,7 +515,7 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> {
)
: null,
actions: [
- // 1. CONFIRM SELECTION (Resizing mode)
+ // 1. Confirm Selection (Resizing mode)
if (_isResizing && _finalSelectionRect != null)
IconButton(
icon: const Icon(
@@ -807,14 +525,14 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> {
onPressed: _confirmSelectionAndShowModal,
),
- // 2. CANCEL SELECTION (Any selection mode)
+ // 2. Cancel Selection (Any selection mode)
if (isSelectionModeActive)
IconButton(
icon: const Icon(Icons.close, color: Colors.black),
onPressed: _resetSelectionMode,
),
- // 3. STANDARD ACTIONS (when NOT selecting)
+ // 3. Standard Actions (when NOT selecting)
if (!isSelectionModeActive && _imageModel != null) ...[
// Share Button
IconButton(
@@ -862,7 +580,7 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- // --- 1. IMAGE CONTAINER ---
+ // 1. Image Container
Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
constraints: const BoxConstraints(maxHeight: 500),
@@ -874,18 +592,6 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> {
borderRadius: BorderRadius.circular(20),
child: LayoutBuilder(
builder: (context, constraints) {
- // Unified Pan Handlers
- final onPanStartHandler =
- isSelectionModeActive
- ? _onPanStart
- : null;
- final onPanUpdateHandler =
- isSelectionModeActive
- ? _onPanUpdate
- : null;
- final onPanEndHandler =
- isSelectionModeActive ? _onPanEnd : null;
-
return Stack(
fit: StackFit.passthrough,
children: [
@@ -894,9 +600,9 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> {
panEnabled: !isSelectionModeActive,
scaleEnabled: !isSelectionModeActive,
child: GestureDetector(
- onPanStart: onPanStartHandler,
- onPanUpdate: onPanUpdateHandler,
- onPanEnd: onPanEndHandler,
+ onPanStart: _onPanStart,
+ onPanUpdate: _onPanUpdate,
+ onPanEnd: _onPanEnd,
child: Stack(
children: [
Image.file(
@@ -906,14 +612,14 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> {
width: double.infinity,
),
- // DRAWING OVERLAY (if in drawing mode)
+ // Drawing Overlay (if in drawing mode)
if (_isDrawMode &&
_startPos != null &&
_currentPos != null)
Positioned.fill(
child: CustomPaint(
painter:
- ResizingSelectionOverlayPainter(
+ SelectionOverlayPainter(
rect:
Rect.fromPoints(
_startPos!,
@@ -924,13 +630,13 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> {
),
),
- // FINAL SELECTION RECT (if in resizing mode)
+ // Final Selection Rect (if in resizing mode)
if (_isResizing &&
_finalSelectionRect != null)
Positioned.fill(
child: CustomPaint(
painter:
- ResizingSelectionOverlayPainter(
+ SelectionOverlayPainter(
rect:
_finalSelectionRect!,
isResizing: true,
@@ -950,9 +656,6 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> {
note.normX * constraints.maxWidth;
final y =
note.normY * constraints.maxHeight;
- final isActive =
- note.id == _activeNoteId;
-
return Positioned(
left: x - 10,
top: y - 10,
@@ -970,13 +673,13 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> {
boxShadow: [
BoxShadow(
color: Colors.black
- .withValues(alpha: 0.3),
+ .withOpacity(0.3),
blurRadius: 4,
offset: const Offset(0, 1),
),
],
border:
- isActive
+ note.id == _activeNoteId
? Border.all(
color: const Color(
0xFF7C4DFF,
@@ -1012,7 +715,6 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> {
),
),
child: Row(
- mainAxisSize: MainAxisSize.min,
children: const [
Text(
"Notes",
@@ -1031,7 +733,7 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> {
),
),
- // INSTRUCTION OVERLAYS
+ // Instruction Overlays
if (_isDrawMode && _startPos == null)
Positioned(
top: 20,
@@ -1097,7 +799,7 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> {
const SizedBox(height: 20),
- // --- 2. INFO SECTION ---
+ // 2. Info Section
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
@@ -1105,10 +807,9 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> {
children: [
Text(
_imageModel?.name ?? "Untitled Image",
- style: const TextStyle(
- fontWeight: FontWeight.bold,
+ style: Variables.bodyStyle.copyWith(
fontSize: 16,
- color: Colors.black87,
+ fontWeight: FontWeight.bold,
),
),
@@ -1153,8 +854,6 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> {
color: Colors.grey[300],
),
const SizedBox(height: 16),
-
- // Main View: Only show selected tags
SizedBox(
width: double.infinity,
child:
@@ -1169,33 +868,33 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> {
spacing: 8,
runSpacing: 8,
children:
- _currentTags.map((tag) {
- return Container(
- padding:
- const EdgeInsets.symmetric(
- horizontal: 12,
- vertical: 6,
- ),
- decoration: BoxDecoration(
- color: const Color(
- 0xFFEEF0FF,
- ),
- borderRadius:
- BorderRadius.circular(
- 20,
- ),
- border: Border.all(
- color:
- const Color(
+ _currentTags
+ .map(
+ (tag) => Container(
+ padding:
+ const EdgeInsets.symmetric(
+ horizontal:
+ 12,
+ vertical: 6,
+ ),
+ decoration: BoxDecoration(
+ color:
+ const Color(
+ 0xFFEEF0FF,
+ ),
+ borderRadius:
+ BorderRadius.circular(
+ 20,
+ ),
+ border: Border.all(
+ color: const Color(
0xFF7C4DFF,
),
- width: 1,
- ),
- ),
- child: Text(
- tag,
- style:
- const TextStyle(
+ ),
+ ),
+ child: Text(
+ tag,
+ style: const TextStyle(
fontSize: 13,
fontWeight:
FontWeight
@@ -1204,9 +903,10 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> {
0xFF7C4DFF,
),
),
- ),
- );
- }).toList(),
+ ),
+ ),
+ )
+ .toList(),
),
),
],
@@ -1226,25 +926,22 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> {
}
}
-// --- NOTES LIST SHEET ---
+// Notes List Sheet
class _NotesListSheet extends StatefulWidget {
final List<NoteModel> notes;
final int? highlightId;
final VoidCallback onAddNotePressed;
-
const _NotesListSheet({
required this.notes,
this.highlightId,
required this.onAddNotePressed,
});
-
@override
State<_NotesListSheet> createState() => __NotesListSheetState();
}
class __NotesListSheetState extends State<_NotesListSheet> {
final ScrollController _scrollController = ScrollController();
-
@override
void initState() {
super.initState();
@@ -1253,13 +950,12 @@ class __NotesListSheetState extends State<_NotesListSheet> {
final index = widget.notes.indexWhere(
(n) => n.id == widget.highlightId,
);
- if (index != -1 && _scrollController.hasClients) {
+ if (index != -1 && _scrollController.hasClients)
_scrollController.animateTo(
index * 80.0,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
- }
});
}
}
@@ -1308,85 +1004,78 @@ class __NotesListSheetState extends State<_NotesListSheet> {
),
const Divider(),
Expanded(
- child: ListView.builder(
- controller: controller,
- itemCount: widget.notes.length,
- itemBuilder: (context, index) {
- final note = widget.notes[index];
- final isHighlighted = note.id == widget.highlightId;
-
- return Container(
- margin: const EdgeInsets.only(bottom: 12),
- padding: const EdgeInsets.all(16),
- decoration: BoxDecoration(
- color:
- isHighlighted
- ? const Color(0xFFF3F0FF)
- : Colors.white,
- borderRadius: BorderRadius.circular(12),
- border: Border.all(
- color:
- isHighlighted
- ? const Color(0xFF7C4DFF)
- : Colors.grey[200]!,
- width: isHighlighted ? 1.5 : 1,
- ),
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Align(
- alignment: Alignment.centerLeft,
- child: Container(
- padding: const EdgeInsets.symmetric(
- horizontal: 10,
- vertical: 4,
- ),
+ child:
+ widget.notes.isEmpty
+ ? const EmptyState(
+ icon: Icons.comment,
+ title: "No notes yet",
+ subtitle: "Tap 'Add Note' to start",
+ )
+ : ListView.builder(
+ controller: controller,
+ itemCount: widget.notes.length,
+ itemBuilder: (context, index) {
+ final note = widget.notes[index];
+ final isHighlighted = note.id == widget.highlightId;
+ return Container(
+ margin: const EdgeInsets.only(bottom: 12),
+ padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
- color: const Color(0xFFEEF0FF),
- borderRadius: BorderRadius.circular(6),
- ),
- child: Text(
- note.category,
- style: const TextStyle(
- fontSize: 11,
- fontWeight: FontWeight.w700,
- color: Color(0xFF7C4DFF),
+ color:
+ isHighlighted
+ ? const Color(0xFFF3F0FF)
+ : Colors.white,
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(
+ color:
+ isHighlighted
+ ? const Color(0xFF7C4DFF)
+ : Colors.grey[200]!,
+ width: isHighlighted ? 1.5 : 1,
),
),
- ),
- ),
- const SizedBox(height: 8),
- Text(
- note.content,
- style: const TextStyle(
- fontSize: 15,
- color: Colors.black87,
- height: 1.4,
- ),
- ),
- ],
- ),
- );
- },
- ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 10,
+ vertical: 4,
+ ),
+ decoration: BoxDecoration(
+ color: const Color(0xFFEEF0FF),
+ borderRadius: BorderRadius.circular(6),
+ ),
+ child: Text(
+ note.category,
+ style: const TextStyle(
+ fontSize: 11,
+ fontWeight: FontWeight.w700,
+ color: Color(0xFF7C4DFF),
+ ),
+ ),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ note.content,
+ style: const TextStyle(
+ fontSize: 15,
+ color: Colors.black87,
+ height: 1.4,
+ ),
+ ),
+ ],
+ ),
+ );
+ },
+ ),
),
- // --- ADD NOTE BUTTON (BOTTOM OF SHEET) ---
+ // Add Note Button
const SizedBox(height: 10),
- SizedBox(
- width: double.infinity,
- height: 50,
- child: FilledButton.icon(
- onPressed: widget.onAddNotePressed,
- icon: const Icon(Icons.add),
- label: const Text("Add Note"),
- style: FilledButton.styleFrom(
- backgroundColor: Colors.black,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(12),
- ),
- ),
- ),
+ PrimaryButton(
+ text: "Add Note",
+ onPressed: widget.onAddNotePressed,
+ iconPath: "assets/icons/add-line.svg",
),
const SizedBox(height: 10),
],
@@ -1396,156 +1085,3 @@ class __NotesListSheetState extends State<_NotesListSheet> {
);
}
}
-
-// --- REUSABLE WIDGETS FOR MODAL OVERLAY ---
-
-class NoteModalOverlay extends StatelessWidget {
- final Widget modalContent;
- final Size screenSize;
-
- const NoteModalOverlay({
- super.key,
- required this.modalContent,
- required this.screenSize,
- });
-
- @override
- Widget build(BuildContext context) {
- // Note: modalMinHeight is kept only for potential use with MaxHeight, but is not enforced as a minimum
- final mq = MediaQuery.of(context);
- final keyboardHeight = mq.viewInsets.bottom;
- final systemBottomPadding = mq.padding.bottom;
-
- return Align(
- alignment: Alignment.bottomCenter,
- // Use AnimatedPadding on the outside to correctly handle keyboard elevation smoothly
- child: AnimatedPadding(
- duration: const Duration(milliseconds: 250),
- curve: Curves.easeOut,
- padding: EdgeInsets.only(
- bottom: keyboardHeight, // Moves modal up to avoid keyboard
- ),
- child: ConstrainedBox(
- constraints: BoxConstraints(maxHeight: screenSize.height),
- child: Material(
- color: Colors.white,
- elevation: 10,
- shadowColor: Colors.black26,
- borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
- clipBehavior: Clip.antiAlias,
- child: SingleChildScrollView(
- // Allows the content inside to scroll if keyboard reduces available space
- child: Padding(
- // Only apply system bottom padding for safe area/gesture bar here
- padding: EdgeInsets.only(bottom: systemBottomPadding),
- child: modalContent,
- ),
- ),
- ),
- ),
- ),
- );
- }
-}
-
-// --- FULL OVERLAY PAINTER WITH RESIZING LOGIC ---
-class ResizingSelectionOverlayPainter extends CustomPainter {
- final Rect rect;
- final bool isResizing;
- final DragHandle activeHandle;
-
- ResizingSelectionOverlayPainter({
- required this.rect,
- required this.isResizing,
- this.activeHandle = DragHandle.none,
- });
-
- @override
- void paint(Canvas canvas, Size size) {
- // 1. DIM BACKGROUND (Black overlay with hole for the selected area)
- if (isResizing) {
- final Path backgroundPath =
- Path()..addRect(Rect.fromLTWH(0, 0, size.width, size.height));
- final Path holePath = Path()..addRect(rect);
- final Path overlayPath = Path.combine(
- ui.PathOperation.difference,
- backgroundPath,
- holePath,
- );
-
- canvas.drawPath(overlayPath, Paint()..color = Colors.black54);
- }
-
- // 2. DRAW DASHED BORDER
- final Paint borderPaint =
- Paint()
- ..color = const Color(0xFF448AFF)
- ..strokeWidth = 2.0
- ..style = PaintingStyle.stroke;
-
- double dashWidth = 6;
- double dashSpace = 4;
- Path borderPath = Path()..addRect(rect);
-
- for (ui.PathMetric pathMetric in borderPath.computeMetrics()) {
- double distance = 0.0;
- while (distance < pathMetric.length) {
- canvas.drawPath(
- pathMetric.extractPath(distance, distance + dashWidth),
- borderPaint,
- );
- distance += (dashWidth + dashSpace);
- }
- }
-
- // 3. DRAW CENTER DOT/RESIZE HANDLES
- if (!isResizing) {
- // Draw center dot in initial draw mode
- final Paint dotPaint =
- Paint()
- ..color = Colors.white
- ..style = PaintingStyle.fill;
-
- canvas.drawCircle(
- rect.center,
- 8,
- Paint()
- ..color = Colors.black26
- ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 3),
- );
- canvas.drawCircle(rect.center, 6, dotPaint);
- } else {
- // Draw resize handles in resizing mode
- final List<Offset> corners = [
- rect.topLeft,
- rect.topRight,
- rect.bottomLeft,
- rect.bottomRight,
- ];
-
- const double handleRadius = 8;
- final Paint handleShadow =
- Paint()
- ..color = Colors.black.withValues(alpha: 0.3)
- ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 2);
- final Paint handleFill = Paint()..color = Colors.white;
- final Paint handleBorder =
- Paint()
- ..color = const Color(0xFF448AFF)
- ..strokeWidth = 2
- ..style = PaintingStyle.stroke;
-
- for (final corner in corners) {
- canvas.drawCircle(corner, handleRadius, handleShadow);
- canvas.drawCircle(corner, handleRadius, handleFill);
- canvas.drawCircle(corner, handleRadius, handleBorder);
- }
- }
- }
-
- @override
- bool shouldRepaint(covariant ResizingSelectionOverlayPainter oldDelegate) =>
- rect != oldDelegate.rect ||
- isResizing != oldDelegate.isResizing ||
- activeHandle != oldDelegate.activeHandle;
-}
diff --git a/lib/ui/pages/image_save_page.dart b/lib/ui/pages/image_save_page.dart
@@ -1,20 +1,18 @@
import 'dart:io';
-import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:creekui/services/image_service.dart';
import 'package:creekui/services/note_service.dart';
+import 'package:creekui/ui/styles/variables.dart';
+import 'package:creekui/ui/widgets/primary_button.dart';
+import 'package:creekui/ui/widgets/selection_overlay_painter.dart';
+import 'package:creekui/ui/widgets/note_input_sheet.dart';
import 'project_board_page.dart';
-// --- HELPER CLASS FOR TEMPORARY NOTES ---
+// Temporary note model
class TempNote {
- final double normX;
- final double normY;
- final double normWidth;
- final double normHeight;
- final String content;
- final String category;
-
+ final double normX, normY, normWidth, normHeight;
+ final String content, category;
TempNote({
required this.normX,
required this.normY,
@@ -45,37 +43,22 @@ class ImageSavePage extends StatefulWidget {
State<ImageSavePage> createState() => _ImageSavePageState();
}
-// --- STATE MACHINE FOR SELECTION MODE ---
-enum DragHandle {
- none,
- topLeft,
- topRight,
- bottomLeft,
- bottomRight,
- center, // For dragging the entire box
-}
-
class _ImageSavePageState extends State<ImageSavePage> {
- // --- SERVICES ---
final ImageService _imageService = ImageService();
final NoteService _noteService = NoteService();
- // --- STATE ---
late PageController _pageController;
int _currentImageIndex = 0;
- // Data storage per image
final Map<int, Set<String>> _tagsPerImage = {};
final Map<int, List<TempNote>> _notesPerImage = {};
- // CRITICAL: A unique key for EACH image to calculate drawing coordinates correctly
+ // A unique key for each image to calculate drawing coordinates correctly
late List<GlobalKey> _imageKeys;
- final TextEditingController _commentController = TextEditingController();
- String _selectedCategory = 'Compositions';
bool _isSaving = false;
- // --- DRAWING/RESIZING STATE ---
+ // Drawing/Resizing state
bool _isDrawMode =
false; // True when initial drag is happening (to create box)
bool _isResizing =
@@ -88,6 +71,7 @@ class _ImageSavePageState extends State<ImageSavePage> {
DragHandle _activeHandle = DragHandle.none;
Offset? _startDragLocalOffset; // Used for moving the entire rect
final Map<int, Size> _imageRenderSizes = {};
+ final double _handleSize = 25.0;
final List<String> _availableTags = [
'Compositions',
@@ -103,20 +87,6 @@ class _ImageSavePageState extends State<ImageSavePage> {
'Emotion',
];
- final List<String> _categories = [
- 'Compositions',
- 'Subject',
- 'Fonts',
- 'Background',
- 'Texture',
- 'Colours',
- 'Material Look',
- 'Lighting',
- 'Style',
- 'Era',
- 'Emotion',
- ];
-
@override
void initState() {
super.initState();
@@ -124,8 +94,6 @@ class _ImageSavePageState extends State<ImageSavePage> {
// Generate a unique key for every image path
_imageKeys = List.generate(widget.imagePaths.length, (_) => GlobalKey());
-
- // Initialize data maps
for (int i = 0; i < widget.imagePaths.length; i++) {
_tagsPerImage[i] = {};
_notesPerImage[i] = [];
@@ -135,11 +103,10 @@ class _ImageSavePageState extends State<ImageSavePage> {
@override
void dispose() {
_pageController.dispose();
- _commentController.dispose();
super.dispose();
}
- // --- ACTIONS ---
+ // Actions
void _activateSelectionMode() {
setState(() {
_isDrawMode = true; // Start initial drawing mode
@@ -164,507 +131,238 @@ class _ImageSavePageState extends State<ImageSavePage> {
void _confirmSelectionAndShowModal() {
if (_finalSelectionRect != null) {
// Exit resizing mode before showing the modal to prevent visual conflict
- setState(() {
- _isResizing = false;
- });
+ setState(() => _isResizing = false);
_showNoteModal();
}
}
void _toggleTag(String tag) {
setState(() {
- final currentTags = _tagsPerImage[_currentImageIndex]!;
- if (currentTags.contains(tag)) {
- currentTags.remove(tag);
- } else {
- currentTags.add(tag);
- }
+ final tags = _tagsPerImage[_currentImageIndex]!;
+ if (tags.contains(tag))
+ tags.remove(tag);
+ else
+ tags.add(tag);
});
}
- // --- DRAWING/RESIZING GESTURES (UPDATED LOGIC) ---
- final double _handleSize =
- 25.0; // The size of the touch area for resizing handles
-
- // Helper: Convert global screen touch to local image coordinates
+ // Gestures
Offset? _getLocalPosition(Offset globalPosition) {
final currentKey = _imageKeys[_currentImageIndex];
final RenderBox? box =
currentKey.currentContext?.findRenderObject() as RenderBox?;
if (box == null) return null;
_imageRenderSizes[_currentImageIndex] = box.size;
-
- // Convert global to local
final local = box.globalToLocal(globalPosition);
// Clamp coordinates to ensure we don't draw/drag outside the image
final dx = local.dx.clamp(0.0, box.size.width);
final dy = local.dy.clamp(0.0, box.size.height);
-
return Offset(dx, dy);
}
- // --- INITIAL DRAWING HANDLERS ---
- void _onPanStart(DragStartDetails details) {
- if (!_isDrawMode) return;
-
- final pos = _getLocalPosition(details.globalPosition);
- if (pos == null) return;
-
- setState(() {
- _startPos = pos;
- _currentPos = pos;
- });
- }
-
- void _onPanUpdate(DragUpdateDetails details) {
- if (!_isDrawMode) return;
-
- final pos = _getLocalPosition(details.globalPosition);
- if (pos == null) return;
-
- setState(() {
- _currentPos = pos;
- });
- }
-
- void _onPanEnd(DragEndDetails details) {
- if (!_isDrawMode || _startPos == null || _currentPos == null) return;
-
- // Create the rect from the corrected local positions, normalizing points
- final rect = Rect.fromPoints(_startPos!, _currentPos!).normalize();
-
- // Check if the selected area is too small
- if (rect.width < 10 || rect.height < 10) {
- _resetSelectionMode();
- return;
- }
-
- setState(() {
- _isDrawMode = false;
- _isResizing = true; // Enter resizing/confirming mode
- _finalSelectionRect = rect;
- _startPos = null;
- _currentPos = null;
- });
- }
-
- // --- RESIZING HANDLERS ---
DragHandle _getDragHandle(Offset pos) {
if (_finalSelectionRect == null) return DragHandle.none;
-
final rect = _finalSelectionRect!;
- // final center = rect.center; // Not used but helpful for context
- // final top = rect.top;
- // final bottom = rect.bottom;
- // final left = rect.left;
- // final right = rect.right;
- // Check corners
+ // Check corners first
if (Rect.fromCircle(
center: rect.topLeft,
radius: _handleSize,
- ).contains(pos)) {
+ ).contains(pos))
return DragHandle.topLeft;
- } else if (Rect.fromCircle(
+ if (Rect.fromCircle(
center: rect.topRight,
radius: _handleSize,
- ).contains(pos)) {
+ ).contains(pos))
return DragHandle.topRight;
- } else if (Rect.fromCircle(
+ if (Rect.fromCircle(
center: rect.bottomLeft,
radius: _handleSize,
- ).contains(pos)) {
+ ).contains(pos))
return DragHandle.bottomLeft;
- } else if (Rect.fromCircle(
+ if (Rect.fromCircle(
center: rect.bottomRight,
radius: _handleSize,
- ).contains(pos)) {
+ ).contains(pos))
return DragHandle.bottomRight;
- }
- // Check if dragging the whole box (center)
- else if (rect.contains(pos)) {
- // Only allow center drag if we are not actively drawing (i.e. we are in resizing mode)
- return DragHandle.center;
- }
-
+ if (rect.contains(pos)) return DragHandle.center;
return DragHandle.none;
}
- void _onResizeStart(DragStartDetails details) {
- if (!_isResizing || _finalSelectionRect == null) return;
-
- final pos = _getLocalPosition(details.globalPosition);
- if (pos == null) return;
-
- final handle = _getDragHandle(pos);
- if (handle != DragHandle.none) {
+ void _onPanStart(DragStartDetails details) {
+ if (_isDrawMode) {
+ final pos = _getLocalPosition(details.globalPosition);
+ if (pos == null) return;
setState(() {
- _activeHandle = handle;
- // Calculate offset for moving the entire rect, not for resizing
- if (handle == DragHandle.center) {
- _startDragLocalOffset = pos - _finalSelectionRect!.topLeft;
- }
+ _startPos = pos;
+ _currentPos = pos;
});
+ } else if (_isResizing && _finalSelectionRect != null) {
+ final pos = _getLocalPosition(details.globalPosition);
+ if (pos == null) return;
+ final handle = _getDragHandle(pos);
+ if (handle != DragHandle.none) {
+ setState(() {
+ _activeHandle = handle;
+ if (handle == DragHandle.center) {
+ _startDragLocalOffset = pos - _finalSelectionRect!.topLeft;
+ }
+ });
+ }
}
}
- void _onResizeUpdate(DragUpdateDetails details) {
- if (!_isResizing ||
- _finalSelectionRect == null ||
- _activeHandle == DragHandle.none) {
- return;
- }
-
- final pos = _getLocalPosition(details.globalPosition);
- if (pos == null) return;
-
- setState(() {
- Rect newRect = _finalSelectionRect!;
- final newPoint = pos;
+ void _onPanUpdate(DragUpdateDetails details) {
+ if (_isDrawMode) {
+ final pos = _getLocalPosition(details.globalPosition);
+ if (pos == null) return;
+ setState(() => _currentPos = pos);
+ } else if (_isResizing && _finalSelectionRect != null) {
+ final pos = _getLocalPosition(details.globalPosition);
+ if (pos == null || _activeHandle == DragHandle.none) return;
- switch (_activeHandle) {
- case DragHandle.topLeft:
- newRect = Rect.fromLTRB(
- newPoint.dx,
- newPoint.dy,
- newRect.right,
- newRect.bottom,
- );
- break;
- case DragHandle.topRight:
- newRect = Rect.fromLTRB(
- newRect.left,
- newPoint.dy,
- newPoint.dx,
- newRect.bottom,
- );
- break;
- case DragHandle.bottomLeft:
- newRect = Rect.fromLTRB(
- newPoint.dx,
- newRect.top,
- newRect.right,
- newPoint.dy,
- );
- break;
- case DragHandle.bottomRight:
- newRect = Rect.fromLTRB(
- newRect.left,
- newRect.top,
- newPoint.dx,
- newPoint.dy,
- );
- break;
- case DragHandle.center:
- if (_startDragLocalOffset != null) {
- final newTopLeft = newPoint - _startDragLocalOffset!;
- newRect = Rect.fromLTWH(
- newTopLeft.dx,
- newTopLeft.dy,
- newRect.width,
- newRect.height,
+ setState(() {
+ Rect newRect = _finalSelectionRect!;
+ final newPoint = pos;
+
+ switch (_activeHandle) {
+ case DragHandle.topLeft:
+ newRect = Rect.fromLTRB(
+ newPoint.dx,
+ newPoint.dy,
+ newRect.right,
+ newRect.bottom,
);
- }
- break;
- case DragHandle.none:
- return;
- }
-
- // Clamp the final rectangle to the image boundaries (0,0 to width, height)
- final imageSize = _imageRenderSizes[_currentImageIndex];
- if (imageSize != null) {
- final clampedLeft = newRect.left.clamp(0.0, imageSize.width);
- final clampedTop = newRect.top.clamp(0.0, imageSize.height);
- final clampedRight = newRect.right.clamp(0.0, imageSize.width);
- final clampedBottom = newRect.bottom.clamp(0.0, imageSize.height);
+ break;
+ case DragHandle.topRight:
+ newRect = Rect.fromLTRB(
+ newRect.left,
+ newPoint.dy,
+ newPoint.dx,
+ newRect.bottom,
+ );
+ break;
+ case DragHandle.bottomLeft:
+ newRect = Rect.fromLTRB(
+ newPoint.dx,
+ newRect.top,
+ newRect.right,
+ newPoint.dy,
+ );
+ break;
+ case DragHandle.bottomRight:
+ newRect = Rect.fromLTRB(
+ newRect.left,
+ newRect.top,
+ newPoint.dx,
+ newPoint.dy,
+ );
+ break;
+ case DragHandle.center:
+ if (_startDragLocalOffset != null) {
+ final newTopLeft = newPoint - _startDragLocalOffset!;
+ newRect = Rect.fromLTWH(
+ newTopLeft.dx,
+ newTopLeft.dy,
+ newRect.width,
+ newRect.height,
+ );
+ }
+ break;
+ case DragHandle.none:
+ return;
+ }
- newRect =
- Rect.fromLTRB(
- clampedLeft,
- clampedTop,
- clampedRight,
- clampedBottom,
- ).normalize();
- } else {
- newRect = newRect.normalize();
- }
+ final imageSize = _imageRenderSizes[_currentImageIndex];
+ if (imageSize != null) {
+ final cl = newRect.left.clamp(0.0, imageSize.width);
+ final ct = newRect.top.clamp(0.0, imageSize.height);
+ final cr = newRect.right.clamp(0.0, imageSize.width);
+ final cb = newRect.bottom.clamp(0.0, imageSize.height);
+ newRect = Rect.fromLTRB(cl, ct, cr, cb).normalize();
+ } else {
+ newRect = newRect.normalize();
+ }
- // Ensure min size
- if (newRect.width > 10 && newRect.height > 10) {
- _finalSelectionRect = newRect;
- }
- });
+ if (newRect.width > 10 && newRect.height > 10)
+ _finalSelectionRect = newRect;
+ });
+ }
}
- void _onResizeEnd(DragEndDetails details) {
- if (!_isResizing) return;
- setState(() {
- _activeHandle = DragHandle.none;
- _startDragLocalOffset = null;
- });
+ void _onPanEnd(DragEndDetails details) {
+ if (_isDrawMode && _startPos != null && _currentPos != null) {
+ final rect = Rect.fromPoints(_startPos!, _currentPos!).normalize();
+ if (rect.width < 10 || rect.height < 10) {
+ _resetSelectionMode();
+ return;
+ }
+ setState(() {
+ _isDrawMode = false;
+ _isResizing = true;
+ _finalSelectionRect = rect;
+ _startPos = null;
+ _currentPos = null;
+ });
+ } else if (_isResizing) {
+ setState(() {
+ _activeHandle = DragHandle.none;
+ _startDragLocalOffset = null;
+ });
+ }
}
- //ADD NOTE MODAL
+ // Modal
void _showNoteModal() {
- _commentController.clear();
- // Ensure we have a valid selection to proceed
- if (_finalSelectionRect == null) return;
-
+ String initialCategory = _availableTags.firstOrNull ?? 'General';
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
- builder: (context) {
- final mediaQuery = MediaQuery.of(context);
-
- // 1. Wrap the content variable in StatefulBuilder
- final modalContent = StatefulBuilder(
- builder: (BuildContext context, StateSetter setModalState) {
- return Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- Padding(
- padding: const EdgeInsets.all(16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- // --- ROW 1: Header ---
- Row(
- children: [
- // Avatar
- Container(
- width: 30,
- height: 30,
- decoration: BoxDecoration(
- shape: BoxShape.circle,
- border: Border.all(
- color: const Color(0xFFFAFAFA),
- width: 1.25,
- ),
- ),
- child: const CircleAvatar(
- radius: 15,
- backgroundColor: Colors.grey,
- child: Icon(
- Icons.person,
- color: Colors.white,
- size: 18,
- ),
- ),
- ),
- const SizedBox(width: 10),
-
- // User name
- const Text(
- "Alex",
- style: TextStyle(
- fontFamily: 'GeneralSans',
- fontSize: 12,
- fontWeight: FontWeight.w500,
- color: Colors.black,
- letterSpacing: 0.4,
- height: 16 / 12,
- ),
- ),
-
- const Spacer(),
-
- // Dropdown
- Container(
- padding: const EdgeInsets.symmetric(
- horizontal: 12,
- vertical: 6,
- ),
- decoration: BoxDecoration(
- color: const Color(0xFFE0E7FF),
- borderRadius: BorderRadius.circular(1000),
- ),
- child: DropdownButtonHideUnderline(
- child: DropdownButton<String>(
- value:
- _categories.contains(_selectedCategory)
- ? _selectedCategory
- : null,
- hint: const Text(
- "Type",
- style: TextStyle(
- fontFamily: 'GeneralSans',
- fontSize: 12,
- fontWeight: FontWeight.normal,
- color: Color(0xFF27272A),
- height: 16 / 12,
- ),
- ),
- isDense: true,
- icon: const Icon(
- Icons.arrow_drop_down,
- size: 20,
- color: Color(0xFF27272A),
- ),
- style: const TextStyle(
- fontFamily: 'GeneralSans',
- fontSize: 12,
- fontWeight: FontWeight.normal,
- color: Color(0xFF27272A),
- height: 16 / 12,
- ),
- focusColor: Colors.transparent,
- dropdownColor: Colors.white,
- items:
- _categories
- .map(
- (c) => DropdownMenuItem(
- value: c,
- child: Text(
- c,
- style: const TextStyle(
- fontFamily: 'GeneralSans',
- fontSize: 12,
- fontWeight: FontWeight.normal,
- ),
- ),
- ),
- )
- .toList(),
- onChanged: (v) {
- setModalState(() {
- _selectedCategory = v!;
- });
- },
- ),
- ),
- ),
- ],
- ),
-
- const SizedBox(height: 16),
-
- // --- ROW 2: Input & Send ---
- Row(
- crossAxisAlignment: CrossAxisAlignment.end,
- children: [
- Expanded(
- child: Container(
- decoration: BoxDecoration(
- color: const Color(0xFFF4F4F5),
- border: Border.all(
- color: const Color(0xFFE4E4E7),
- width: 1,
- ),
- borderRadius: BorderRadius.circular(8),
- ),
- child: TextField(
- controller: _commentController,
- autofocus: true,
- maxLines: null,
- style: const TextStyle(
- fontFamily: 'GeneralSans',
- fontSize: 12,
- fontWeight: FontWeight.normal,
- color: Color(0xFF27272A),
- height: 16 / 12,
- ),
- decoration: InputDecoration(
- hintText:
- "I love the serif font and how it is used...",
- hintStyle: const TextStyle(
- fontFamily: 'GeneralSans',
- fontSize: 12,
- fontWeight: FontWeight.normal,
- color: Color(0xFF27272A),
- height: 16 / 12,
- ),
- contentPadding: const EdgeInsets.symmetric(
- horizontal: 12,
- vertical: 10,
- ),
- border: InputBorder.none,
- enabledBorder: InputBorder.none,
- focusedBorder: InputBorder.none,
- ),
- ),
- ),
- ),
-
- const SizedBox(width: 8),
-
- // Send button
- IconButton(
- icon: const Icon(
- Icons.send,
- color: Color(0xFF27272A),
- size: 24,
- ),
- onPressed: () {
- _addTempNote();
- Navigator.pop(context);
- },
- padding: EdgeInsets.zero,
- constraints: const BoxConstraints(
- minWidth: 24,
- minHeight: 24,
- ),
- ),
- ],
- ),
- ],
- ),
- ),
- const SizedBox(height: 10),
- ],
- );
- },
- );
-
- return NoteModalOverlay(
- modalContent: modalContent,
- screenSize: mediaQuery.size,
- );
- },
+ builder:
+ (context) => NoteModalOverlay(
+ screenSize: MediaQuery.of(context).size,
+ modalContent: NoteInputSheet(
+ categories: _availableTags,
+ initialCategory: initialCategory,
+ onSubmit: (content, category) {
+ final imageSize = _imageRenderSizes[_currentImageIndex];
+ if (_finalSelectionRect != null && imageSize != null) {
+ final nX = _finalSelectionRect!.center.dx / imageSize.width;
+ final nY = _finalSelectionRect!.center.dy / imageSize.height;
+ final nW = _finalSelectionRect!.width / imageSize.width;
+ final nH = _finalSelectionRect!.height / imageSize.height;
+
+ final newNote = TempNote(
+ normX: nX,
+ normY: nY,
+ normWidth: nW,
+ normHeight: nH,
+ content: content,
+ category: category,
+ );
+ setState(() {
+ _notesPerImage[_currentImageIndex]?.add(newNote);
+ _finalSelectionRect = null;
+ });
+ Navigator.pop(context);
+ }
+ },
+ ),
+ ),
);
}
- void _addTempNote() {
- // Use the stored render size for the current image index
- final imageSize = _imageRenderSizes[_currentImageIndex];
-
- if (_finalSelectionRect != null &&
- imageSize != null &&
- _commentController.text.isNotEmpty) {
- // Calculate Normalized Coordinates (0.0 - 1.0)
- final nX = _finalSelectionRect!.center.dx / imageSize.width;
- final nY = _finalSelectionRect!.center.dy / imageSize.height;
- final nW = _finalSelectionRect!.width / imageSize.width;
- final nH = _finalSelectionRect!.height / imageSize.height;
-
- final newNote = TempNote(
- normX: nX,
- normY: nY,
- normWidth: nW,
- normHeight: nH,
- content: _commentController.text.trim(),
- category: _selectedCategory,
- );
-
- setState(() {
- _notesPerImage[_currentImageIndex]?.add(newNote);
- _finalSelectionRect = null; // Clear selection after saving note
- });
- }
- }
-
- // --- FINAL SAVE ---
+ // Final Save
Future<void> _onSaveToMoodboard() async {
setState(() => _isSaving = true);
-
try {
for (int i = 0; i < widget.imagePaths.length; i++) {
String path = widget.imagePaths[i];
final file = File(path);
if (!file.existsSync()) continue;
- // 1. Save Image
final tags = _tagsPerImage[i] ?? {};
final imageId = await _imageService.saveOrUpdateImage(
file,
@@ -672,7 +370,6 @@ class _ImageSavePageState extends State<ImageSavePage> {
tags: tags.toList(),
);
- // 2. Save Notes for this specific image
final notes = _notesPerImage[i] ?? [];
for (var note in notes) {
await _noteService.addNote(
@@ -688,30 +385,26 @@ class _ImageSavePageState extends State<ImageSavePage> {
}
if (mounted) {
- // Find the most suitable ScaffoldMessengerState
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('All images saved successfully!'),
backgroundColor: Colors.green,
),
);
-
- if (widget.isFromShare) {
+ if (widget.isFromShare)
SystemNavigator.pop();
- } else {
- // Navigate to project board page in grid view (alternate view)
+ else
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(
builder:
(_) => ProjectBoardPage(
projectId: widget.projectId,
- initialShowAlternateView: true, // Show grid view
+ initialShowAlternateView: true,
),
),
- (route) => false, // Remove all previous routes
+ (route) => false,
);
- }
}
} catch (e) {
if (mounted) {
@@ -727,7 +420,6 @@ class _ImageSavePageState extends State<ImageSavePage> {
Widget build(BuildContext context) {
final currentTags = _tagsPerImage[_currentImageIndex] ?? {};
final String parentName = widget.parentProjectName?.trim() ?? '';
-
final String titleText =
parentName.isNotEmpty
? '$parentName / ${widget.projectName}'
@@ -752,23 +444,16 @@ class _ImageSavePageState extends State<ImageSavePage> {
),
title: Text(
titleText,
- style: const TextStyle(
- fontFamily: 'GeneralSans',
- color: Colors.black,
- fontSize: 20,
- fontWeight: FontWeight.w500,
- height: 24 / 20,
- letterSpacing: 0,
- ),
+ style: Variables.headerStyle.copyWith(fontSize: 20),
),
actions: [
- // CONFIRM SELECTION BUTTON (Visible only in resizing mode)
+ // Confirm Selection Button (Visible only in resizing mode)
if (_isResizing && _finalSelectionRect != null)
IconButton(
icon: const Icon(Icons.check, color: Color(0xFF7C86FF), size: 24),
onPressed: _confirmSelectionAndShowModal,
),
- // CANCEL SELECTION BUTTON (Visible only in drawing/resizing mode)
+ // Cancel Selection Button (Visible only in drawing/resizing mode)
if (_isDrawMode || _isResizing)
IconButton(
icon: const Icon(Icons.close, color: Colors.black87, size: 24),
@@ -778,7 +463,7 @@ class _ImageSavePageState extends State<ImageSavePage> {
),
body: Column(
children: [
- // --- HORIZONTAL IMAGE CAROUSEL ---
+ // Horizontal Image Carousel
Expanded(
child: Container(
width: double.infinity,
@@ -800,12 +485,11 @@ class _ImageSavePageState extends State<ImageSavePage> {
? const NeverScrollableScrollPhysics()
: const PageScrollPhysics(),
itemCount: widget.imagePaths.length,
- onPageChanged: (index) {
- setState(() {
- _currentImageIndex = index;
- _resetSelectionMode(); // Reset selection mode on page change
- });
- },
+ onPageChanged:
+ (index) => setState(() {
+ _currentImageIndex = index;
+ _resetSelectionMode();
+ }),
itemBuilder: (context, index) {
return LayoutBuilder(
builder: (context, constraints) {
@@ -813,15 +497,24 @@ class _ImageSavePageState extends State<ImageSavePage> {
final onPanStartHandler =
_isDrawMode
? _onPanStart
- : (_isResizing ? _onResizeStart : null);
+ : (_isResizing &&
+ index == _currentImageIndex
+ ? _onPanStart
+ : null);
final onPanUpdateHandler =
_isDrawMode
? _onPanUpdate
- : (_isResizing ? _onResizeUpdate : null);
+ : (_isResizing &&
+ index == _currentImageIndex
+ ? _onPanUpdate
+ : null);
final onPanEndHandler =
_isDrawMode
? _onPanEnd
- : (_isResizing ? _onResizeEnd : null);
+ : (_isResizing &&
+ index == _currentImageIndex
+ ? _onPanEnd
+ : null);
return Stack(
fit: StackFit.expand,
@@ -837,14 +530,13 @@ class _ImageSavePageState extends State<ImageSavePage> {
onPanEnd: onPanEndHandler,
child: Stack(
children: [
- // THE IMAGE WITH UNIQUE KEY
Image.file(
File(widget.imagePaths[index]),
key: _imageKeys[index],
fit: BoxFit.contain,
width: double.infinity,
),
- // DRAWING OVERLAY (if in drawing mode)
+ // Drawing Overlay (if in drawing mode)
if (_isDrawMode &&
index == _currentImageIndex &&
_startPos != null &&
@@ -853,15 +545,16 @@ class _ImageSavePageState extends State<ImageSavePage> {
child: CustomPaint(
painter:
SelectionOverlayPainter(
- rect: Rect.fromPoints(
- _startPos!,
- _currentPos!,
- ),
+ rect:
+ Rect.fromPoints(
+ _startPos!,
+ _currentPos!,
+ ).normalize(),
isResizing: false,
),
),
),
- // FINAL SELECTION RECT (if in resizing mode)
+ // Final Selection Rect (if in resizing mode)
if (_isResizing &&
index == _currentImageIndex &&
_finalSelectionRect != null)
@@ -882,7 +575,7 @@ class _ImageSavePageState extends State<ImageSavePage> {
),
),
),
- // EXISTING NOTE INDICATORS (Dots) - visible only if no selection is active
+ // Existing Note Indicators (Dots) - visible only if no selection is active
if (!isPageLocked)
...(_notesPerImage[index] ?? []).map((note) {
return Positioned(
@@ -900,8 +593,8 @@ class _ImageSavePageState extends State<ImageSavePage> {
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
- color: Colors.black.withValues(
- alpha: 0.3,
+ color: Colors.black.withOpacity(
+ 0.3,
),
blurRadius: 4,
offset: const Offset(0, 1),
@@ -915,7 +608,7 @@ class _ImageSavePageState extends State<ImageSavePage> {
),
);
}),
- // PAGE DOTS
+ // Page Dots
if (widget.imagePaths.length > 1 &&
!isPageLocked)
Positioned(
@@ -927,7 +620,7 @@ class _ImageSavePageState extends State<ImageSavePage> {
MainAxisAlignment.center,
children: List.generate(
widget.imagePaths.length,
- (index) => Container(
+ (i) => Container(
margin: const EdgeInsets.symmetric(
horizontal: 4,
),
@@ -936,17 +629,17 @@ class _ImageSavePageState extends State<ImageSavePage> {
decoration: BoxDecoration(
shape: BoxShape.circle,
color:
- _currentImageIndex == index
+ _currentImageIndex == i
? Colors.blue
- : Colors.white.withValues(
- alpha: 0.5,
+ : Colors.white.withOpacity(
+ 0.5,
),
),
),
),
),
),
- // NOTES BUTTON (Visible only when not drawing/resizing)
+ // Notes Button (Visible only when not drawing/resizing)
if (!isPageLocked)
Positioned(
bottom: 24,
@@ -976,13 +669,11 @@ class _ImageSavePageState extends State<ImageSavePage> {
fontFamily: 'GeneralSans',
fontSize: 14,
fontWeight: FontWeight.w500,
- height: 20 / 14,
- letterSpacing: 0.25,
),
),
),
),
- // INSTRUCTION OVERLAY (for initial drawing)
+ // Instruction overlay (for initial drawing)
if (_isDrawMode && _startPos == null)
Positioned(
top: 20,
@@ -1003,16 +694,14 @@ class _ImageSavePageState extends State<ImageSavePage> {
child: const Text(
"Drag on image to select area",
style: TextStyle(
- fontFamily: 'GeneralSans',
color: Colors.white,
fontSize: 12,
- fontWeight: FontWeight.normal,
),
),
),
),
),
- // INSTRUCTION OVERLAY (for resizing)
+ // Instruction overlay (for resizing)
if (_isResizing &&
_finalSelectionRect != null &&
_activeHandle == DragHandle.none)
@@ -1035,10 +724,8 @@ class _ImageSavePageState extends State<ImageSavePage> {
child: const Text(
"Adjust area or tap checkmark to confirm",
style: TextStyle(
- fontFamily: 'GeneralSans',
color: Colors.white,
fontSize: 12,
- fontWeight: FontWeight.normal,
),
),
),
@@ -1055,21 +742,18 @@ class _ImageSavePageState extends State<ImageSavePage> {
),
),
),
- // --- BOTTOM FORM (TAGS & SAVE) ---
+ // Bottom Form (Tags & Save)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- // Tags box matching Figma design
+ // Tags box
Container(
width: double.infinity,
decoration: BoxDecoration(
color: const Color(0xFFFEFEFE),
- border: Border.all(
- color: const Color(0xFFE4E4E7),
- width: 1,
- ),
+ border: Border.all(color: Variables.borderSubtle),
borderRadius: BorderRadius.circular(16),
),
child: Column(
@@ -1087,25 +771,16 @@ class _ImageSavePageState extends State<ImageSavePage> {
fontFamily: 'GeneralSans',
fontSize: 14,
fontWeight: FontWeight.w500,
- color: Colors.black,
- letterSpacing: 0.25,
- height: 20 / 14,
),
),
if (widget.imagePaths.length > 1)
Text(
"Image ${_currentImageIndex + 1}/${widget.imagePaths.length}",
- style: const TextStyle(
- fontFamily: 'GeneralSans',
- color: Colors.grey,
- fontSize: 12,
- fontWeight: FontWeight.normal,
- ),
+ style: Variables.captionStyle,
),
],
),
),
- // Divider
const Divider(
height: 0,
thickness: 1,
@@ -1148,9 +823,6 @@ class _ImageSavePageState extends State<ImageSavePage> {
style: TextStyle(
fontFamily: 'GeneralSans',
fontSize: 14,
- fontWeight: FontWeight.normal,
- height: 20 / 14,
- letterSpacing: 0,
color:
isSelected
? const Color(0xFF27272A)
@@ -1166,39 +838,11 @@ class _ImageSavePageState extends State<ImageSavePage> {
),
),
const SizedBox(height: 24),
- SizedBox(
- width: double.infinity,
- height: 54,
- child: ElevatedButton(
- onPressed: _isSaving ? null : _onSaveToMoodboard,
- style: ElevatedButton.styleFrom(
- backgroundColor: Colors.black,
- foregroundColor: Colors.white,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(27),
- ),
- ),
- child:
- _isSaving
- ? const SizedBox(
- height: 24,
- width: 24,
- child: CircularProgressIndicator(
- color: Colors.white,
- strokeWidth: 2,
- ),
- )
- : Text(
- 'Save ${widget.imagePaths.length > 1 ? "All" : ""} to Moodboard',
- style: const TextStyle(
- fontFamily: 'GeneralSans',
- fontSize: 14,
- fontWeight: FontWeight.w500,
- height: 20 / 14,
- letterSpacing: 0.25,
- ),
- ),
- ),
+ PrimaryButton(
+ text:
+ 'Save ${widget.imagePaths.length > 1 ? "All" : ""} to Moodboard',
+ isLoading: _isSaving,
+ onPressed: _onSaveToMoodboard,
),
],
),
@@ -1208,174 +852,3 @@ class _ImageSavePageState extends State<ImageSavePage> {
);
}
}
-
-// ---------------------------------------------------------
-// --- HELPER CLASSES FOR CUSTOM HALF-PAGE MODAL OVERLAY ---
-// ---------------------------------------------------------
-
-class NoteModalOverlay extends StatelessWidget {
- final Widget modalContent;
- final Size screenSize;
-
- const NoteModalOverlay({
- super.key,
- required this.modalContent,
- required this.screenSize,
- });
-
- @override
- Widget build(BuildContext context) {
- // The target initial height of the bottom sheet (half the screen height is no longer the minimum)
- final mq = MediaQuery.of(context);
- final keyboardHeight = mq.viewInsets.bottom;
- final systemBottomPadding = mq.padding.bottom;
-
- return Align(
- alignment: Alignment.bottomCenter,
- child: AnimatedPadding(
- duration: const Duration(milliseconds: 250),
- curve: Curves.easeOut,
- padding: EdgeInsets.only(
- bottom: keyboardHeight, // Moves modal up to avoid keyboard
- ),
- child: ConstrainedBox(
- constraints: BoxConstraints(maxHeight: screenSize.height),
- child: Material(
- // Using Material to provide the background, border radius, and shadow.
- color: Colors.white,
- elevation:
- 10, // Replicating the box shadow of the old container for visual style.
- shadowColor: Colors.black26,
- borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
- clipBehavior: Clip.antiAlias,
- child: SingleChildScrollView(
- // Allows scrolling if content + keyboard height exceed screen height
- child: Padding(
- // Add system bottom padding to respect the safe area/gesture bar
- padding: EdgeInsets.only(bottom: systemBottomPadding),
- child: modalContent,
- ),
- ),
- ),
- ),
- ),
- );
- }
-}
-
-// --- EXTENSION TO NORMALIZE RECT ---
-extension on Rect {
- Rect normalize() {
- return Rect.fromLTRB(
- left < right ? left : right,
- top < bottom ? top : bottom,
- left > right ? left : right,
- top > bottom ? top : bottom,
- );
- }
-}
-
-// --- OVERLAY PAINTER (KEPT FOR MAIN IMAGE SELECTION HIGHLIGHT) ---
-class SelectionOverlayPainter extends CustomPainter {
- final Rect rect;
- final bool isResizing;
- final DragHandle activeHandle;
-
- SelectionOverlayPainter({
- required this.rect,
- required this.isResizing,
- this.activeHandle = DragHandle.none,
- });
-
- @override
- void paint(Canvas canvas, Size size) {
- // 1. DIM BACKGROUND (Black overlay with hole for the selected area)
- if (isResizing) {
- final Path backgroundPath =
- Path()..addRect(Rect.fromLTWH(0, 0, size.width, size.height));
- final Path holePath = Path()..addRect(rect);
- final Path overlayPath = Path.combine(
- ui.PathOperation.difference,
- backgroundPath,
- holePath,
- );
-
- canvas.drawPath(overlayPath, Paint()..color = Colors.black54);
- }
-
- // 2. DRAW DASHED BORDER
- final Paint borderPaint =
- Paint()
- ..color = const Color(0xFF448AFF)
- ..strokeWidth = 2.0
- ..style = PaintingStyle.stroke;
-
- double dashWidth = 6;
- double dashSpace = 4;
- Path borderPath = Path()..addRect(rect);
-
- for (ui.PathMetric pathMetric in borderPath.computeMetrics()) {
- double distance = 0.0;
- while (distance < pathMetric.length) {
- canvas.drawPath(
- pathMetric.extractPath(distance, distance + dashWidth),
- borderPaint,
- );
- distance += (dashWidth + dashSpace);
- }
- }
-
- // 3. DRAW CENTER DOT (Only needed in drawing mode, the app bar button replaces the functionality in resizing mode)
- if (!isResizing) {
- final Paint dotPaint =
- Paint()
- ..color = Colors.white
- ..style = PaintingStyle.fill;
-
- canvas.drawCircle(
- rect.center,
- 8,
- Paint()
- ..color = Colors.black26
- ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 3),
- );
- canvas.drawCircle(rect.center, 6, dotPaint);
- }
-
- // 4. DRAW RESIZE HANDLES (Only in resizing mode)
- if (isResizing) {
- final List<Offset> corners = [
- rect.topLeft,
- rect.topRight,
- rect.bottomLeft,
- rect.bottomRight,
- ];
-
- final Paint handleShadow =
- Paint()
- ..color = Colors.black.withValues(alpha: 0.3)
- ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 2);
-
- final Paint handleFill = Paint()..color = Colors.white;
- final Paint handleBorder =
- Paint()
- ..color = const Color(0xFF448AFF)
- ..strokeWidth = 2
- ..style = PaintingStyle.stroke;
-
- const double handleRadius = 8;
-
- for (final corner in corners) {
- canvas.drawCircle(corner, handleRadius, handleShadow);
- canvas.drawCircle(corner, handleRadius, handleFill);
- canvas.drawCircle(corner, handleRadius, handleBorder);
- }
- }
- }
-
- @override
- bool shouldRepaint(covariant SelectionOverlayPainter oldDelegate) =>
- rect != oldDelegate.rect ||
- isResizing != oldDelegate.isResizing ||
- activeHandle != oldDelegate.activeHandle;
-}
diff --git a/lib/ui/pages/project_board_page_alternate.dart b/lib/ui/pages/project_board_page_alternate.dart
@@ -1,14 +1,13 @@
-import 'dart:io';
import 'package:flutter/material.dart';
import 'package:creekui/ui/styles/variables.dart';
import 'package:creekui/data/models/image_model.dart';
import 'package:creekui/data/repos/image_repo.dart';
-import 'package:creekui/ui/widgets/image_context_menu.dart';
+import 'package:creekui/ui/widgets/moodboard_image_card.dart';
+import 'package:creekui/ui/widgets/empty_state.dart';
import 'image_details_page.dart';
class ProjectBoardPageAlternate extends StatefulWidget {
final int projectId;
-
const ProjectBoardPageAlternate({super.key, required this.projectId});
@override
@@ -18,10 +17,8 @@ class ProjectBoardPageAlternate extends StatefulWidget {
class ProjectBoardPageAlternateState extends State<ProjectBoardPageAlternate> {
final _imageRepo = ImageRepo();
-
List<ImageModel> _allImages = [];
List<ImageModel> _filteredImages = [];
-
List<String> _allTags = [];
final Set<String> _selectedTags = {};
bool _isLoading = true;
@@ -32,23 +29,17 @@ class ProjectBoardPageAlternateState extends State<ProjectBoardPageAlternate> {
_loadData();
}
- // Called when project changes in parent
@override
void didUpdateWidget(covariant ProjectBoardPageAlternate oldWidget) {
super.didUpdateWidget(oldWidget);
- if (oldWidget.projectId != widget.projectId) {
- _loadData();
- }
+ if (oldWidget.projectId != widget.projectId) _loadData();
}
- void refreshData() {
- _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);
@@ -66,136 +57,17 @@ class ProjectBoardPageAlternateState extends State<ProjectBoardPageAlternate> {
}
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: const BoxDecoration(
- color: Colors.white,
- borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
- ),
- padding: const EdgeInsets.all(24),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text(
- "Filter by Tags",
- style: Variables.headerStyle.copyWith(fontSize: 20),
- ),
- IconButton(
- icon: const Icon(Icons.close),
- onPressed: () => Navigator.pop(context),
- ),
- ],
- ),
- const SizedBox(height: 20),
- if (_allTags.isEmpty)
- Text("No tags available.", style: Variables.bodyStyle),
-
- Expanded(
- child: SingleChildScrollView(
- child: Wrap(
- spacing: 10,
- runSpacing: 10,
- children:
- _allTags.map((tag) {
- final isSelected = _selectedTags.contains(tag);
- return FilterChip(
- label: Text(tag.toUpperCase()),
- selected: isSelected,
- onSelected: (selected) {
- setModalState(() {
- if (selected) {
- _selectedTags.add(tag);
- } else {
- _selectedTags.remove(tag);
- }
- });
- // Update main state
- setState(() {
- _applyFilter();
- });
- },
- labelStyle: Variables.captionStyle.copyWith(
- color:
- isSelected
- ? Colors.white
- : Variables.textPrimary,
- fontWeight: FontWeight.w600,
- ),
- backgroundColor: Variables.surfaceSubtle,
- selectedColor: Variables.textPrimary,
- checkmarkColor: Colors.white,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(20),
- side: BorderSide.none,
- ),
- );
- }).toList(),
- ),
- ),
- ),
- const SizedBox(height: 16),
- Row(
- children: [
- Expanded(
- child: TextButton(
- onPressed: () {
- 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: Variables.textPrimary,
- foregroundColor: Colors.white,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(12),
- ),
- ),
- child: const Text("Done"),
- ),
- ),
- ],
- ),
- ],
- ),
- );
- },
- );
- },
- );
+ setState(() {
+ _filteredImages =
+ _selectedTags.isEmpty
+ ? _allImages
+ : _allImages
+ .where(
+ (img) =>
+ img.tags.toSet().intersection(_selectedTags).isNotEmpty,
+ )
+ .toList();
+ });
}
@override
@@ -206,11 +78,33 @@ class ProjectBoardPageAlternateState extends State<ProjectBoardPageAlternate> {
final rightColumn = <Widget>[];
for (int i = 0; i < _filteredImages.length; i++) {
- final item = _buildImageItem(_filteredImages[i], index: i);
+ final item = MoodboardImageCard(
+ image: _filteredImages[i],
+ height: (i % 3 == 0) ? 240 : 180,
+ showTags: true,
+ onTap:
+ () => Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder:
+ (_) => ImageDetailsPage(
+ imagePath: _filteredImages[i].filePath,
+ imageId: _filteredImages[i].id,
+ projectId: widget.projectId,
+ ),
+ ),
+ ),
+ onDeleted: refreshData,
+ );
+
if (i % 2 == 0) {
- leftColumn.add(item);
+ leftColumn.add(
+ Padding(padding: const EdgeInsets.only(bottom: 12), child: item),
+ );
} else {
- rightColumn.add(item);
+ rightColumn.add(
+ Padding(padding: const EdgeInsets.only(bottom: 12), child: item),
+ );
}
}
@@ -248,55 +142,19 @@ class ProjectBoardPageAlternateState extends State<ProjectBoardPageAlternate> {
Expanded(
child:
_filteredImages.isEmpty
- ? Center(
- child: Text(
- "No images found",
- style: Variables.bodyStyle.copyWith(
- color: Variables.textSecondary,
- ),
- ),
+ ? const EmptyState(
+ icon: Icons.image_not_supported_outlined,
+ title: "No images found",
+ subtitle: "Try removing filters or adding new images",
)
: SingleChildScrollView(
- padding: const EdgeInsets.fromLTRB(
- 16,
- 0,
- 16,
- 80,
- ), // Bottom padding for FAB
+ padding: const EdgeInsets.fromLTRB(16, 0, 16, 80),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Expanded(
- child: Column(
- children:
- leftColumn
- .map(
- (e) => Padding(
- padding: const EdgeInsets.only(
- bottom: 12,
- ),
- child: e,
- ),
- )
- .toList(),
- ),
- ),
+ Expanded(child: Column(children: leftColumn)),
const SizedBox(width: 12),
- Expanded(
- child: Column(
- children:
- rightColumn
- .map(
- (e) => Padding(
- padding: const EdgeInsets.only(
- bottom: 12,
- ),
- child: e,
- ),
- )
- .toList(),
- ),
- ),
+ Expanded(child: Column(children: rightColumn)),
],
),
),
@@ -304,125 +162,4 @@ class ProjectBoardPageAlternateState extends State<ProjectBoardPageAlternate> {
],
);
}
-
- Widget _buildImageItem(ImageModel image, {required int index}) {
- return ImageContextMenu(
- image: image,
- onImageDeleted: () => refreshData(),
- child: 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: Variables.surfaceSubtle,
- ),
- 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: Variables.textDisabled,
- ),
- ),
- ),
- 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.withValues(alpha: 0.8),
- Colors.transparent,
- ],
- ),
- ),
- child: Wrap(
- spacing: 4,
- runSpacing: 4,
- children: [
- // Show first 2 tags
- ...image.tags.take(2).map((tag) {
- return Container(
- padding: const EdgeInsets.symmetric(
- horizontal: 6,
- vertical: 2,
- ),
- decoration: BoxDecoration(
- color: Colors.white.withValues(alpha: 0.2),
- borderRadius: BorderRadius.circular(4),
- border: Border.all(
- color: Colors.white.withValues(alpha: 0.1),
- ),
- ),
- child: Text(
- tag.toUpperCase(),
- style: const TextStyle(
- color: Colors.white,
- fontSize: 9,
- fontFamily: 'GeneralSans',
- fontWeight: FontWeight.w600,
- ),
- ),
- );
- }),
- // Show "+x" tag if there are more than 2 tags
- if (image.tags.length > 2)
- Container(
- padding: const EdgeInsets.symmetric(
- horizontal: 6,
- vertical: 2,
- ),
- decoration: BoxDecoration(
- color: Colors.white.withValues(alpha: 0.2),
- borderRadius: BorderRadius.circular(4),
- border: Border.all(
- color: Colors.white.withValues(alpha: 0.1),
- ),
- ),
- child: Text(
- '+${image.tags.length - 2}',
- style: const TextStyle(
- color: Colors.white,
- fontSize: 9,
- fontFamily: 'GeneralSans',
- fontWeight: FontWeight.w600,
- ),
- ),
- ),
- ],
- ),
- ),
- ),
- ],
- ),
- ),
- ),
- );
- }
}
diff --git a/lib/ui/pages/project_tag_page.dart b/lib/ui/pages/project_tag_page.dart
@@ -4,11 +4,12 @@ import 'package:image_picker/image_picker.dart';
import 'package:creekui/ui/styles/variables.dart';
import 'package:creekui/ui/widgets/top_bar.dart';
import 'package:creekui/ui/widgets/bottom_bar.dart';
+import 'package:creekui/ui/widgets/moodboard_image_card.dart';
+import 'package:creekui/ui/widgets/empty_state.dart';
import 'package:creekui/data/models/image_model.dart';
import 'package:creekui/data/repos/image_repo.dart';
import 'package:creekui/data/repos/project_repo.dart';
import 'package:creekui/data/repos/note_repo.dart';
-import 'package:creekui/ui/widgets/image_context_menu.dart';
import 'image_save_page.dart';
import 'image_details_page.dart';
@@ -45,31 +46,23 @@ class _ProjectTagPageState extends State<ProjectTagPage> {
final allImages = await _imageRepo.getImages(widget.projectId);
final List<ImageModel> filtered = [];
- // Process logic in parallel for speed
await Future.wait(
allImages.map((img) async {
bool matches = false;
-
if (widget.tag == 'Uncategorized') {
if (img.tags.isEmpty) {
final notes = await _noteRepo.getNotesForImage(img.id);
- final hasCategorizedNote = notes.any((n) => n.category.isNotEmpty);
- if (!hasCategorizedNote) matches = true;
+ if (!notes.any((n) => n.category.isNotEmpty)) matches = true;
}
} else {
- if (img.tags.contains(widget.tag)) {
+ if (img.tags.contains(widget.tag))
matches = true;
- } else {
+ else {
final notes = await _noteRepo.getNotesForImage(img.id);
- if (notes.any((n) => n.category == widget.tag)) {
- matches = true;
- }
+ if (notes.any((n) => n.category == widget.tag)) matches = true;
}
}
-
- if (matches) {
- filtered.add(img);
- }
+ if (matches) filtered.add(img);
}),
);
@@ -106,154 +99,77 @@ class _ProjectTagPageState extends State<ProjectTagPage> {
@override
Widget build(BuildContext context) {
- // 1. Prepare Columns for Masonry Layout
final leftColumn = <Widget>[];
final rightColumn = <Widget>[];
for (int i = 0; i < _images.length; i++) {
- final item = _buildStaggeredImageItem(_images[i], index: i);
+ final item = MoodboardImageCard(
+ image: _images[i],
+ height: (i % 3 == 0) ? 240 : 180,
+ onTap:
+ () => Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder:
+ (_) => ImageDetailsPage(
+ imagePath: _images[i].filePath,
+ imageId: _images[i].id,
+ projectId: widget.projectId,
+ ),
+ ),
+ ).then((_) => _loadData()),
+ onDeleted: _loadData,
+ );
+
if (i % 2 == 0) {
- leftColumn.add(item);
+ leftColumn.add(
+ Padding(padding: const EdgeInsets.only(bottom: 12), child: item),
+ );
} else {
- rightColumn.add(item);
+ rightColumn.add(
+ Padding(padding: const EdgeInsets.only(bottom: 12), child: item),
+ );
}
}
return Scaffold(
backgroundColor: Variables.background,
-
appBar: TopBar(
currentProjectId: widget.projectId,
titleOverride: widget.tag.toUpperCase(),
onBack: () => Navigator.pop(context),
hideSecondRow: true,
),
-
bottomNavigationBar: BottomBar(
currentTab: BottomBarItem.moodboard,
projectId: widget.projectId,
),
-
floatingActionButton: FloatingActionButton(
onPressed: _pickAndRedirect,
backgroundColor: Variables.textPrimary,
foregroundColor: Variables.background,
child: const Icon(Icons.add_photo_alternate_outlined),
),
-
body:
_isLoading
? const Center(child: CircularProgressIndicator())
: _images.isEmpty
- ? Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- const Icon(
- Icons.image_not_supported_outlined,
- size: 64,
- color: Variables.textDisabled,
- ),
- const SizedBox(height: 16),
- Text(
- "No images found for '${widget.tag}'",
- style: Variables.bodyStyle.copyWith(
- color: Variables.textSecondary,
- ),
- ),
- ],
- ),
+ ? EmptyState(
+ icon: Icons.image_not_supported_outlined,
+ title: "No images found",
+ subtitle: "No images found for '${widget.tag}'",
)
: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 80),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Expanded(
- child: Column(
- children:
- leftColumn
- .map(
- (e) => Padding(
- padding: const EdgeInsets.only(bottom: 12),
- child: e,
- ),
- )
- .toList(),
- ),
- ),
+ Expanded(child: Column(children: leftColumn)),
const SizedBox(width: 12),
- Expanded(
- child: Column(
- children:
- rightColumn
- .map(
- (e) => Padding(
- padding: const EdgeInsets.only(bottom: 12),
- child: e,
- ),
- )
- .toList(),
- ),
- ),
+ Expanded(child: Column(children: rightColumn)),
],
),
),
);
}
-
- Widget _buildStaggeredImageItem(ImageModel image, {required int index}) {
- return ImageContextMenu(
- image: image,
- onImageDeleted: () => _loadData(),
- child: GestureDetector(
- onTap: () {
- Navigator.push(
- context,
- MaterialPageRoute(
- builder:
- (_) => ImageDetailsPage(
- imagePath: image.filePath,
- imageId: image.id,
- projectId: widget.projectId,
- ),
- ),
- ).then((_) => _loadData());
- },
- child: Container(
- // Simulate staggered heights similar to Alternate Page
- height: (index % 3 == 0) ? 240 : 180,
- decoration: BoxDecoration(
- color: Variables.surfaceSubtle,
- borderRadius: BorderRadius.circular(16),
- border: Border.all(color: Variables.borderSubtle),
- ),
- // Explicitly clip content to border radius
- child: ClipRRect(
- borderRadius: BorderRadius.circular(16),
- child: Stack(
- fit: StackFit.expand,
- children: [
- Image.file(
- File(image.filePath),
- fit: BoxFit.cover,
- width: double.infinity,
- errorBuilder:
- (_, __, ___) => Container(
- color: Variables.surfaceSubtle,
- child: const Center(
- child: Icon(
- Icons.broken_image,
- color: Variables.textDisabled,
- ),
- ),
- ),
- ),
- ],
- ),
- ),
- ),
- ),
- );
- }
}
diff --git a/lib/ui/pages/share_handler_page.dart b/lib/ui/pages/share_handler_page.dart
@@ -3,6 +3,8 @@ import 'package:flutter/material.dart';
import 'package:creekui/services/download_service.dart';
import 'package:creekui/services/instagram_download_service.dart';
import 'package:creekui/services/image_service.dart';
+import 'package:creekui/ui/styles/variables.dart';
+import 'package:creekui/ui/widgets/primary_button.dart';
import 'share_to_moodboard_page.dart';
import 'share_to_file_page.dart';
@@ -21,12 +23,10 @@ class ShareHandlerPage extends StatefulWidget {
}
class _ShareHandlerPageState extends State<ShareHandlerPage> {
- // Services
final _downloadService = DownloadService();
final _instagramService = InstagramDownloadService();
final _imageService = ImageService();
- // State
bool _hasError = false;
String? _errorMessage;
@@ -41,18 +41,17 @@ class _ShareHandlerPageState extends State<ShareHandlerPage> {
List<File> tempFiles = [];
try {
- // CASE A: It's a Local File Path
+ // CASE A: Local File Path
if (await File(sharedContent).exists()) {
tempFiles.add(File(sharedContent));
}
- // CASE B: It's a URL
+ // CASE B: URL
else {
final urlRegExp = RegExp(r'(https?://\S+)');
final match = urlRegExp.firstMatch(sharedContent);
if (match != null) {
final url = match.group(0)!;
-
if (url.contains('instagram.com')) {
// Instagram Logic
final downloadedPaths = await _instagramService
@@ -72,13 +71,11 @@ class _ShareHandlerPageState extends State<ShareHandlerPage> {
}
}
- // SUCCESS: Route based on Destination
+ // Success: Route based on Destination
if (tempFiles.isNotEmpty) {
if (!mounted) return;
-
- // CHECK DESTINATION
if (widget.destination == 'files') {
- // --- ROUTE TO FILES ---
+ // Files
Navigator.pushReplacement(
context,
MaterialPageRoute(
@@ -86,22 +83,15 @@ class _ShareHandlerPageState extends State<ShareHandlerPage> {
),
);
} else {
- // --- ROUTE TO MOODBOARDS (Default) ---
+ // Moodboard
List<File> permanentFiles = [];
-
for (var file in tempFiles) {
- final id = await _imageService.saveImage(
- file,
- 0, // Project 0 = Inbox
- tags: [],
- );
-
+ final id = await _imageService.saveImage(file, 0, tags: []);
final savedImage = await _imageService.getImage(id);
if (savedImage != null) {
permanentFiles.add(File(savedImage.filePath));
}
}
-
if (mounted) {
Navigator.pushReplacement(
context,
@@ -146,7 +136,7 @@ class _ShareHandlerPageState extends State<ShareHandlerPage> {
const SizedBox(height: 16),
Text(
"Error processing media",
- style: Theme.of(context).textTheme.titleMedium,
+ style: Variables.headerStyle.copyWith(fontSize: 18),
),
const SizedBox(height: 8),
Text(
@@ -155,9 +145,12 @@ class _ShareHandlerPageState extends State<ShareHandlerPage> {
style: const TextStyle(color: Colors.grey),
),
const SizedBox(height: 24),
- ElevatedButton(
- onPressed: () => Navigator.pop(context),
- child: const Text("Close"),
+ SizedBox(
+ width: 200,
+ child: PrimaryButton(
+ text: "Close",
+ onPressed: () => Navigator.pop(context),
+ ),
),
],
),
diff --git a/lib/ui/pages/share_to_file_page.dart b/lib/ui/pages/share_to_file_page.dart
@@ -6,6 +6,11 @@ import 'package:creekui/services/file_service.dart';
import 'package:creekui/services/project_service.dart';
import 'package:creekui/data/models/file_model.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/file_card.dart';
+import 'package:creekui/ui/widgets/empty_state.dart';
+import 'package:creekui/ui/widgets/section_header.dart';
import 'create_file_page.dart';
import 'canvas_page.dart';
@@ -20,17 +25,14 @@ class ShareToFilePage extends StatefulWidget {
class _ShareToFilePageState extends State<ShareToFilePage> {
final FileService _fileService = FileService();
final ProjectService _projectService = ProjectService();
-
final TextEditingController _searchController = TextEditingController();
List<FileModel> _allFiles = [];
List<FileModel> _filteredFiles = [];
List<FileModel> _recentFiles = [];
-
- // file.id -> { 'preview': path, 'dimensions': str }
Map<String, Map<String, String>> _fileMetadata = {};
- // project cache to avoid repeated fetches
+ // Avoid repeated fetches
final Map<int, ProjectModel> _projectCache = {};
bool _isLoading = true;
@@ -64,29 +66,23 @@ class _ShareToFilePageState extends State<ShareToFilePage> {
_filteredFiles = List.from(files);
_recentFiles = recent.take(3).toList();
- // Load metadata for files (previews, dimensions)
await _loadFileMetadata(files);
-
setState(() => _isLoading = false);
} catch (e) {
- debugPrint('Error fetching files in ShareToFilePage: $e');
+ debugPrint('Error fetching files: $e');
setState(() => _isLoading = false);
}
}
Future<void> _loadFileMetadata(List<FileModel> files) async {
final Map<String, Map<String, String>> meta = {};
-
for (final fmodel in files) {
try {
final f = File(fmodel.filePath);
- if (!await f.exists()) {
- // skip if disk file missing
- continue;
- }
+ if (!await f.exists()) continue;
if (fmodel.filePath.toLowerCase().endsWith('.json')) {
- // Canvas JSON - attempt to parse preview_path, width/height
+ // File JSON
try {
final content = await f.readAsString();
final data = jsonDecode(content);
@@ -94,8 +90,7 @@ class _ShareToFilePageState extends State<ShareToFilePage> {
String dims = 'Unknown';
if (data is Map) {
- if (data['preview_path'] != null &&
- data['preview_path'].toString().isNotEmpty) {
+ if (data['preview_path'] != null) {
preview = data['preview_path'].toString();
// If preview is relative, try to resolve relative to JSON file
if (!File(preview).existsSync()) {
@@ -104,35 +99,24 @@ class _ShareToFilePageState extends State<ShareToFilePage> {
if (candidate.existsSync()) preview = candidate.path;
}
}
-
if (data['width'] != null && data['height'] != null) {
- final w = (data['width'] as num).toInt();
- final h = (data['height'] as num).toInt();
- dims = '$w x $h px';
+ dims = '${data['width']} x ${data['height']} px';
}
}
-
meta[fmodel.id] = {'preview': preview, 'dimensions': dims};
- } catch (e) {
- debugPrint('Error parsing canvas json for ${fmodel.id}: $e');
- }
+ } catch (_) {}
} else {
- // Regular image file - assign path and try to get dims
+ // Regular image file - assign path and try
String dims = 'Unknown';
try {
final bytes = await f.readAsBytes();
final image = img.decodeImage(bytes);
if (image != null) dims = '${image.width} x ${image.height} px';
- } catch (_) {
- // ignore
- }
+ } catch (_) {}
meta[fmodel.id] = {'preview': fmodel.filePath, 'dimensions': dims};
}
- } catch (e) {
- debugPrint('Error while loading metadata for ${fmodel.id}: $e');
- }
+ } catch (_) {}
}
-
_fileMetadata = meta;
}
@@ -147,8 +131,7 @@ class _ShareToFilePageState extends State<ShareToFilePage> {
_allFiles.where((file) {
final nameMatch = file.name.toLowerCase().contains(q);
final breadcrumb = _getProjectBreadcrumbSync(file).toLowerCase();
- final projectMatch = breadcrumb.contains(q);
- return nameMatch || projectMatch;
+ return nameMatch || breadcrumb.contains(q);
}).toList();
}
});
@@ -167,69 +150,40 @@ class _ShareToFilePageState extends State<ShareToFilePage> {
// Async breadcrumb loader that fetches projects into cache as needed
Future<String> _getProjectEventLabel(FileModel file) async {
- // load file.projectId
if (!_projectCache.containsKey(file.projectId)) {
try {
final p = await _projectService.getProjectById(file.projectId);
if (p != null) _projectCache[file.projectId] = p;
- } catch (e) {
- debugPrint('Project load failed for ${file.projectId}: $e');
- }
+ } catch (_) {}
}
-
final project = _projectCache[file.projectId];
if (project == null) return "Unknown";
+ if (project.parentId == null) return project.title;
- if (project.parentId == null) {
- return project.title;
- }
-
- final parentId = project.parentId!;
- if (!_projectCache.containsKey(parentId)) {
+ if (!_projectCache.containsKey(project.parentId!)) {
try {
- final parent = await _projectService.getProjectById(parentId);
- if (parent != null) _projectCache[parentId] = parent;
- } catch (e) {
- debugPrint('Parent project load failed for $parentId: $e');
- }
+ final parent = await _projectService.getProjectById(project.parentId!);
+ if (parent != null) _projectCache[project.parentId!] = parent;
+ } catch (_) {}
}
-
- 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),
- ),
- );
+ final parent = _projectCache[project.parentId];
+ return parent == null
+ ? project.title
+ : "${parent.title} / ${project.title}";
}
void _onFileSelected(FileModel file) async {
try {
final f = File(file.filePath);
- if (!await f.exists()) {
- ScaffoldMessenger.of(
- context,
- ).showSnackBar(const SnackBar(content: Text("File not found")));
- return;
- }
-
- double width = 1080;
- double height = 1080;
+ if (!await f.exists()) return;
+ double width = 1080, height = 1080;
if (file.filePath.toLowerCase().endsWith('.json')) {
final content = await f.readAsString();
final data = jsonDecode(content);
-
- if (data is Map) {
- if (data['width'] != null && data['height'] != null) {
- width = (data['width'] as num).toDouble();
- height = (data['height'] as num).toDouble();
- }
+ if (data is Map && data['width'] != null) {
+ width = (data['width'] as num).toDouble();
+ height = (data['height'] as num).toDouble();
}
}
@@ -237,7 +191,7 @@ class _ShareToFilePageState extends State<ShareToFilePage> {
context,
MaterialPageRoute(
builder:
- (context) => CanvasPage(
+ (_) => CanvasPage(
projectId: file.projectId,
width: width,
height: height,
@@ -252,68 +206,38 @@ class _ShareToFilePageState extends State<ShareToFilePage> {
}
String _formatDate(DateTime date) {
- final now = DateTime.now();
- final diff = now.difference(date);
+ final diff = DateTime.now().difference(date);
if (diff.inDays == 0) return 'Today';
if (diff.inDays == 1) return 'Yesterday';
if (diff.inDays < 7) return '${diff.inDays} days ago';
return '${date.day}/${date.month}/${date.year}';
}
- // Resolve preview path; if empty or missing, fallback to original filePath
- 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;
- // fallback: if file is json and has no preview, return placeholder or file.path
- if (file.filePath.toLowerCase().endsWith('.json')) {
- // attempt to find PNG/JPG sibling in same folder with same base name
- 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: Colors.white,
+ backgroundColor: Variables.background,
appBar: AppBar(
- backgroundColor: Colors.white,
+ backgroundColor: Variables.background,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black),
onPressed: () => Navigator.pop(context),
),
- title: const Text(
+ title: Text(
'Files',
- style: TextStyle(
- color: Color(0xFF27272A),
- fontFamily: 'GeneralSans',
- fontSize: 20,
- fontWeight: FontWeight.w500,
- height: 1.2,
- ),
+ style: Variables.headerStyle.copyWith(fontSize: 20),
),
actions: [
IconButton(
icon: const Icon(Icons.add, color: Colors.black, size: 28),
- onPressed: _onAddPressed,
- tooltip: "Create New File",
+ onPressed:
+ () => Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (_) => CreateFilePage(file: widget.sharedImage),
+ ),
+ ),
),
],
),
@@ -321,130 +245,61 @@ class _ShareToFilePageState extends State<ShareToFilePage> {
_isLoading
? const Center(child: CircularProgressIndicator())
: _allFiles.isEmpty
- ? _buildEmptyState()
+ ? const EmptyState(
+ icon: Icons.folder_open,
+ title: "No files yet",
+ subtitle: "Tap + to create your first file",
+ )
: Column(
children: [
- // Search bar
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
- child: SizedBox(
- height: 42,
- child: TextField(
- controller: _searchController,
- onChanged: _filterFiles,
- style: const TextStyle(
- fontFamily: "GeneralSans",
- fontSize: 16,
- fontWeight: FontWeight.w400,
- color: Color(0xFF71717B),
- height: 1.4,
- ),
- decoration: InputDecoration(
- hintText: "Search",
- hintStyle: const TextStyle(
- fontFamily: "GeneralSans",
- fontSize: 16,
- fontWeight: FontWeight.w400,
- color: Color(0xFF71717B),
- ),
- prefixIcon: const Icon(
- Icons.search,
- size: 20,
- color: Color(0xFF9F9FA9),
- ),
- filled: true,
- fillColor: const Color(0xFFE4E4E7),
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(8),
- borderSide: BorderSide.none,
- ),
- contentPadding: const EdgeInsets.symmetric(
- vertical: 12,
- horizontal: 16,
- ),
- suffixIcon:
- _searchQuery.isNotEmpty
- ? IconButton(
- icon: const Icon(
- Icons.clear,
- size: 20,
- color: Color(0xFF9F9FA9),
- ),
- padding: EdgeInsets.zero,
- onPressed: () {
- _searchController.clear();
- _filterFiles('');
- },
- )
- : null,
- ),
- ),
+ child: CommonSearchBar(
+ controller: _searchController,
+ onChanged: _filterFiles,
),
),
-
Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- // Recent Files
if (_recentFiles.isNotEmpty &&
_searchQuery.isEmpty) ...[
const Padding(
padding: EdgeInsets.symmetric(
- horizontal: 20,
+ horizontal: 16,
vertical: 8,
),
- child: Text(
- "Recent Files",
- style: TextStyle(
- fontFamily: 'GeneralSans',
- fontSize: 14,
- fontWeight: FontWeight.w400,
- color: Color(0xFF27272A),
- height: 1.43,
- ),
- ),
+ child: SectionHeader(title: "Recent Files"),
),
Padding(
padding: const EdgeInsets.symmetric(
- horizontal: 20,
+ horizontal: 16,
),
child: Column(
children:
_recentFiles
- .map(
- (file) => _buildRecentFileItem(file),
- )
+ .map((file) => _buildFileCard(file))
.toList(),
),
),
const SizedBox(height: 24),
],
-
- // All files header
Padding(
padding: const EdgeInsets.symmetric(
- horizontal: 20,
+ horizontal: 16,
vertical: 8,
),
- child: Text(
- _searchQuery.isEmpty
- ? "All Files"
- : "Search Results",
- style: const TextStyle(
- fontFamily: 'GeneralSans',
- fontSize: 14,
- fontWeight: FontWeight.w400,
- color: Color(0xFF27272A),
- height: 1.43,
- ),
+ child: SectionHeader(
+ title:
+ _searchQuery.isEmpty
+ ? "All Files"
+ : "Search Results",
),
),
-
- // All files list
Padding(
- padding: const EdgeInsets.symmetric(horizontal: 20),
+ padding: const EdgeInsets.symmetric(horizontal: 16),
child: ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
@@ -464,202 +319,26 @@ 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),
+ Widget _buildFileCard(FileModel file) {
+ 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'] ?? "",
+ timeAgo: _formatDate(file.lastUpdated),
+ onTap: () => _onFileSelected(file),
+ onMenuAction: null,
),
- const SizedBox(height: 8),
- TextButton(
- onPressed: _onAddPressed,
- child: const Text("Create your first file"),
- ),
- ],
- ),
- );
- }
-
- Widget _buildRecentFileItem(FileModel file) {
- final preview = _resolvePreviewPath(file);
-
- return InkWell(
- onTap: () => _onFileSelected(file),
- child: Container(
- margin: const EdgeInsets.only(bottom: 12),
- decoration: BoxDecoration(
- color: Colors.white,
- borderRadius: BorderRadius.circular(16),
- border: Border.all(color: Color(0xFFE4E4E7)),
- ),
- child: Row(
- children: [
- ClipRRect(
- borderRadius: const BorderRadius.only(
- topLeft: Radius.circular(16),
- bottomLeft: Radius.circular(16),
- ),
- child: Image.file(
- File(preview),
- width: 120,
- height: 120,
- fit: BoxFit.cover,
- errorBuilder:
- (_, __, ___) => Container(
- width: 120,
- height: 120,
- color: Colors.grey[300],
- child: const Icon(Icons.image),
- ),
- ),
- ),
-
- const SizedBox(width: 12),
-
- Expanded(
- child: Padding(
- padding: const EdgeInsets.symmetric(vertical: 14),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- FutureBuilder<String>(
- future: _getProjectEventLabel(file),
- builder:
- (_, s) => Text(
- s.data ?? "",
- style: const TextStyle(
- fontSize: 12,
- color: Color(0xFF71717B),
- fontFamily: "GeneralSans",
- ),
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- ),
- ),
- const SizedBox(height: 6),
- Text(
- file.name,
- style: const TextStyle(
- fontSize: 15,
- fontWeight: FontWeight.w600,
- fontFamily: "GeneralSans",
- ),
- ),
- const SizedBox(height: 6),
- Text(
- _formatDate(file.lastUpdated),
- style: const TextStyle(
- fontSize: 12,
- color: Color(0xFF71717B),
- ),
- ),
- ],
- ),
- ),
- ),
- ],
- ),
- ),
- );
- }
-
- Widget _buildFileItem(FileModel file) {
- final preview = _resolvePreviewPath(file);
-
- return InkWell(
- onTap: () => _onFileSelected(file),
- child: Container(
- margin: const EdgeInsets.only(bottom: 12),
- decoration: BoxDecoration(
- color: Colors.white,
- borderRadius: BorderRadius.circular(16),
- border: Border.all(color: Color(0xFFE4E4E7)),
- ),
- child: Row(
- children: [
- ClipRRect(
- borderRadius: const BorderRadius.only(
- topLeft: Radius.circular(16),
- bottomLeft: Radius.circular(16),
- ),
- child: Image.file(
- File(preview),
- width: 120,
- height: 120,
- fit: BoxFit.cover,
- errorBuilder:
- (_, __, ___) => Container(
- width: 120,
- height: 120,
- color: Colors.grey[300],
- child: const Icon(Icons.image),
- ),
- ),
- ),
-
- const SizedBox(width: 12),
-
- Expanded(
- child: Padding(
- padding: const EdgeInsets.symmetric(vertical: 14),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- FutureBuilder<String>(
- future: _getProjectEventLabel(file),
- builder:
- (_, s) => Text(
- s.data ?? "",
- style: const TextStyle(
- fontSize: 12,
- color: Color(0xFF71717B),
- fontFamily: "GeneralSans",
- ),
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- ),
- ),
-
- const SizedBox(height: 4),
-
- Text(
- file.name,
- style: const TextStyle(
- fontSize: 15,
- fontWeight: FontWeight.w600,
- color: Color(0xFF27272A),
- fontFamily: "GeneralSans",
- ),
- ),
-
- const SizedBox(height: 6),
-
- Text(
- _formatDate(file.lastUpdated),
- style: const TextStyle(
- fontSize: 12,
- color: Color(0xFF71717B),
- ),
- ),
- ],
- ),
- ),
- ),
- ],
- ),
- ),
+ );
+ },
);
}
- Widget _placeholderIcon() {
- return Container(
- color: Colors.grey[200],
- child: Icon(Icons.image, size: 28, color: Colors.grey[400]),
- );
- }
+ 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
@@ -5,17 +5,16 @@ 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 'image_save_page.dart';
-// --- VIEW MODELS ---
-
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!;
@@ -26,7 +25,6 @@ class ProjectGroup {
final List<ProjectItemViewModel> events;
final String? coverPath;
bool isExpanded;
-
ProjectGroup({
required this.project,
this.events = const [],
@@ -35,11 +33,8 @@ class ProjectGroup {
});
}
-// --- WIDGET ---
-
class ShareToMoodboardPage extends StatefulWidget {
final List<File> imageFiles;
-
const ShareToMoodboardPage({super.key, required this.imageFiles});
@override
@@ -74,12 +69,8 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> {
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");
- }
+ if (images.isNotEmpty) return images.first.filePath;
+ } catch (_) {}
return null;
}
@@ -98,9 +89,7 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> {
final parent = await _projectRepo.getProjectById(item.parentId!);
parentTitle = parent?.title;
}
-
final cover = await _getProjectCover(item.id!);
-
recents.add(
ProjectItemViewModel(
item: item,
@@ -120,15 +109,12 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> {
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);
}
@@ -144,7 +130,6 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> {
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(
@@ -210,7 +195,6 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> {
}) {
_projectService.openProject(projectId);
ReceiveSharingIntent.instance.reset();
-
Navigator.push(
context,
MaterialPageRoute(
@@ -237,15 +221,9 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> {
icon: const Icon(Icons.arrow_back, color: Colors.black),
onPressed: () => Navigator.pop(context),
),
- title: const Text(
+ title: Text(
"MoodBoards",
- style: TextStyle(
- color: Color(0xFF27272A),
- fontFamily: 'GeneralSans',
- fontSize: 20,
- fontWeight: FontWeight.w500,
- height: 1.2,
- ),
+ style: Variables.headerStyle.copyWith(fontSize: 20),
),
actions: [
IconButton(
@@ -260,70 +238,18 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> {
? const Center(child: CircularProgressIndicator())
: Column(
children: [
- // --- Search Bar ---
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
- child: SizedBox(
- height: 42,
- child: TextField(
- controller: _searchController,
- onChanged: _filterProjects,
- style: const TextStyle(
- fontFamily: "GeneralSans",
- fontSize: 16,
- fontWeight: FontWeight.w400,
- color: Color(0xFF71717B),
- height: 1.4,
- ),
- decoration: InputDecoration(
- hintText: "Search",
- hintStyle: const TextStyle(
- fontFamily: "GeneralSans",
- fontSize: 16,
- fontWeight: FontWeight.w400,
- color: Color(0xFF71717B),
- ),
- prefixIcon: const Icon(
- Icons.search,
- size: 20,
- color: Color(0xFF9F9FA9),
- ),
- filled: true,
- fillColor: const Color(0xFFE4E4E7),
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(8),
- borderSide: BorderSide.none,
- ),
- contentPadding: const EdgeInsets.symmetric(
- vertical: 12,
- horizontal: 16,
- ),
- suffixIcon:
- _searchQuery.isNotEmpty
- ? IconButton(
- icon: const Icon(
- Icons.clear,
- size: 20,
- color: Color(0xFF9F9FA9),
- ),
- padding: EdgeInsets.zero,
- onPressed: () {
- _searchController.clear();
- _filterProjects("");
- },
- )
- : null,
- ),
- ),
+ child: CommonSearchBar(
+ controller: _searchController,
+ onChanged: _filterProjects,
),
),
-
Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- // --- RECENT SECTION ---
if (_searchQuery.isEmpty &&
_recentViewModels.isNotEmpty) ...[
const Padding(
@@ -331,15 +257,8 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> {
horizontal: 20,
vertical: 8,
),
- child: Text(
- "Recent Projects/Events",
- style: TextStyle(
- fontFamily: 'GeneralSans',
- fontSize: 14,
- fontWeight: FontWeight.w400,
- color: Color(0xFF27272A),
- height: 1.43,
- ),
+ child: SectionHeader(
+ title: "Recent Projects/Events",
),
),
Padding(
@@ -355,24 +274,16 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> {
),
const SizedBox(height: 24),
],
-
- // --- ALL PROJECTS SECTION ---
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 8,
),
- child: Text(
- _searchQuery.isEmpty
- ? "All Projects/Events"
- : "Search Results",
- style: const TextStyle(
- fontFamily: 'GeneralSans',
- fontSize: 14,
- fontWeight: FontWeight.w400,
- color: Color(0xFF27272A),
- height: 1.43,
- ),
+ child: SectionHeader(
+ title:
+ _searchQuery.isEmpty
+ ? "All Projects/Events"
+ : "Search Results",
),
),
Padding(
@@ -397,11 +308,10 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> {
);
}
- // --- WIDGETS ---
-
+ // Wrappers for consistent UI
Widget _buildRecentItem(ProjectItemViewModel vm) {
return Container(
- margin: const EdgeInsets.only(bottom: 8), // Reduced spacing
+ margin: const EdgeInsets.only(bottom: 8),
child: InkWell(
onTap:
() => _navigateToSavePage(
@@ -411,15 +321,11 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> {
),
borderRadius: BorderRadius.circular(16),
child: Container(
- // padding: 4px 0 4px 4px
padding: const EdgeInsets.fromLTRB(4, 4, 0, 4),
decoration: BoxDecoration(
- color: Colors.white, // #FAFAFA
+ color: Colors.white,
borderRadius: BorderRadius.circular(12),
- border: Border.all(
- color: const Color(0xFFE4E4E7),
- width: 1,
- ), // #E4E4E7
+ border: Border.all(color: Variables.borderSubtle, width: 1),
),
child: Row(
children: [
@@ -428,7 +334,7 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> {
width: 56,
height: 56,
decoration: BoxDecoration(
- color: const Color(0xFFFAFAFA),
+ color: Variables.surfaceSubtle,
borderRadius: BorderRadius.circular(8),
image:
vm.coverPath != null
@@ -443,35 +349,24 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> {
? Icon(Icons.image, color: Colors.grey[400], size: 28)
: null,
),
- const SizedBox(width: 10), // 10px Gap
- // Text Content
+ const SizedBox(width: 10),
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: const TextStyle(
- fontFamily: 'GeneralSans',
- fontSize: 12,
- color: Color(0xFF27272A),
- fontWeight: FontWeight.w400,
- ),
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- ),
+ Text(
+ vm.parentTitle!,
+ style: Variables.captionStyle.copyWith(fontSize: 12),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
),
Text(
vm.title,
- style: const TextStyle(
- fontFamily: 'GeneralSans',
- fontSize: 16,
+ style: Variables.bodyStyle.copyWith(
fontWeight: FontWeight.w500,
- color: Color(0xFF27272A), // Same black as event
+ fontSize: 16,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
@@ -495,10 +390,8 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> {
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Colors.white,
- borderRadius: BorderRadius.circular(12), // Match recent radius
- border: Border.all(
- color: const Color(0xFFE4E4E7),
- ), // Match recent border
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(color: Variables.borderSubtle),
),
child: Column(
children: [
@@ -512,8 +405,8 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> {
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 4,
- ), // Reduced vertical padding
- visualDensity: VisualDensity.compact, // Reduce height
+ ),
+ visualDensity: VisualDensity.compact,
leading: Container(
width: 48,
height: 48,
@@ -535,11 +428,9 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> {
),
title: Text(
project.title,
- style: const TextStyle(
- fontFamily: 'GeneralSans',
+ style: Variables.bodyStyle.copyWith(
+ fontWeight: FontWeight.w500,
fontSize: 16,
- fontWeight: FontWeight.w500, // Matched with Recent
- color: Color(0xFF27272A),
),
),
trailing:
@@ -551,9 +442,8 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> {
: Icons.keyboard_arrow_down,
color: Colors.grey[600],
),
- onPressed: () {
- setState(() => g.isExpanded = !g.isExpanded);
- },
+ onPressed:
+ () => setState(() => g.isExpanded = !g.isExpanded),
)
: null,
),
@@ -564,7 +454,7 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> {
firstChild: const SizedBox.shrink(),
secondChild: Container(
width: double.infinity,
- color: const Color(0xFFF9FAFB),
+ color: Variables.surfaceSubtle,
child: Column(
children:
g.events.map((e) {
@@ -573,7 +463,7 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> {
contentPadding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 2,
- ), // Reduced vertical padding
+ ),
visualDensity: VisualDensity.compact,
leading: Container(
width: 40,
@@ -601,11 +491,9 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> {
),
title: Text(
e.title,
- style: const TextStyle(
- fontFamily: 'GeneralSans',
+ style: Variables.bodyStyle.copyWith(
fontSize: 15,
- fontWeight: FontWeight.w500, // Matched
- color: Color(0xFF27272A),
+ fontWeight: FontWeight.w500,
),
),
);
diff --git a/lib/ui/widgets/file_card.dart b/lib/ui/widgets/file_card.dart
@@ -12,7 +12,6 @@ class FileCard extends StatelessWidget {
final VoidCallback onTap;
// Optional actions for the context menu.
- // If null, the menu icon is hidden or disabled.
final Function(String)? onMenuAction;
const FileCard({
@@ -28,9 +27,6 @@ class FileCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
- final theme = Theme.of(context);
- final isDark = theme.brightness == Brightness.dark;
-
// Resolve valid image path
final bool hasPreview =
previewPath.isNotEmpty && File(previewPath).existsSync();
@@ -40,175 +36,119 @@ class FileCard extends StatelessWidget {
return GestureDetector(
onTap: onTap,
child: Container(
+ margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
- color: theme.scaffoldBackgroundColor,
- borderRadius: BorderRadius.circular(Variables.radiusSmall),
- // Optional: Add subtle border if needed to match FilePage style
- border: Border.all(
- color: isDark ? Variables.borderDark : Colors.transparent,
- ),
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(16),
+ border: Border.all(color: const Color(0xFFE4E4E7)),
),
child: Row(
- crossAxisAlignment: CrossAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Thumbnail
- SizedBox(
- width: 88,
- height: 88,
- child: Center(
- child: Container(
- width: 80,
- height: 80,
- decoration: BoxDecoration(
- color:
- isDark
- ? Variables.surfaceDark
- : Variables.surfaceSubtle,
- borderRadius: BorderRadius.circular(Variables.radiusSmall),
- boxShadow: [
- BoxShadow(
- color: Colors.black.withOpacity(0.12),
- blurRadius: 6,
- offset: const Offset(0, 3),
- ),
- ],
- ),
- child: ClipRRect(
- borderRadius: BorderRadius.circular(Variables.radiusSmall),
- child:
- hasPreview
- ? Image(
- image: imageProvider!,
- fit: BoxFit.cover,
- errorBuilder:
- (_, __, ___) => _buildPlaceholder(theme),
- )
- : _buildPlaceholder(theme),
- ),
- ),
+ ClipRRect(
+ borderRadius: const BorderRadius.only(
+ topLeft: Radius.circular(16),
+ bottomLeft: Radius.circular(16),
+ ),
+ child: SizedBox(
+ width: 120,
+ height: 120,
+ child:
+ hasPreview
+ ? Image(
+ image: imageProvider!,
+ fit: BoxFit.cover,
+ errorBuilder: (_, __, ___) => _buildPlaceholder(),
+ )
+ : _buildPlaceholder(),
),
),
- const SizedBox(width: 16),
+
+ const SizedBox(width: 12),
+
// Info Column
Expanded(
child: Padding(
- padding: const EdgeInsets.symmetric(vertical: 4),
+ padding: const EdgeInsets.symmetric(vertical: 14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
- mainAxisSize: MainAxisSize.min,
children: [
- Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- if (breadcrumb.isNotEmpty)
- Text(
- breadcrumb,
- style: TextStyle(
- fontSize: 11,
- fontWeight: FontWeight.w500,
- fontFamily: 'GeneralSans',
- color: theme.colorScheme.onSurface
- .withValues(alpha: 0.6),
- ),
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- ),
- const SizedBox(height: 4),
- Text(
- file.name,
- style: TextStyle(
- fontSize: 15,
- fontWeight: FontWeight.w600,
- fontFamily: 'GeneralSans',
- color: theme.colorScheme.onSurface,
- ),
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- ),
- ],
- ),
+ if (breadcrumb.isNotEmpty)
+ Text(
+ breadcrumb,
+ style: const TextStyle(
+ fontSize: 11,
+ color: Color(0xFF71717B),
+ fontFamily: 'GeneralSans',
),
- const SizedBox(width: 8),
- // Menu
- if (onMenuAction != null)
- PopupMenuButton<String>(
- padding: EdgeInsets.zero,
- icon: Icon(
- Icons.more_vert,
- size: 20,
- color: theme.colorScheme.onSurface.withValues(
- alpha: 0.6,
- ),
- ),
- onSelected: onMenuAction,
- itemBuilder:
- (_) => const [
- PopupMenuItem(
- value: "open",
- child: Text("Open"),
- ),
- PopupMenuItem(
- value: "rename",
- child: Text("Rename"),
- ),
- PopupMenuItem(
- value: "delete",
- child: Text("Delete"),
- ),
- ],
- )
- else
- Icon(
- Icons.more_vert,
- size: 20,
- color: theme.colorScheme.onSurface.withValues(
- alpha: 0.6,
- ),
- ),
- ],
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ const SizedBox(height: 4),
+ Text(
+ file.name,
+ style: const TextStyle(
+ fontSize: 15,
+ fontWeight: FontWeight.w600,
+ fontFamily: 'GeneralSans',
+ color: Color(0xFF27272A),
+ ),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 6),
Text(
dimensions,
- style: TextStyle(
- fontSize: 11,
+ style: const TextStyle(
+ fontSize: 13,
+ color: Color(0xFF71717B),
fontFamily: 'GeneralSans',
- color: theme.colorScheme.onSurface.withValues(
- alpha: 0.6,
- ),
),
),
- const SizedBox(height: 4),
+ const SizedBox(height: 6),
Text(
timeAgo,
style: TextStyle(
- fontSize: 11,
+ fontSize: 12,
+ color: const Color(0xFF71717B).withValues(alpha: 0.8),
fontFamily: 'GeneralSans',
- color: theme.colorScheme.onSurface.withValues(
- alpha: 0.4,
- ),
),
),
],
),
),
),
+
+ // Menu
+ if (onMenuAction != null)
+ PopupMenuButton<String>(
+ onSelected: onMenuAction,
+ itemBuilder:
+ (_) => const [
+ PopupMenuItem(value: "open", child: Text("Open")),
+ PopupMenuItem(value: "rename", child: Text("Rename")),
+ PopupMenuItem(value: "delete", child: Text("Delete")),
+ ],
+ icon: const Icon(
+ Icons.more_vert,
+ size: 20,
+ color: Color(0xFF71717B),
+ ),
+ )
+ else
+ const SizedBox(width: 40),
],
),
),
);
}
- Widget _buildPlaceholder(ThemeData theme) {
- return Center(
- child: Icon(
- Icons.image,
- size: 24,
- color: theme.colorScheme.onSurface.withValues(alpha: 0.3),
+ Widget _buildPlaceholder() {
+ return Container(
+ color: Colors.grey[300],
+ child: const Center(
+ child: Icon(Icons.image, size: 32, color: Colors.white),
),
);
}
diff --git a/lib/ui/widgets/moodboard_image_card.dart b/lib/ui/widgets/moodboard_image_card.dart
@@ -0,0 +1,104 @@
+import 'dart:io';
+import 'package:flutter/material.dart';
+import 'package:creekui/data/models/image_model.dart';
+import 'package:creekui/ui/styles/variables.dart';
+import 'package:creekui/ui/widgets/image_context_menu.dart';
+
+class MoodboardImageCard extends StatelessWidget {
+ final ImageModel image;
+ final VoidCallback onTap;
+ final VoidCallback onDeleted;
+ final bool showTags;
+ final double? height;
+
+ const MoodboardImageCard({
+ super.key,
+ required this.image,
+ required this.onTap,
+ required this.onDeleted,
+ this.showTags = false,
+ this.height,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return ImageContextMenu(
+ image: image,
+ onImageDeleted: onDeleted,
+ child: GestureDetector(
+ onTap: onTap,
+ child: Container(
+ height: height,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(Variables.radiusMedium),
+ color: Variables.surfaceSubtle,
+ border: Border.all(color: Variables.borderSubtle),
+ ),
+ 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: Variables.textDisabled,
+ ),
+ ),
+ ),
+ if (showTags && 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),
+ ),
+ child: Text(
+ tag.toUpperCase(),
+ style: const TextStyle(
+ color: Colors.white,
+ fontSize: 9,
+ fontFamily: 'GeneralSans',
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ );
+ }).toList(),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/ui/widgets/note_input_sheet.dart b/lib/ui/widgets/note_input_sheet.dart
@@ -0,0 +1,209 @@
+import 'package:flutter/material.dart';
+import 'package:creekui/ui/styles/variables.dart';
+
+class NoteInputSheet extends StatefulWidget {
+ final List<String> categories;
+ final String initialCategory;
+ final Function(String content, String category) onSubmit;
+
+ const NoteInputSheet({
+ super.key,
+ required this.categories,
+ required this.initialCategory,
+ required this.onSubmit,
+ });
+
+ @override
+ State<NoteInputSheet> createState() => _NoteInputSheetState();
+}
+
+class _NoteInputSheetState extends State<NoteInputSheet> {
+ late String _selectedCategory;
+ final TextEditingController _controller = TextEditingController();
+
+ @override
+ void initState() {
+ super.initState();
+ _selectedCategory = widget.initialCategory;
+ }
+
+ @override
+ void dispose() {
+ _controller.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ // Header Row
+ Row(
+ children: [
+ Container(
+ width: 30,
+ height: 30,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ border: Border.all(
+ color: const Color(0xFFFAFAFA),
+ width: 1.25,
+ ),
+ ),
+ child: const CircleAvatar(
+ radius: 15,
+ backgroundColor: Colors.grey,
+ child: Icon(Icons.person, color: Colors.white, size: 18),
+ ),
+ ),
+ const SizedBox(width: 10),
+ const Text(
+ "Alex", // TODO
+ style: TextStyle(
+ fontFamily: 'GeneralSans',
+ fontSize: 12,
+ fontWeight: FontWeight.w500,
+ color: Colors.black,
+ ),
+ ),
+ const Spacer(),
+ Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 12,
+ vertical: 6,
+ ),
+ decoration: BoxDecoration(
+ color: const Color(0xFFE0E7FF),
+ borderRadius: BorderRadius.circular(1000),
+ ),
+ child: DropdownButtonHideUnderline(
+ child: DropdownButton<String>(
+ value:
+ widget.categories.contains(_selectedCategory)
+ ? _selectedCategory
+ : null,
+ hint: const Text(
+ "Type",
+ style: TextStyle(fontFamily: 'GeneralSans', fontSize: 12),
+ ),
+ isDense: true,
+ icon: const Icon(
+ Icons.arrow_drop_down,
+ size: 20,
+ color: Color(0xFF27272A),
+ ),
+ style: const TextStyle(
+ fontFamily: 'GeneralSans',
+ fontSize: 12,
+ color: Color(0xFF27272A),
+ ),
+ dropdownColor: Colors.white,
+ items:
+ widget.categories
+ .map(
+ (c) => DropdownMenuItem(value: c, child: Text(c)),
+ )
+ .toList(),
+ onChanged: (v) {
+ if (v != null) setState(() => _selectedCategory = v);
+ },
+ ),
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 16),
+ // Input Row
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ Expanded(
+ child: Container(
+ decoration: BoxDecoration(
+ color: const Color(0xFFF4F4F5),
+ border: Border.all(color: const Color(0xFFE4E4E7)),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: TextField(
+ controller: _controller,
+ autofocus: true,
+ maxLines: null,
+ style: const TextStyle(
+ fontFamily: 'GeneralSans',
+ fontSize: 12,
+ ),
+ decoration: const InputDecoration(
+ hintText: "Enter note details...",
+ border: InputBorder.none,
+ contentPadding: EdgeInsets.symmetric(
+ horizontal: 12,
+ vertical: 10,
+ ),
+ ),
+ ),
+ ),
+ ),
+ const SizedBox(width: 8),
+ IconButton(
+ icon: const Icon(
+ Icons.send,
+ color: Color(0xFF27272A),
+ size: 24,
+ ),
+ onPressed: () {
+ if (_controller.text.isNotEmpty) {
+ widget.onSubmit(_controller.text.trim(), _selectedCategory);
+ }
+ },
+ padding: EdgeInsets.zero,
+ constraints: const BoxConstraints(minWidth: 24, minHeight: 24),
+ ),
+ ],
+ ),
+ const SizedBox(height: 10),
+ ],
+ ),
+ );
+ }
+}
+
+class NoteModalOverlay extends StatelessWidget {
+ final Widget modalContent;
+ final Size screenSize;
+
+ const NoteModalOverlay({
+ super.key,
+ required this.modalContent,
+ required this.screenSize,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final mq = MediaQuery.of(context);
+ return Align(
+ alignment: Alignment.bottomCenter,
+ child: AnimatedPadding(
+ duration: const Duration(milliseconds: 250),
+ curve: Curves.easeOut,
+ padding: EdgeInsets.only(bottom: mq.viewInsets.bottom),
+ child: ConstrainedBox(
+ constraints: BoxConstraints(maxHeight: screenSize.height),
+ child: Material(
+ color: Colors.white,
+ elevation: 10,
+ shadowColor: Colors.black26,
+ borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
+ clipBehavior: Clip.antiAlias,
+ child: SingleChildScrollView(
+ padding: EdgeInsets.only(bottom: mq.padding.bottom),
+ child: modalContent,
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/ui/widgets/primary_button.dart b/lib/ui/widgets/primary_button.dart
@@ -0,0 +1,65 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:creekui/ui/styles/variables.dart';
+
+class PrimaryButton extends StatelessWidget {
+ final String text;
+ final VoidCallback? onPressed;
+ final bool isLoading;
+ final String? iconPath;
+
+ const PrimaryButton({
+ super.key,
+ required this.text,
+ this.onPressed,
+ this.isLoading = false,
+ this.iconPath,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return SizedBox(
+ width: double.infinity,
+ child: ElevatedButton(
+ onPressed: isLoading ? null : onPressed,
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Variables.textPrimary,
+ foregroundColor: Colors.white,
+ padding: const EdgeInsets.symmetric(vertical: 12),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(100),
+ ),
+ elevation: 0,
+ ),
+ child:
+ isLoading
+ ? const SizedBox(
+ height: 20,
+ width: 20,
+ child: CircularProgressIndicator(
+ color: Colors.white,
+ strokeWidth: 2,
+ ),
+ )
+ : Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text(text, style: Variables.buttonTextStyle),
+ if (iconPath != null) ...[
+ const SizedBox(width: 12),
+ SvgPicture.asset(
+ iconPath!,
+ width: 18,
+ height: 18,
+ colorFilter: const ColorFilter.mode(
+ Colors.white,
+ BlendMode.srcIn,
+ ),
+ ),
+ ],
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/ui/widgets/selection_overlay_painter.dart b/lib/ui/widgets/selection_overlay_painter.dart
@@ -0,0 +1,115 @@
+import 'dart:ui' as ui;
+import 'package:flutter/material.dart';
+
+enum DragHandle { none, topLeft, topRight, bottomLeft, bottomRight, center }
+
+extension RectUtils on Rect {
+ Rect normalize() {
+ return Rect.fromLTRB(
+ left < right ? left : right,
+ top < bottom ? top : bottom,
+ left > right ? left : right,
+ top > bottom ? top : bottom,
+ );
+ }
+}
+
+class SelectionOverlayPainter extends CustomPainter {
+ final Rect rect;
+ final bool isResizing;
+ final DragHandle activeHandle;
+
+ SelectionOverlayPainter({
+ required this.rect,
+ required this.isResizing,
+ this.activeHandle = DragHandle.none,
+ });
+
+ @override
+ void paint(Canvas canvas, Size size) {
+ // 1. Dim Background
+ if (isResizing) {
+ final Path backgroundPath =
+ Path()..addRect(Rect.fromLTWH(0, 0, size.width, size.height));
+ final Path holePath = Path()..addRect(rect);
+ final Path overlayPath = Path.combine(
+ ui.PathOperation.difference,
+ backgroundPath,
+ holePath,
+ );
+ canvas.drawPath(overlayPath, Paint()..color = Colors.black54);
+ }
+
+ // 2. Draw Dashed Border
+ final Paint borderPaint =
+ Paint()
+ ..color = const Color(0xFF448AFF)
+ ..strokeWidth = 2.0
+ ..style = PaintingStyle.stroke;
+
+ double dashWidth = 6;
+ double dashSpace = 4;
+ Path borderPath = Path()..addRect(rect);
+
+ for (ui.PathMetric pathMetric in borderPath.computeMetrics()) {
+ double distance = 0.0;
+ while (distance < pathMetric.length) {
+ canvas.drawPath(
+ pathMetric.extractPath(distance, distance + dashWidth),
+ borderPaint,
+ );
+ distance += (dashWidth + dashSpace);
+ }
+ }
+
+ // 3. Draw Center Dot (Initial) or Resize Handles (Resizing)
+ if (!isResizing) {
+ canvas.drawCircle(
+ rect.center,
+ 8,
+ Paint()
+ ..color = Colors.black26
+ ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 3),
+ );
+ canvas.drawCircle(
+ rect.center,
+ 6,
+ Paint()
+ ..color = Colors.white
+ ..style = PaintingStyle.fill,
+ );
+ } else {
+ final List<Offset> corners = [
+ rect.topLeft,
+ rect.topRight,
+ rect.bottomLeft,
+ rect.bottomRight,
+ ];
+
+ final Paint handleShadow =
+ Paint()
+ ..color = Colors.black.withOpacity(0.3)
+ ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 2);
+ final Paint handleFill = Paint()..color = Colors.white;
+ final Paint handleBorder =
+ Paint()
+ ..color = const Color(0xFF448AFF)
+ ..strokeWidth = 2
+ ..style = PaintingStyle.stroke;
+
+ const double handleRadius = 8;
+
+ for (final corner in corners) {
+ canvas.drawCircle(corner, handleRadius, handleShadow);
+ canvas.drawCircle(corner, handleRadius, handleFill);
+ canvas.drawCircle(corner, handleRadius, handleBorder);
+ }
+ }
+ }
+
+ @override
+ bool shouldRepaint(covariant SelectionOverlayPainter oldDelegate) =>
+ rect != oldDelegate.rect ||
+ isResizing != oldDelegate.isResizing ||
+ activeHandle != oldDelegate.activeHandle;
+}
diff --git a/lib/ui/widgets/text_field.dart b/lib/ui/widgets/text_field.dart
@@ -0,0 +1,70 @@
+import 'package:flutter/material.dart';
+import 'package:creekui/ui/styles/variables.dart';
+
+class CommonTextField extends StatelessWidget {
+ final String label;
+ final String hintText;
+ final TextEditingController controller;
+ final int maxLines;
+ final bool isRequired;
+ final ValueChanged<String>? onSubmitted;
+
+ const CommonTextField({
+ super.key,
+ required this.label,
+ required this.hintText,
+ required this.controller,
+ this.maxLines = 1,
+ this.isRequired = false,
+ this.onSubmitted,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Text(
+ label,
+ style: Variables.bodyStyle.copyWith(fontWeight: FontWeight.w500),
+ ),
+ if (isRequired)
+ Text(
+ '*',
+ style: Variables.bodyStyle.copyWith(
+ color: const Color(0xFF4F39F6),
+ fontSize: 12,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 6),
+ Container(
+ decoration: BoxDecoration(
+ color: Variables.borderSubtle,
+ borderRadius: BorderRadius.circular(Variables.radiusSmall),
+ ),
+ child: TextField(
+ controller: controller,
+ maxLines: maxLines,
+ onSubmitted: onSubmitted,
+ style: Variables.bodyStyle,
+ decoration: InputDecoration(
+ hintText: hintText,
+ hintStyle: Variables.bodyStyle.copyWith(
+ color: Variables.textSecondary,
+ ),
+ border: InputBorder.none,
+ contentPadding: const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 12,
+ ),
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+}