creek

The AI Image Editor of 2030
commit 4dff95225f394412a3c0b930a4daef15f4002f5f
parent 2e4848288bd1450e0ba9a470ec14210dc2d6ffc9
Author: maydayv7 <maydayv7@gmail.com>
Date:   Mon,  8 Dec 2025 23:34:28 +0530

Add new widgets and refactor

Diffstat:
Mlib/data/models/canvas_models.dart | 2++
Mlib/ui/pages/canvas_page.dart | 133++++++++++++++++++++++++++++++-------------------------------------------------
Mlib/ui/pages/canvas_toolbar/magic_draw_overlay.dart | 51+++++++++++++++++++--------------------------------
Mlib/ui/pages/canvas_toolbar/text_tools_overlay.dart | 30++++++++++++++++++++----------
Mlib/ui/pages/create_file_page.dart | 18+++++-------------
Mlib/ui/pages/define_brand_page.dart | 191+++++++++++++++++++++++++++----------------------------------------------------
Mlib/ui/pages/home_page.dart | 265++++++++++++++++++++++++-------------------------------------------------------
Mlib/ui/pages/image_analysis_page.dart | 93+++++++++++++++++++++----------------------------------------------------------
Mlib/ui/pages/image_details_page.dart | 214++++++++++++++++++++++++++++---------------------------------------------------
Mlib/ui/pages/image_save_page.dart | 16+++++++---------
Mlib/ui/pages/project_board_page.dart | 9++++-----
Mlib/ui/pages/project_detail_page.dart | 240+++++++++++++++++--------------------------------------------------------------
Mlib/ui/pages/project_file_page.dart | 191+++++++++++++++++++++++++------------------------------------------------------
Mlib/ui/pages/project_tag_page.dart | 2+-
Mlib/ui/pages/settings_page.dart | 95++++++++++++++++++-------------------------------------------------------------
Mlib/ui/pages/share_handler_page.dart | 7++++---
Mlib/ui/pages/share_to_file_page.dart | 29++++++++++++-----------------
Mlib/ui/pages/share_to_moodboard_page.dart | 75++++++++++++++++++++++++++++-----------------------------------------------
Mlib/ui/pages/stylesheet_page.dart | 73++++++++++++++++++++++++++++++-------------------------------------------
Mlib/ui/styles/variables.dart | 5+++++
Alib/ui/widgets/app_bar.dart | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/ui/widgets/bottom_bar.dart | 121++++++++++++++++++++++++++++++++++++++-----------------------------------------
Mlib/ui/widgets/canvas/asset_picker_sheet.dart | 20+++-----------------
Mlib/ui/widgets/canvas/canvas_bottom_bar.dart | 18++++++++++--------
Mlib/ui/widgets/canvas/manipulating_box.dart | 3+--
Alib/ui/widgets/dialog.dart | 108+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/ui/widgets/empty_state.dart | 22++++++++--------------
Mlib/ui/widgets/file_card.dart | 182+++++++++++++++++++++++++++++++++++++------------------------------------------
Mlib/ui/widgets/image_context_menu.dart | 2+-
Mlib/ui/widgets/note_input_sheet.dart | 31+++++++++++--------------------
Mlib/ui/widgets/project_card.dart | 3+--
Mlib/ui/widgets/project_selector.dart | 84++++++++++++++++++-------------------------------------------------------------
Mlib/ui/widgets/search_bar.dart | 19+++++--------------
Alib/ui/widgets/secondary_button.dart | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/ui/widgets/selection_overlay_painter.dart | 129++++++++++++++++++++++++++++---------------------------------------------------
Alib/ui/widgets/tag_chip.dart | 48++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/ui/widgets/text_field.dart | 43++++++++++++++++++++++++++++---------------
Mlib/ui/widgets/top_bar.dart | 11++---------
Mlib/utils/image_actions_helper.dart | 146++++++++++++++++++-------------------------------------------------------------
Mlib/utils/image_utils.dart | 13+++++++++++++
40 files changed, 1150 insertions(+), 1698 deletions(-)

diff --git a/lib/data/models/canvas_models.dart b/lib/data/models/canvas_models.dart @@ -1,5 +1,7 @@ import 'dart:ui'; +enum DragHandle { none, topLeft, topRight, bottomLeft, bottomRight, center } + class DrawingPoint { final Offset offset; final Paint paint; diff --git a/lib/ui/pages/canvas_page.dart b/lib/ui/pages/canvas_page.dart @@ -22,10 +22,14 @@ import 'package:creekui/services/flask_service.dart'; import 'package:creekui/ui/styles/variables.dart'; import 'package:creekui/data/models/canvas_models.dart'; import 'package:creekui/ui/painters/canvas_painter.dart'; +import 'package:creekui/utils/image_utils.dart'; import 'package:creekui/ui/widgets/canvas/manipulating_box.dart'; import 'package:creekui/ui/widgets/canvas/canvas_bottom_bar.dart'; import 'package:creekui/ui/widgets/canvas/asset_picker_sheet.dart'; +import 'package:creekui/ui/widgets/app_bar.dart'; +import 'package:creekui/ui/widgets/dialog.dart'; +import 'package:creekui/ui/widgets/text_field.dart'; import './canvas_toolbar/magic_draw_overlay.dart'; import './canvas_toolbar/text_tools_overlay.dart'; import 'project_file_page.dart'; @@ -93,7 +97,6 @@ class _CanvasPageState extends State<CanvasPage> { // Tools bool _isMagicDrawActive = false; bool _isTextToolsActive = false; - bool _isMagicPanelDisabled = false; bool _isViewMode = false; @@ -160,30 +163,18 @@ class _CanvasPageState extends State<CanvasPage> { } Future<bool> _confirmDiscardMagicDraw() async { - if (_magicPaths.isEmpty) return true; // nothing drawn – no popup - - final result = await showDialog<bool>( - context: context, - builder: - (context) => AlertDialog( - title: const Text("Discard Magic Draw?"), - content: const Text( - "Leaving Magic Draw will remove your sketch. Continue?", - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context, false), - child: const Text("Stay"), - ), - TextButton( - onPressed: () => Navigator.pop(context, true), - child: const Text("Discard"), - ), - ], - ), - ); - - return result == true; + if (_magicPaths.isEmpty) return true; + return await ShowDialog.show<bool>( + context, + title: "Discard Magic Draw?", + description: "Leaving Magic Draw will remove your sketch. Continue?", + primaryButtonText: "Discard", + isDestructive: true, + onPrimaryPressed: () => Navigator.pop(context, true), + secondaryButtonText: "Stay", + onSecondaryPressed: () => Navigator.pop(context, false), + ) ?? + false; } Future<void> _analyzeCanvas() async { @@ -551,70 +542,49 @@ class _CanvasPageState extends State<CanvasPage> { return; } - showDialog( - context: context, - builder: - (context) => AlertDialog( - title: const Text("Save Changes?"), - content: const Text( - "Do you want to save your canvas before leaving?", - ), - actions: [ - TextButton( - onPressed: () { - Navigator.pop(context); // Close dialog - Navigator.pop(context); // Leave page - }, - child: const Text( - "Discard", - style: TextStyle(color: Colors.red), - ), - ), - FilledButton( - onPressed: () async { - Navigator.pop(context); // Close dialog - await _saveCanvas(); // Save - // Note: The redirect logic is handled in _saveCanvas for new files. - // For existing files, we pop here. - if (mounted && widget.existingFile != null) { - Navigator.pop(context); - } - }, - child: const Text("Save"), - ), - ], - ), + ShowDialog.show( + context, + title: "Save Changes?", + description: "Do you want to save your canvas before leaving?", + primaryButtonText: "Save", + onPrimaryPressed: () async { + Navigator.pop(context); // Close dialog + await _saveCanvas(); // Save + if (mounted && widget.existingFile != null) { + Navigator.pop(context); + } + }, + secondaryButtonText: "Discard", + onSecondaryPressed: () { + Navigator.pop(context); // Close dialog + Navigator.pop(context); // Leave page + }, ); } Future<String?> _showNameDialog() async { - TextEditingController nameController = TextEditingController( - text: "Untitled Canvas", - ); - return showDialog<String>( + final nameController = TextEditingController(text: "Untitled Canvas"); + return await showDialog<String>( context: context, builder: - (ctx) => AlertDialog( - title: const Text("Save Canvas"), - content: TextField( - controller: nameController, - autofocus: true, - decoration: const InputDecoration( - labelText: "Canvas Name", + (ctx) => Dialog( + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(Variables.radiusLarge), + ), + child: ShowDialog( + title: "Save Canvas", + content: CommonTextField( hintText: "Enter a name for your file", - border: OutlineInputBorder(), + controller: nameController, + autoFocus: true, ), + primaryButtonText: "Save", + onPrimaryPressed: + () => Navigator.pop(ctx, nameController.text.trim()), + secondaryButtonText: "Cancel", + onSecondaryPressed: () => Navigator.pop(ctx), ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(ctx), - child: const Text("Cancel"), - ), - FilledButton( - onPressed: () => Navigator.pop(ctx, nameController.text.trim()), - child: const Text("Save"), - ), - ], ), ); } @@ -1795,9 +1765,6 @@ class _CanvasPageState extends State<CanvasPage> { ], ), ), - backgroundColor: Variables.background, - foregroundColor: Variables.textPrimary, - elevation: 0, actions: [ SafeArea( child: Row( diff --git a/lib/ui/pages/canvas_toolbar/magic_draw_overlay.dart b/lib/ui/pages/canvas_toolbar/magic_draw_overlay.dart @@ -155,21 +155,6 @@ class _MagicDrawToolsState extends State<MagicDrawTools> { }); } - void _handleToolTap(VoidCallback toolAction) { - setState(() { - _showStrokeSlider = false; - _showModelMenu = false; - if (_isViewMode) { - _isViewMode = false; - widget.onViewModeToggle(false); - } - if (widget.isMagicPanelDisabled) { - widget.onMagicPanelActivityToggle(false); - } - }); - toolAction(); - } - @override Widget build(BuildContext context) { if (!widget.isActive) return const SizedBox.shrink(); @@ -210,8 +195,8 @@ class _MagicDrawToolsState extends State<MagicDrawTools> { } Widget _buildToolIcon(dynamic icon, bool isActive, VoidCallback onTap) { - final Color activeColor = Colors.black; - final Color inactiveColor = Colors.grey; + final Color activeColor = Variables.textPrimary; + final Color inactiveColor = Variables.textSecondary; final Color iconColor = isActive ? activeColor : inactiveColor; final Widget iconWidget = @@ -229,7 +214,7 @@ class _MagicDrawToolsState extends State<MagicDrawTools> { child: Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( - color: isActive ? Colors.grey.shade200 : Colors.transparent, + color: isActive ? Variables.surfaceSubtle : Colors.transparent, shape: BoxShape.circle, ), child: iconWidget, @@ -245,7 +230,7 @@ class _MagicDrawToolsState extends State<MagicDrawTools> { children: [ Text( widget.hasImageLayers ? 'Inpainting Models' : 'Sketch Models', - style: const TextStyle(color: Colors.grey, fontSize: 12), + style: Variables.captionStyle.copyWith(fontSize: 12), ), const SizedBox(height: 4), @@ -270,10 +255,9 @@ class _MagicDrawToolsState extends State<MagicDrawTools> { const SizedBox(width: 8), Text( model.name, - style: TextStyle( + style: Variables.bodyStyle.copyWith( fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, - color: Colors.black87, ), ), const Spacer(), @@ -289,10 +273,11 @@ class _MagicDrawToolsState extends State<MagicDrawTools> { ), child: Text( model.badge!, - style: TextStyle( - color: Colors.blue.shade900, + style: const TextStyle( + color: Color(0xFF0D47A1), // Dark blue fontSize: 10, fontWeight: FontWeight.bold, + fontFamily: 'GeneralSans', ), ), ), @@ -301,7 +286,7 @@ class _MagicDrawToolsState extends State<MagicDrawTools> { ), ); }).toList(), - Divider(height: 1, color: Colors.grey.shade100), + const Divider(height: 1, color: Variables.borderSubtle), ], ), ); @@ -320,8 +305,9 @@ class _MagicDrawToolsState extends State<MagicDrawTools> { child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: _showModelMenu ? Colors.grey.shade100 : Colors.white, - border: Border.all(color: Colors.grey.shade300), + color: + _showModelMenu ? Variables.surfaceSubtle : Colors.white, + border: Border.all(color: Variables.borderSubtle), borderRadius: BorderRadius.circular(8), ), child: const Icon( @@ -338,9 +324,10 @@ class _MagicDrawToolsState extends State<MagicDrawTools> { controller: _promptController, enabled: !widget.isProcessing, onSubmitted: (_) => _handleSubmit(), - decoration: const InputDecoration.collapsed( + style: Variables.bodyStyle, + decoration: InputDecoration.collapsed( hintText: "tap imagination...", - hintStyle: TextStyle(fontSize: 14, color: Colors.black54), + hintStyle: Variables.captionStyle.copyWith(fontSize: 14), ), ), ), @@ -352,7 +339,7 @@ class _MagicDrawToolsState extends State<MagicDrawTools> { width: 44, height: 44, decoration: const BoxDecoration( - color: Color(0xFF2B2B2B), + color: Variables.textPrimary, shape: BoxShape.circle, ), child: @@ -374,7 +361,7 @@ class _MagicDrawToolsState extends State<MagicDrawTools> { ], ), ), - Divider(height: 1, color: Colors.grey.shade200), + const Divider(height: 1, color: Variables.borderSubtle), Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( @@ -461,7 +448,7 @@ class _MagicDrawToolsState extends State<MagicDrawTools> { decoration: BoxDecoration( color: _showStrokeSlider - ? Colors.grey.shade200 + ? Variables.surfaceSubtle : Colors.transparent, shape: BoxShape.circle, ), @@ -469,7 +456,7 @@ class _MagicDrawToolsState extends State<MagicDrawTools> { width: 10, height: 10, decoration: const BoxDecoration( - color: Colors.black87, + color: Variables.textPrimary, shape: BoxShape.circle, ), ), diff --git a/lib/ui/pages/canvas_toolbar/text_tools_overlay.dart b/lib/ui/pages/canvas_toolbar/text_tools_overlay.dart @@ -47,7 +47,7 @@ class TextToolsOverlay extends StatelessWidget { ), ], ), - // Use SingleChildScrollView + Row to prevent overflow if screen is narrow + // Prevent overflow if screen is narrow child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( @@ -61,7 +61,7 @@ class TextToolsOverlay extends StatelessWidget { onPressed: onClose, tooltip: "Done", ), - Container(width: 1, height: 20, color: Colors.grey[300]), + Container(width: 1, height: 20, color: Variables.borderSubtle), const SizedBox(width: 8), IconButton( @@ -75,13 +75,17 @@ class TextToolsOverlay extends StatelessWidget { if (isTextSelected) ...[ const SizedBox(width: 8), - Container(width: 1, height: 20, color: Colors.grey[300]), + Container( + width: 1, + height: 20, + color: Variables.borderSubtle, + ), const SizedBox(width: 12), const Icon( Icons.text_fields, size: 18, - color: Colors.black54, + color: Variables.textSecondary, ), SizedBox( width: 100, @@ -92,7 +96,7 @@ class TextToolsOverlay extends StatelessWidget { enabledThumbRadius: 6, ), activeTrackColor: Variables.textPrimary, - inactiveTrackColor: Colors.grey[200], + inactiveTrackColor: Variables.borderSubtle, thumbColor: Colors.black, overlayShape: SliderComponentShape.noOverlay, ), @@ -115,13 +119,20 @@ class TextToolsOverlay extends StatelessWidget { decoration: BoxDecoration( color: currentColor, shape: BoxShape.circle, - border: Border.all(color: Colors.grey[300]!, width: 1), + border: Border.all( + color: Variables.borderSubtle, + width: 1, + ), ), ), ), const SizedBox(width: 12), - Container(width: 1, height: 20, color: Colors.grey[300]), + Container( + width: 1, + height: 20, + color: Variables.borderSubtle, + ), const SizedBox(width: 8), ], ], @@ -144,13 +155,12 @@ class TextToolsOverlay extends StatelessWidget { borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), child: SafeArea( - // Added SafeArea here for the bottom sheet content child: Column( mainAxisSize: MainAxisSize.min, children: [ - const Text( + Text( "Text Color", - style: TextStyle(fontWeight: FontWeight.bold), + style: Variables.headerStyle.copyWith(fontSize: 16), ), const SizedBox(height: 20), BlockPicker( diff --git a/lib/ui/pages/create_file_page.dart b/lib/ui/pages/create_file_page.dart @@ -5,6 +5,7 @@ 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/project_selector.dart'; +import 'package:creekui/ui/widgets/app_bar.dart'; import 'canvas_page.dart'; import 'define_brand_page.dart'; @@ -183,22 +184,13 @@ class _CreateFilePageState extends State<CreateFilePage> { return Scaffold( backgroundColor: theme.scaffoldBackgroundColor, - appBar: AppBar( - backgroundColor: theme.scaffoldBackgroundColor, - elevation: 0, + appBar: CustomAppBar( + title: 'Create Files', + showBack: true, leading: IconButton( icon: Icon(Icons.arrow_back, color: theme.colorScheme.onSurface), onPressed: () => Navigator.pop(context), ), - title: Text( - 'Create Files', - style: Variables.headerStyle.copyWith( - fontSize: 16, - color: theme.colorScheme.onSurface, - ), - ), - - // Show project chooser only when ID not passed actions: [ if (widget.projectId == null) Padding( @@ -409,7 +401,7 @@ class _ProjectSelectionModalContentState if (result != null && result is Map) { widget.onProjectSelected(result["id"], result["title"]); } else { - // If project is created but not selected immediately, refreshing the list is a safe bet. + // If project is created but not selected immediately, refreshing the list is a safe bet setState(() { _selectorKey = UniqueKey(); }); diff --git a/lib/ui/pages/define_brand_page.dart b/lib/ui/pages/define_brand_page.dart @@ -4,6 +4,9 @@ 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 'package:creekui/ui/widgets/app_bar.dart'; +import 'package:creekui/ui/widgets/tag_chip.dart'; +import 'package:creekui/ui/widgets/dialog.dart'; import 'project_detail_page.dart'; class DefineBrandPage extends StatefulWidget { @@ -114,32 +117,47 @@ class _DefineBrandPageState extends State<DefineBrandPage> { } } + void _showAddKeywordDialog() { + final controller = TextEditingController(); + + ShowDialog.show( + context, + title: 'Add Keyword', + primaryButtonText: 'Add', + content: CommonTextField( + hintText: 'e.g., Minimal', + controller: controller, + autoFocus: true, + ), + onPrimaryPressed: () { + final val = controller.text.trim(); + if (val.isNotEmpty && !_keywords.contains(val)) { + setState(() => _keywords.add(val)); + } + Navigator.pop(context); + }, + ); + } + @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Variables.background, + appBar: CustomAppBar( + title: "", + showBack: true, + leading: IconButton( + icon: const Icon( + Icons.arrow_back_ios, + size: 24, + color: Variables.textSecondary, + ), + onPressed: () => Navigator.pop(context), + ), + ), body: SafeArea( child: Column( children: [ - // Top Bar with Back Button - Container( - height: 48, - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), - child: Row( - children: [ - IconButton( - icon: Icon( - Icons.arrow_back_ios, - size: 24, - color: Variables.textSecondary, - ), - onPressed: () => Navigator.pop(context), - ), - ], - ), - ), - - // Scrollable Content Expanded( child: SingleChildScrollView( padding: const EdgeInsets.fromLTRB(16, 0, 16, 0), @@ -271,36 +289,9 @@ class _DefineBrandPageState extends State<DefineBrandPage> { runSpacing: 8, children: [ for (final keyword in _keywords) - Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - decoration: BoxDecoration( - color: const Color(0xFFE0E7FF), - borderRadius: BorderRadius.circular(48), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - keyword, - style: Variables.captionStyle.copyWith( - fontSize: 12, - color: Variables.textPrimary, - ), - ), - const SizedBox(width: 8), - GestureDetector( - onTap: () => setState(() => _keywords.remove(keyword)), - child: Icon( - Icons.close, - size: 16, - color: Variables.textPrimary, - ), - ), - ], - ), + TagChip( + label: keyword, + onDelete: () => setState(() => _keywords.remove(keyword)), ), GestureDetector( onTap: _showAddKeywordDialog, @@ -324,7 +315,11 @@ class _DefineBrandPageState extends State<DefineBrandPage> { ), ), const SizedBox(width: 4), - Icon(Icons.add, size: 14, color: Variables.textPrimary), + const Icon( + Icons.add, + size: 14, + color: Variables.textPrimary, + ), ], ), ), @@ -365,58 +360,33 @@ class _DefineBrandPageState extends State<DefineBrandPage> { const SizedBox(height: 8), SizedBox( height: 44, - child: ListView.builder( + child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: _competitorBrands.length, + separatorBuilder: (_, __) => const SizedBox(width: 8), itemBuilder: (context, index) { final brand = _competitorBrands[index]; - return Container( - margin: const EdgeInsets.only(right: 8), - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - decoration: BoxDecoration( - color: Variables.surfaceSubtle, - border: Border.all(color: Variables.borderSubtle), - borderRadius: BorderRadius.circular(64), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 30, - height: 30, - decoration: const BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - ), - child: Center( - child: Text( - brand['initial'] ?? '', - style: Variables.captionStyle.copyWith( - fontWeight: FontWeight.bold, - fontSize: 12, - ), - ), - ), - ), - const SizedBox(width: 8), - Text(brand['name'] ?? '', style: Variables.bodyStyle), - const SizedBox(width: 8), - GestureDetector( - onTap: - () => setState( - () => _competitorBrands.removeAt(index), - ), - child: Icon( - Icons.close, - size: 16, - color: Variables.textSecondary, + return TagChip( + label: brand['name'] ?? '', + icon: Container( + width: 30, + height: 30, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: Center( + child: Text( + brand['initial'] ?? '', + style: Variables.captionStyle.copyWith( + fontWeight: FontWeight.bold, + fontSize: 12, ), ), - ], + ), ), + onDelete: + () => setState(() => _competitorBrands.removeAt(index)), ); }, ), @@ -425,35 +395,4 @@ class _DefineBrandPageState extends State<DefineBrandPage> { ], ); } - - void _showAddKeywordDialog() { - final controller = TextEditingController(); - showDialog<void>( - context: context, - builder: - (context) => AlertDialog( - title: const Text('Add Keyword'), - content: TextField( - controller: controller, - autofocus: true, - decoration: const InputDecoration(hintText: 'e.g., Minimal'), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Cancel'), - ), - ElevatedButton( - onPressed: () { - final val = controller.text.trim(); - 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 @@ -15,6 +15,8 @@ import 'package:creekui/ui/widgets/file_card.dart'; import 'package:creekui/ui/widgets/project_card.dart'; import 'package:creekui/ui/widgets/empty_state.dart'; import 'package:creekui/ui/widgets/section_header.dart'; +import 'package:creekui/ui/widgets/dialog.dart'; +import 'package:creekui/ui/widgets/text_field.dart'; import 'package:creekui/ui/pages/settings_page.dart'; import 'project_detail_page.dart'; import 'define_brand_page.dart'; @@ -138,9 +140,7 @@ class _HomePageState extends State<HomePage> { try { final bytes = await File(filePath).readAsBytes(); final image = img.decodeImage(bytes); - if (image != null) { - return '${image.width} x ${image.height} px'; - } + if (image != null) return '${image.width} x ${image.height} px'; } catch (e) { debugPrint('Error getting dimensions: $e'); } @@ -150,16 +150,13 @@ class _HomePageState extends State<HomePage> { String _formatTimeAgo(DateTime dateTime) { final now = DateTime.now(); final difference = now.difference(dateTime); - - if (difference.inDays > 0) { + if (difference.inDays > 0) return 'Edited ${difference.inDays} ${difference.inDays == 1 ? 'day' : 'days'} ago'; - } else if (difference.inHours > 0) { + if (difference.inHours > 0) return 'Edited ${difference.inHours} ${difference.inHours == 1 ? 'hour' : 'hours'} ago'; - } else if (difference.inMinutes > 0) { + if (difference.inMinutes > 0) return 'Edited ${difference.inMinutes} ${difference.inMinutes == 1 ? 'minute' : 'minutes'} ago'; - } else { - return 'Edited just now'; - } + return 'Edited just now'; } String _getProjectBreadcrumb(FileModel file) { @@ -169,9 +166,8 @@ class _HomePageState extends State<HomePage> { // If the project has a parent, it's an event. Show Parent / Event if (project.parentId != null) { final parentProject = _projectMap[project.parentId!]; - if (parentProject != null) { + if (parentProject != null) return '${parentProject.title} / ${project.title}'; - } } // Otherwise just the project name return project.title; @@ -187,9 +183,7 @@ class _HomePageState extends State<HomePage> { projectDescription: null, ), ), - ).then((result) { - _loadData(); - }); + ).then((_) => _loadData()); } void _openProject(ProjectModel project) { @@ -207,152 +201,73 @@ class _HomePageState extends State<HomePage> { // Rename Project Logic Future<void> _renameProject(ProjectModel project) async { final controller = TextEditingController(text: project.title); - final theme = Theme.of(context); - final didRename = await showDialog<bool>( - context: context, - builder: - (ctx) => AlertDialog( - backgroundColor: theme.cardColor, - title: Text( - 'Rename Project', - style: Variables.headerStyle.copyWith(fontSize: 18), - ), - content: TextField( - controller: controller, - autofocus: true, - style: Variables.bodyStyle, - decoration: InputDecoration( - hintText: 'Enter new name', - hintStyle: TextStyle( - color: theme.colorScheme.onSurface.withValues(alpha: 0.5), - ), - enabledBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: theme.colorScheme.primary.withValues(alpha: 0.5), - ), - ), - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide(color: theme.colorScheme.primary), - ), - ), - textCapitalization: TextCapitalization.sentences, - ), - actions: [ - TextButton( - child: Text( - 'Cancel', - style: Variables.bodyStyle.copyWith( - color: theme.colorScheme.onSurface.withValues(alpha: 0.7), - ), - ), - onPressed: () => Navigator.pop(ctx, false), - ), - TextButton( - child: Text( - 'Save', - style: Variables.bodyStyle.copyWith( - color: theme.colorScheme.primary, - ), - ), - onPressed: () { - if (controller.text.trim().isNotEmpty) { - Navigator.pop(ctx, true); - } - }, - ), - ], - ), - ); - - if (didRename == true && project.id != null) { - setState(() => _isLoading = true); - try { - await _projectService.updateProjectDetails( - project.id!, - title: controller.text.trim(), - ); - if (mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('Project renamed'))); - } - _loadData(); - } catch (e) { - if (mounted) { - setState(() => _isLoading = false); - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('Error renaming: $e'))); + await ShowDialog.show( + context, + title: 'Rename Project', + primaryButtonText: 'Save', + content: CommonTextField( + hintText: 'Enter new name', + controller: controller, + autoFocus: true, + ), + onPrimaryPressed: () async { + if (controller.text.trim().isNotEmpty) { + try { + await _projectService.updateProjectDetails( + project.id!, + title: controller.text.trim(), + ); + if (mounted) { + Navigator.pop(context); + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('Project renamed'))); + _loadData(); + } + } catch (e) { + if (mounted) + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Error renaming: $e'))); + } } - } - } + }, + ); } // Delete Project Logic Future<void> _deleteProject(ProjectModel project) async { - final theme = Theme.of(context); - final confirm = await showDialog<bool>( - context: context, - builder: - (ctx) => AlertDialog( - backgroundColor: theme.cardColor, - title: Text( - 'Delete Project', - style: Variables.headerStyle.copyWith(fontSize: 18), - ), - content: Text( - 'Are you sure you want to delete "${project.title}"? This cannot be undone.', - style: Variables.bodyStyle.copyWith( - color: theme.colorScheme.onSurface.withValues(alpha: 0.8), - ), - ), - actions: [ - TextButton( - child: Text( - 'Cancel', - style: Variables.bodyStyle.copyWith( - color: theme.colorScheme.onSurface.withValues(alpha: 0.7), - ), - ), - onPressed: () => Navigator.pop(ctx, false), - ), - TextButton( - child: Text( - 'Delete', - style: Variables.bodyStyle.copyWith(color: Colors.red), - ), - onPressed: () => Navigator.pop(ctx, true), - ), - ], - ), - ); - - if (confirm == true && project.id != null) { - setState(() => _isLoading = true); - try { - await _projectService.deleteProject(project.id!); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Project deleted successfully')), - ); - } - _loadData(); - } catch (e) { - if (mounted) { - setState(() => _isLoading = false); - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('Error deleting project: $e'))); + await ShowDialog.show( + context, + title: 'Delete Project', + description: + 'Are you sure you want to delete "${project.title}"? This cannot be undone.', + primaryButtonText: 'Delete', + isDestructive: true, + onPrimaryPressed: () async { + try { + await _projectService.deleteProject(project.id!); + if (mounted) { + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Project deleted successfully')), + ); + _loadData(); + } + } catch (e) { + if (mounted) + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error deleting project: $e')), + ); } - } - } + }, + ); } Future<void> _openFile(FileModel file) async { double width = 1080; double height = 1920; - try { final f = File(file.filePath); if (await f.exists()) { @@ -426,8 +341,8 @@ class _HomePageState extends State<HomePage> { Widget build(BuildContext context) { final theme = Theme.of(context); final isDark = theme.brightness == Brightness.dark; - final bool isSearching = _searchQuery.isNotEmpty; + final List<FileModel> filteredFiles = isSearching ? _recentFiles @@ -518,17 +433,13 @@ class _HomePageState extends State<HomePage> { padding: const EdgeInsets.symmetric(horizontal: 16), child: CommonSearchBar( controller: _searchController, - onChanged: (value) { - setState(() { - _searchQuery = value.trim(); - }); - }, + onChanged: + (value) => + setState(() => _searchQuery = value.trim()), ), ), ), - const SliverToBoxAdapter(child: SizedBox(height: 12)), - if (isSearching) SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 16), @@ -541,14 +452,13 @@ class _HomePageState extends State<HomePage> { title: "No results found", subtitle: "Try searching for something else", ), - if (filteredProjects.isNotEmpty) ...[ Text( "Projects & Events", style: Variables.bodyStyle.copyWith( fontWeight: FontWeight.w600, color: theme.colorScheme.onSurface - .withValues(alpha: 0.7), + .withOpacity(0.7), ), ), const SizedBox(height: 8), @@ -578,7 +488,7 @@ class _HomePageState extends State<HomePage> { style: Variables.bodyStyle.copyWith( fontWeight: FontWeight.w600, color: theme.colorScheme.onSurface - .withValues(alpha: 0.7), + .withOpacity(0.7), ), ), const SizedBox(height: 8), @@ -685,10 +595,7 @@ class _HomePageState extends State<HomePage> { ), ), const SizedBox(height: 24), - SectionHeader( - title: 'Explore templates', - onTap: () {}, - ), + const SectionHeader(title: 'Explore templates'), const SizedBox(height: 12), _buildTemplatesSection(theme, isDark), const SizedBox(height: 24), @@ -706,7 +613,7 @@ class _HomePageState extends State<HomePage> { ? null : FloatingActionButton( onPressed: _createNewProject, - backgroundColor: isDark ? Colors.grey[900] : Colors.grey[900], + backgroundColor: Colors.grey[900], foregroundColor: Colors.white, child: const Icon(Icons.add, size: 24), ), @@ -726,7 +633,6 @@ class _HomePageState extends State<HomePage> { border: Border.all( color: isDark ? Variables.borderDark : Variables.borderSubtle, width: 1, - style: BorderStyle.solid, ), ), child: Column( @@ -735,7 +641,7 @@ class _HomePageState extends State<HomePage> { Icon( Icons.add_circle_outline, size: 32, - color: theme.colorScheme.primary.withValues(alpha: 0.7), + color: theme.colorScheme.primary.withOpacity(0.7), ), const SizedBox(height: 8), Text( @@ -743,7 +649,7 @@ class _HomePageState extends State<HomePage> { style: Variables.bodyStyle.copyWith( fontSize: 12, fontWeight: FontWeight.w500, - color: theme.colorScheme.onSurface.withValues(alpha: 0.7), + color: theme.colorScheme.onSurface.withOpacity(0.7), ), ), ], @@ -804,8 +710,8 @@ class _HomePageState extends State<HomePage> { child: Icon( Icons.image_outlined, size: 32, - color: theme.colorScheme.onSurface.withValues( - alpha: 0.3, + color: theme.colorScheme.onSurface.withOpacity( + 0.3, ), ), ); @@ -829,7 +735,7 @@ class _HomePageState extends State<HomePage> { template['subtitle']!, style: Variables.captionStyle.copyWith( fontSize: 11, - color: theme.colorScheme.onSurface.withValues(alpha: 0.6), + color: theme.colorScheme.onSurface.withOpacity(0.6), height: 1.2, ), maxLines: 1, @@ -895,13 +801,10 @@ class _SeeAllPageState extends State<_SeeAllPage> { if (p.id != null) { final events = await widget.projectRepo.getEvents(p.id!); List<String> eventImages = []; - // Get 1 image from up to 4 distinct events for (final event in events.take(4)) { if (event.id != null) { final imgs = await widget.imageRepo.getImages(event.id!); - if (imgs.isNotEmpty) { - eventImages.add(imgs.first.filePath); - } + if (imgs.isNotEmpty) eventImages.add(imgs.first.filePath); } } _projectPreviews[p.id!] = eventImages; @@ -912,7 +815,6 @@ class _SeeAllPageState extends State<_SeeAllPage> { final files = await widget.fileRepo.getRecentFiles(limit: 50); final allProjects = await widget.projectRepo.getAllProjects(); _projectMap = {for (var p in allProjects) p.id!: p}; - for (final file in files) { try { final f = File(file.filePath); @@ -924,13 +826,11 @@ class _SeeAllPageState extends State<_SeeAllPage> { String dims = 'Unknown'; String? previewPath; if (data is Map) { - if (data['width'] != null && data['height'] != null) { + if (data['width'] != null && data['height'] != null) dims = '${(data['width'] as num).toInt()} x ${(data['height'] as num).toInt()} px'; - } - if (data['preview_path'] != null) { + if (data['preview_path'] != null) previewPath = data['preview_path']; - } } _fileMetadata[file.id] = { 'dimensions': dims, @@ -940,12 +840,11 @@ class _SeeAllPageState extends State<_SeeAllPage> { } else { final bytes = await f.readAsBytes(); final image = img.decodeImage(bytes); - if (image != null) { + if (image != null) _fileMetadata[file.id] = { 'dimensions': '${image.width} x ${image.height} px', 'preview': file.filePath, }; - } } } } catch (_) {} @@ -978,8 +877,6 @@ class _SeeAllPageState extends State<_SeeAllPage> { @override Widget build(BuildContext context) { final theme = Theme.of(context); - final isDark = theme.brightness == Brightness.dark; - return Scaffold( appBar: AppBar( title: Text( diff --git a/lib/ui/pages/image_analysis_page.dart b/lib/ui/pages/image_analysis_page.dart @@ -5,6 +5,8 @@ import 'package:image_picker/image_picker.dart'; import 'package:path_provider/path_provider.dart'; import 'package:creekui/services/analyze/image_analyzer.dart'; import 'package:creekui/ui/styles/variables.dart'; +import 'package:creekui/ui/widgets/app_bar.dart'; +import 'package:creekui/ui/widgets/tag_chip.dart'; class ImageAnalysisPage extends StatefulWidget { const ImageAnalysisPage({super.key}); @@ -115,10 +117,7 @@ class _ImageAnalysisPageState extends State<ImageAnalysisPage> { Icons.camera_alt_outlined, color: Colors.black, ), - title: const Text( - "Take Photo", - style: TextStyle(fontFamily: 'GeneralSans'), - ), + title: Text("Take Photo", style: Variables.bodyStyle), onTap: () { Navigator.pop(ctx); _pickImage(ImageSource.camera); @@ -129,9 +128,9 @@ class _ImageAnalysisPageState extends State<ImageAnalysisPage> { Icons.image_outlined, color: Colors.black, ), - title: const Text( + title: Text( "Choose from Gallery", - style: TextStyle(fontFamily: 'GeneralSans'), + style: Variables.bodyStyle, ), onTap: () { Navigator.pop(ctx); @@ -149,22 +148,14 @@ class _ImageAnalysisPageState extends State<ImageAnalysisPage> { Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Scaffold( - backgroundColor: Colors.white, - appBar: AppBar( - backgroundColor: Colors.white, - elevation: 0, + backgroundColor: Variables.surfaceBackground, + appBar: CustomAppBar( + title: "Image Analysis", + showBack: true, leading: IconButton( icon: const Icon(Icons.arrow_back, color: Colors.black), onPressed: () => Navigator.pop(context), ), - title: const Text( - "Image Analysis", - style: TextStyle( - color: Colors.black, - fontFamily: 'GeneralSans', - fontWeight: FontWeight.bold, - ), - ), actions: [ if (_selectedImage != null && !_isAnalyzing) IconButton( @@ -185,7 +176,7 @@ class _ImageAnalysisPageState extends State<ImageAnalysisPage> { width: double.infinity, height: 250, decoration: BoxDecoration( - color: Colors.grey[100], + color: Variables.surfaceSubtle, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( @@ -214,8 +205,7 @@ class _ImageAnalysisPageState extends State<ImageAnalysisPage> { const SizedBox(height: 12), Text( "Select image to analyze", - style: TextStyle( - fontFamily: 'GeneralSans', + style: Variables.bodyStyle.copyWith( color: Colors.grey[500], ), ), @@ -259,52 +249,17 @@ class _ImageAnalysisPageState extends State<ImageAnalysisPage> { } }); }, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - decoration: BoxDecoration( - color: - isSelected - ? const Color(0xFFEEF0FF) - : Colors.white, - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: - isSelected - ? const Color(0xFF7C4DFF) - : Colors.grey[300]!, - width: 1.0, - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (isSelected) ...[ - const Icon( - Icons.check, - size: 14, - color: Color(0xFF7C4DFF), - ), - const SizedBox(width: 4), - ], - Text( - tag, - style: TextStyle( - fontSize: 13, - fontWeight: - isSelected - ? FontWeight.w600 - : FontWeight.normal, - color: - isSelected - ? const Color(0xFF7C4DFF) - : Colors.black87, - ), - ), - ], - ), + child: TagChip( + label: tag, + icon: + isSelected + ? const Icon( + Icons.check, + size: 14, + color: Variables.chipText, + ) + : null, + onDelete: () {}, ), ); }).toList(), @@ -367,9 +322,9 @@ class _ImageAnalysisPageState extends State<ImageAnalysisPage> { width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Variables.borderSubtle, + color: Variables.surfaceSubtle, borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.grey[800]!), + border: Border.all(color: Variables.borderSubtle), ), child: SelectableText( prettyJson, diff --git a/lib/ui/pages/image_details_page.dart b/lib/ui/pages/image_details_page.dart @@ -5,12 +5,17 @@ import 'package:creekui/services/image_service.dart'; 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/data/models/canvas_models.dart'; import 'package:creekui/utils/image_actions_helper.dart'; +import 'package:creekui/utils/image_utils.dart'; 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'; +import 'package:creekui/ui/widgets/app_bar.dart'; +import 'package:creekui/ui/widgets/dialog.dart'; +import 'package:creekui/ui/widgets/tag_chip.dart'; class ImageDetailsPage extends StatefulWidget { final String imagePath; @@ -356,96 +361,60 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> { // Edit Tags Dialog void _openEditTagsDialog() { + List<String> tempTags = List.from(_currentTags); showDialog( context: context, - builder: (ctx) { - List<String> tempTags = List.from(_currentTags); - return StatefulBuilder( - builder: (context, setState) { - return AlertDialog( - title: const Text( - "Edit Tags", - style: TextStyle(fontWeight: FontWeight.bold), - ), - content: SingleChildScrollView( - child: Wrap( - spacing: 8, - runSpacing: 8, - children: - _allAvailableTags.map((tag) { - final isSelected = tempTags.contains(tag); - return GestureDetector( - onTap: - () => setState( - () => - isSelected - ? tempTags.remove(tag) - : tempTags.add(tag), - ), - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - decoration: BoxDecoration( - color: - isSelected - ? const Color(0xFFEEF0FF) - : Colors.white, - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: - isSelected - ? const Color(0xFF7C4DFF) - : Colors.grey[300]!, - ), - ), - child: Text( - tag, - style: TextStyle( - fontSize: 13, - fontWeight: - isSelected - ? FontWeight.w600 - : FontWeight.normal, - color: - isSelected - ? const Color(0xFF7C4DFF) - : Colors.black87, - ), - ), - ), - ); - }).toList(), - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text( - "Cancel", - style: TextStyle(color: Colors.grey), - ), + builder: + (ctx) => StatefulBuilder( + builder: (context, setLocalState) { + return Dialog( + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(Variables.radiusLarge), ), - TextButton( - onPressed: () async { - this.setState(() => _currentTags = tempTags); + child: ShowDialog( + title: "Edit Tags", + primaryButtonText: "Save", + onPrimaryPressed: () async { + setState(() => _currentTags = tempTags); await _imageService.updateTags(widget.imageId, tempTags); - if (context.mounted) Navigator.pop(context); + if (mounted) Navigator.pop(context); }, - child: const Text( - "Save", - style: TextStyle( - color: Colors.black, - fontWeight: FontWeight.bold, + content: SingleChildScrollView( + child: Wrap( + spacing: 8, + runSpacing: 8, + children: + _allAvailableTags.map((tag) { + final isSelected = tempTags.contains(tag); + return GestureDetector( + onTap: () { + setLocalState(() { + isSelected + ? tempTags.remove(tag) + : tempTags.add(tag); + }); + }, + child: TagChip( + label: tag, + icon: + isSelected + ? const Icon( + Icons.check, + size: 14, + color: Variables.chipText, + ) + : null, + onDelete: () {}, + ), + ); + }).toList(), ), ), ), - ], - ); - }, - ); - }, + ); + }, + ), ); } @@ -457,25 +426,24 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> { return Scaffold( // Prevents the main screen from pushing up when the keyboard opens resizeToAvoidBottomInset: false, - backgroundColor: Colors.white, - appBar: AppBar( - backgroundColor: Colors.white, - elevation: 0, + backgroundColor: Variables.surfaceBackground, + appBar: CustomAppBar( + showBack: true, leading: IconButton( icon: const Icon( Icons.arrow_back_ios_new, size: 20, - color: Colors.black, + color: Variables.textPrimary, ), onPressed: () => Navigator.pop(context), ), centerTitle: true, - title: + titleWidget: (!isSelectionModeActive && _imageModel != null) ? Container( height: 40, decoration: BoxDecoration( - color: const Color(0xFFF3F4F6), + color: Variables.surfaceSubtle, borderRadius: BorderRadius.circular(30), ), child: InkWell( @@ -495,17 +463,16 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> { width: 18, height: 18, colorFilter: const ColorFilter.mode( - Colors.black, + Variables.textPrimary, BlendMode.srcIn, ), ), const SizedBox(width: 8), - const Text( + Text( "Send to File", - style: TextStyle( - color: Colors.black, - fontWeight: FontWeight.w600, + style: Variables.headerStyle.copyWith( fontSize: 14, + fontWeight: FontWeight.w600, ), ), ], @@ -528,7 +495,7 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> { // 2. Cancel Selection (Any selection mode) if (isSelectionModeActive) IconButton( - icon: const Icon(Icons.close, color: Colors.black), + icon: const Icon(Icons.close, color: Variables.textPrimary), onPressed: _resetSelectionMode, ), @@ -546,7 +513,7 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> { width: 20, height: 20, colorFilter: const ColorFilter.mode( - Colors.black, + Variables.textPrimary, BlendMode.srcIn, ), ), @@ -558,11 +525,11 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> { () => ImageActionsHelper.renameImage( context, _imageModel!, - () => _loadData(), // Refresh title after rename + () => _loadData(), ), icon: const Icon( Icons.drive_file_rename_outline, - color: Colors.black, + color: Variables.textPrimary, ), ), ], @@ -571,7 +538,7 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> { body: _isLoading ? const Center( - child: CircularProgressIndicator(color: Colors.black), + child: CircularProgressIndicator(color: Variables.textPrimary), ) : Column( children: [ @@ -822,7 +789,7 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> { color: const Color(0xFFF9F9F9), borderRadius: BorderRadius.circular(16), border: Border.all( - color: Colors.grey[200]!, + color: Variables.borderSubtle, ), ), child: Column( @@ -843,15 +810,15 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> { child: const Icon( Icons.edit_outlined, size: 18, - color: Colors.black54, + color: Variables.textSecondary, ), ), ], ), const SizedBox(height: 12), - Divider( + const Divider( height: 1, - color: Colors.grey[300], + color: Variables.borderSubtle, ), const SizedBox(height: 16), SizedBox( @@ -870,40 +837,10 @@ class _ImageDetailsPageState extends State<ImageDetailsPage> { children: _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, - ), - ), - ), - child: Text( - tag, - style: const TextStyle( - fontSize: 13, - fontWeight: - FontWeight - .w500, - color: Color( - 0xFF7C4DFF, - ), - ), - ), + (tag) => TagChip( + label: tag, + onDelete: () {}, + icon: null, ), ) .toList(), @@ -950,12 +887,13 @@ 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, ); + } }); } } diff --git a/lib/ui/pages/image_save_page.dart b/lib/ui/pages/image_save_page.dart @@ -1,12 +1,15 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:creekui/data/models/canvas_models.dart'; import 'package:creekui/services/image_service.dart'; import 'package:creekui/services/note_service.dart'; +import 'package:creekui/utils/image_utils.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 'package:creekui/ui/widgets/app_bar.dart'; import 'project_board_page.dart'; // Temporary note model @@ -430,22 +433,17 @@ class _ImageSavePageState extends State<ImageSavePage> { return Scaffold( resizeToAvoidBottomInset: false, - backgroundColor: Colors.white, - appBar: AppBar( - backgroundColor: Colors.white, - elevation: 0, + backgroundColor: Variables.surfaceBackground, + appBar: CustomAppBar( + title: titleText, leading: IconButton( icon: const Icon( Icons.arrow_back_ios_new, size: 20, - color: Colors.black, + color: Variables.textPrimary, ), onPressed: () => Navigator.pop(context), ), - title: Text( - titleText, - style: Variables.headerStyle.copyWith(fontSize: 20), - ), actions: [ // Confirm Selection Button (Visible only in resizing mode) if (_isResizing && _finalSelectionRect != null) diff --git a/lib/ui/pages/project_board_page.dart b/lib/ui/pages/project_board_page.dart @@ -48,7 +48,7 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> { @override void initState() { super.initState(); - // Set initial view based on parameter, default to true (grid view) + // Set initial view, default to grid view _showAlternateView = widget.initialShowAlternateView ?? true; _initData(); } @@ -158,13 +158,13 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> { Widget build(BuildContext context) { if (_currentProject == null) { return const Scaffold( - backgroundColor: Variables.background, + backgroundColor: Variables.surfaceBackground, body: Center(child: CircularProgressIndicator()), ); } return Scaffold( - backgroundColor: Variables.background, + backgroundColor: Variables.surfaceBackground, appBar: TopBar( currentProjectId: _currentProject!.id!, onBack: () => Navigator.pop(context), @@ -220,7 +220,7 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> { Widget _buildCategorizedView() { if (_isLoading) return const Center(child: CircularProgressIndicator()); if (_categorizedImages.isEmpty) { - return EmptyState( + return const EmptyState( icon: Icons.image_not_supported_outlined, title: "No images found", subtitle: "Try adding new images", @@ -308,7 +308,6 @@ class _ProjectBoardPageState extends State<ProjectBoardPage> { ), ); }, - child: Container( width: 120, decoration: BoxDecoration( diff --git a/lib/ui/pages/project_detail_page.dart b/lib/ui/pages/project_detail_page.dart @@ -9,6 +9,9 @@ import 'package:creekui/services/project_service.dart'; import 'package:creekui/ui/styles/variables.dart'; import 'package:creekui/ui/widgets/empty_state.dart'; import 'package:creekui/ui/widgets/section_header.dart'; +import 'package:creekui/ui/widgets/app_bar.dart'; +import 'package:creekui/ui/widgets/dialog.dart'; +import 'package:creekui/ui/widgets/text_field.dart'; import 'package:creekui/ui/pages/settings_page.dart'; import 'package:creekui/ui/pages/home_page.dart'; import 'project_board_page.dart'; @@ -90,182 +93,45 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { if (!mounted) return; - await showModalBottomSheet( - context: context, - backgroundColor: Colors.transparent, - isScrollControlled: true, - builder: - (context) => Container( - decoration: const BoxDecoration( - color: Color(0xFFFAFAFA), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(24), - topRight: Radius.circular(24), - ), - ), - child: Padding( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: const EdgeInsets.only(top: 8, bottom: 16), - child: Center( - child: Container( - width: 48, - height: 2, - decoration: BoxDecoration( - color: const Color(0xFF71717B), - borderRadius: BorderRadius.circular(100), - ), - ), - ), - ), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide(color: Color(0xFFE4E4E7), width: 1), - ), - ), - child: Text("Event Details", style: Variables.bodyStyle), - ), - Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - height: 40, - decoration: BoxDecoration( - color: const Color(0xFFE4E4E7), - borderRadius: BorderRadius.circular(8), - ), - child: TextField( - controller: nameController, - autofocus: true, - style: Variables.bodyStyle.copyWith(fontSize: 12), - decoration: InputDecoration( - hintText: "Add Name*", - hintStyle: Variables.bodyStyle.copyWith( - fontSize: 12, - color: const Color(0xFF71717B), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - border: InputBorder.none, - ), - ), - ), - const SizedBox(height: 16), - Container( - height: 80, - decoration: BoxDecoration( - color: const Color(0xFFE4E4E7), - borderRadius: BorderRadius.circular(8), - ), - child: TextField( - controller: descriptionController, - maxLines: null, - expands: true, - textAlignVertical: TextAlignVertical.top, - style: Variables.bodyStyle.copyWith(fontSize: 12), - decoration: InputDecoration( - hintText: "Add Description", - hintStyle: Variables.bodyStyle.copyWith( - fontSize: 12, - color: const Color(0xFF71717B), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - border: InputBorder.none, - ), - ), - ), - ], - ), - ), - Container( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 32), - child: Row( - children: [ - Expanded( - child: GestureDetector( - onTap: () => Navigator.pop(context), - child: Container( - height: 40, - decoration: BoxDecoration( - color: const Color(0xFFE4E4E7), - borderRadius: BorderRadius.circular(1000), - ), - child: Center( - child: Text( - "Cancel", - style: Variables.buttonTextStyle.copyWith( - color: Variables.textPrimary, - ), - ), - ), - ), - ), - ), - const SizedBox(width: 8), - Expanded( - child: GestureDetector( - onTap: () async { - if (nameController.text.trim().isNotEmpty) { - try { - await _projectService.createProject( - nameController.text.trim(), - description: - descriptionController.text - .trim() - .isEmpty - ? null - : descriptionController.text.trim(), - parentId: _project!.id, - ); - if (context.mounted) { - Navigator.pop(context); - _loadData(); - } - } catch (e) { - debugPrint("Error creating event: $e"); - } - } - }, - child: Container( - height: 40, - decoration: BoxDecoration( - color: const Color(0xFF27272A), - borderRadius: BorderRadius.circular(1000), - ), - child: Center( - child: Text( - "Add Event", - style: Variables.buttonTextStyle, - ), - ), - ), - ), - ), - ], - ), - ), - ], - ), - ), + await ShowDialog.show( + context, + title: "Event Details", + primaryButtonText: "Add Event", + content: Column( + children: [ + CommonTextField( + hintText: "Add Name*", + controller: nameController, + autoFocus: true, + ), + const SizedBox(height: 16), + CommonTextField( + hintText: "Add Description", + controller: descriptionController, + maxLines: 3, ), + ], + ), + onPrimaryPressed: () async { + if (nameController.text.trim().isNotEmpty) { + try { + await _projectService.createProject( + nameController.text.trim(), + description: + descriptionController.text.trim().isEmpty + ? null + : descriptionController.text.trim(), + parentId: _project!.id, + ); + if (context.mounted) { + Navigator.pop(context); + _loadData(); + } + } catch (e) { + debugPrint("Error creating event: $e"); + } + } + }, ); } @@ -294,7 +160,7 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { Widget build(BuildContext context) { if (_isLoading) { return const Scaffold( - backgroundColor: Color(0xFFFAFAFA), + backgroundColor: Variables.surfaceBackground, body: Center(child: CircularProgressIndicator()), ); } @@ -302,14 +168,10 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { if (_project == null) return const Scaffold(body: SizedBox()); return Scaffold( - backgroundColor: const Color(0xFFFAFAFA), - appBar: AppBar( - title: Text(_project!.title, style: Variables.headerStyle), - backgroundColor: const Color(0xFFFAFAFA), - elevation: 0, - leadingWidth: 50, - titleSpacing: 0, - automaticallyImplyLeading: false, + backgroundColor: Variables.surfaceBackground, + appBar: CustomAppBar( + title: _project!.title, + showBack: true, leading: IconButton( icon: const Icon( Icons.arrow_back, @@ -351,7 +213,7 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { onRefresh: _loadData, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), - padding: const EdgeInsets.only(left: 16, right: 16), + padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -451,12 +313,12 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), - border: Border.all(color: const Color(0xFFE4E4E7), width: 1), + border: Border.all(color: Variables.borderSubtle, width: 1), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Blob image container with dark background + // Blob image container Container( height: 115, decoration: const BoxDecoration( @@ -556,7 +418,7 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { width: 328, decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), - border: Border.all(color: const Color(0xFFE4E4E7), width: 1), + border: Border.all(color: Variables.borderSubtle, width: 1), ), clipBehavior: Clip.antiAlias, child: Column( @@ -591,7 +453,7 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { ), // Event title Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 16), + padding: const EdgeInsets.all(16), child: Text( event.title, style: Variables.bodyStyle.copyWith( diff --git a/lib/ui/pages/project_file_page.dart b/lib/ui/pages/project_file_page.dart @@ -12,6 +12,8 @@ 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/dialog.dart'; +import 'package:creekui/ui/widgets/text_field.dart'; import 'create_file_page.dart'; import 'canvas_page.dart'; @@ -51,9 +53,7 @@ class _ProjectFilePageState extends State<ProjectFilePage> { super.dispose(); } - // --------------------------------------- - // LOAD EVERYTHING - // --------------------------------------- + // Load data Future<void> _loadEverything() async { setState(() => _isLoading = true); try { @@ -77,9 +77,7 @@ class _ProjectFilePageState extends State<ProjectFilePage> { if (mounted) setState(() => _isLoading = false); } - // --------------------------------------- - // LOAD METADATA - // --------------------------------------- + // File Metadata Future<void> _loadMetadata(List<FileModel> list) async { for (final file in list) { try { @@ -128,9 +126,6 @@ class _ProjectFilePageState extends State<ProjectFilePage> { } } - // --------------------------------------- - // SELECT EVENT - // --------------------------------------- Future<void> _onSelectEvent(ProjectModel event) async { setState(() => _selectedEvent = event); _eventFiles = await _fileService.getFiles(event.id!); @@ -138,9 +133,6 @@ class _ProjectFilePageState extends State<ProjectFilePage> { setState(() {}); } - // --------------------------------------- - // OPEN FILE - // --------------------------------------- void _openFile(FileModel file) { Navigator.push( context, @@ -175,91 +167,57 @@ class _ProjectFilePageState extends State<ProjectFilePage> { Future<void> _renameFile(FileModel file) async { final controller = TextEditingController(text: file.name); - final newName = await showDialog<String>( - context: context, - builder: (context) { - return AlertDialog( - title: Text( - "Rename File", - style: Variables.headerStyle.copyWith(fontSize: 18), - ), - content: TextField( - controller: controller, - autofocus: true, - style: Variables.bodyStyle, - decoration: const InputDecoration( - labelText: "New file name", - border: OutlineInputBorder(), - ), - ), - actions: [ - TextButton( - child: Text("Cancel", style: Variables.bodyStyle), - onPressed: () => Navigator.pop(context), - ), - FilledButton( - child: Text("Save", style: Variables.buttonTextStyle), - onPressed: () => Navigator.pop(context, controller.text.trim()), - ), - ], - ); + await ShowDialog.show( + context, + title: "Rename File", + primaryButtonText: "Save", + content: CommonTextField( + hintText: "New file name", + controller: controller, + autoFocus: true, + ), + onPrimaryPressed: () async { + final newName = controller.text.trim(); + if (newName.isNotEmpty) { + await _fileService.renameFile(file.id, newName); + await _loadEverything(); + if (mounted) Navigator.pop(context); + } }, ); - - if (newName == null || newName.isEmpty) return; - await _fileService.renameFile(file.id, newName); - await _loadEverything(); } Future<void> _deleteFile(FileModel file) async { - final confirm = await showDialog<bool>( - context: context, - builder: - (_) => AlertDialog( - title: Text( - "Delete File?", - style: Variables.headerStyle.copyWith(fontSize: 18), - ), - content: Text( - "This will permanently remove ${file.name}.", - style: Variables.bodyStyle, - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context, false), - child: Text("Cancel", style: Variables.bodyStyle), - ), - FilledButton( - onPressed: () => Navigator.pop(context, true), - child: Text("Delete", style: Variables.buttonTextStyle), - ), - ], - ), + await ShowDialog.show( + context, + title: "Delete File?", + description: "This will permanently remove ${file.name}.", + primaryButtonText: "Delete", + isDestructive: true, + onPrimaryPressed: () async { + try { + await _fileService.deleteFile(file.id!); + final disk = File(file.filePath); + if (await disk.exists()) await disk.delete(); + + setState(() { + _allFiles.removeWhere((f) => f.id == file.id); + _eventFiles.removeWhere((f) => f.id == file.id); + }); + + if (mounted) { + Navigator.pop(context); + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text("File deleted"))); + } + } catch (e) { + debugPrint("Delete error: $e"); + } + }, ); - - if (confirm != true) return; - - try { - await _fileService.deleteFile(file.id!); - final disk = File(file.filePath); - if (await disk.exists()) await disk.delete(); - - setState(() { - _allFiles.removeWhere((f) => f.id == file.id); - _eventFiles.removeWhere((f) => f.id == file.id); - }); - - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text("File deleted"))); - } catch (e) { - debugPrint("Delete error: $e"); - } } - // --------------------------------------- - // CREATE FILE - // --------------------------------------- void _navigateToCreateFile() { Navigator.push( context, @@ -304,9 +262,7 @@ class _ProjectFilePageState extends State<ProjectFilePage> { ); } - // --------------------------------------- - // BREADCRUMB - // --------------------------------------- + // Breadcrumb String _breadcrumbFor(FileModel file) { final event = _events.firstWhere( (e) => e.id == file.projectId, @@ -335,13 +291,11 @@ class _ProjectFilePageState extends State<ProjectFilePage> { return "${parent.title} / ${event.title}"; } - // --------------------------------------- - // UI - // --------------------------------------- + // UI Builder @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFFF7F7F8), + backgroundColor: Variables.surfaceSubtle, appBar: TopBar( currentProjectId: widget.projectId, onBack: () => Navigator.pop(context), @@ -371,7 +325,7 @@ class _ProjectFilePageState extends State<ProjectFilePage> { child: Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), decoration: BoxDecoration( - color: const Color(0xFF27272A), + color: Variables.textPrimary, borderRadius: BorderRadius.circular(50), ), child: Row( @@ -400,31 +354,22 @@ class _ProjectFilePageState extends State<ProjectFilePage> { crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 12), - // Search Bar CommonSearchBar( controller: _searchController, hintText: "Search your files", onChanged: (v) => setState(() => _search = v.trim()), ), - const SizedBox(height: 24), - - // ------------------------- - // ALL FILES - // ------------------------- - const Text( + // All Files + Text( "All Files", - style: TextStyle( + style: Variables.bodyStyle.copyWith( fontSize: 16, fontWeight: FontWeight.w600, - fontFamily: 'GeneralSans', - color: Color(0xFF27272A), ), ), - const SizedBox(height: 12), - if (_filteredAllFiles().isEmpty) EmptyState( icon: Icons.folder_outlined, @@ -444,24 +389,16 @@ class _ProjectFilePageState extends State<ProjectFilePage> { .map((f) => _buildFileCard(f)) .toList(), ), - const SizedBox(height: 32), - - // ------------------------- - // FILES FOR EVENTS - // ------------------------- - const Text( + // Files for Events + Text( "Files for Events", - style: TextStyle( + style: Variables.bodyStyle.copyWith( fontSize: 16, fontWeight: FontWeight.w600, - fontFamily: 'GeneralSans', - color: Color(0xFF27272A), ), ), - const SizedBox(height: 12), - if (_events.isEmpty) const EmptyState( icon: Icons.event_outlined, @@ -472,13 +409,8 @@ class _ProjectFilePageState extends State<ProjectFilePage> { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // ----------------------- - // DROPDOWN - // ----------------------- _buildEventDropdown(), - const SizedBox(height: 16), - _eventFiles.isEmpty ? const EmptyState( icon: Icons.insert_drive_file_outlined, @@ -494,7 +426,6 @@ class _ProjectFilePageState extends State<ProjectFilePage> { ), ], ), - const SizedBox(height: 100), ], ), @@ -503,9 +434,7 @@ class _ProjectFilePageState extends State<ProjectFilePage> { ); } - // --------------------------------------- - // DROPDOWN - // --------------------------------------- + // Dropdown Widget _buildEventDropdown() { return Container( height: 36, @@ -555,9 +484,7 @@ class _ProjectFilePageState extends State<ProjectFilePage> { ); } - // --------------------------------------- - // FILTER - // --------------------------------------- + // Filter List<FileModel> _filteredAllFiles() { if (_search.isEmpty) return _allFiles; return _allFiles diff --git a/lib/ui/pages/project_tag_page.dart b/lib/ui/pages/project_tag_page.dart @@ -133,7 +133,7 @@ class _ProjectTagPageState extends State<ProjectTagPage> { } return Scaffold( - backgroundColor: Variables.background, + backgroundColor: Variables.surfaceBackground, appBar: TopBar( currentProjectId: widget.projectId, titleOverride: widget.tag.toUpperCase(), diff --git a/lib/ui/pages/settings_page.dart b/lib/ui/pages/settings_page.dart @@ -3,6 +3,8 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:creekui/ui/styles/variables.dart'; import 'package:creekui/ui/pages/image_analysis_page.dart'; import 'package:creekui/ui/widgets/primary_button.dart'; +import 'package:creekui/ui/widgets/app_bar.dart'; +import 'package:creekui/ui/widgets/text_field.dart'; class SettingsPage extends StatefulWidget { const SettingsPage({super.key}); @@ -25,9 +27,7 @@ class _SettingsPageState extends State<SettingsPage> { Future<void> _loadUserData() async { final prefs = await SharedPreferences.getInstance(); String name = prefs.getString('user_name') ?? 'Alex'; - if (name.trim().isEmpty) { - name = 'Alex'; - } + if (name.trim().isEmpty) name = 'Alex'; setState(() { _nameController.text = name; @@ -39,17 +39,10 @@ class _SettingsPageState extends State<SettingsPage> { Future<void> _saveUserName() async { final newName = _nameController.text.trim(); if (newName.isEmpty) return; - final prefs = await SharedPreferences.getInstance(); await prefs.setString('user_name', newName); - - setState(() { - _originalName = newName; - }); - - if (mounted) { - FocusScope.of(context).unfocus(); - } + setState(() => _originalName = newName); + if (mounted) FocusScope.of(context).unfocus(); } @override @@ -63,18 +56,7 @@ class _SettingsPageState extends State<SettingsPage> { final bool hasChanges = _nameController.text.trim() != _originalName; return Scaffold( backgroundColor: Colors.white, - appBar: AppBar( - title: const Text( - "Settings", - style: TextStyle( - fontFamily: 'GeneralSans', - color: Variables.textPrimary, - ), - ), - backgroundColor: Colors.white, - elevation: 0, - iconTheme: const IconThemeData(color: Variables.textPrimary), - ), + appBar: const CustomAppBar(title: "Settings"), body: _isLoading ? const Center(child: CircularProgressIndicator()) @@ -93,58 +75,23 @@ class _SettingsPageState extends State<SettingsPage> { ), ), const SizedBox(height: 16), - - // User Name Field - const Text( - "Your Name", - style: TextStyle( - fontFamily: 'GeneralSans', - fontSize: 14, - color: Variables.textSecondary, - ), - ), - const SizedBox(height: 8), - TextField( + CommonTextField( + label: "Your Name", + hintText: "Enter your name", controller: _nameController, - onChanged: (val) { - setState( - () {}, - ); // Trigger rebuild to show/hide checkmark - }, - decoration: InputDecoration( - hintText: "Enter your name", - hintStyle: TextStyle( - color: Variables.textSecondary.withValues(alpha: 0.5), - ), - filled: true, - fillColor: Variables.surfaceSubtle, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide.none, - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - suffixIcon: - hasChanges - ? IconButton( - icon: const Icon( - Icons.check, - color: Variables.textPrimary, - ), - onPressed: _saveUserName, - tooltip: 'Save Name', - ) - : null, - ), - style: const TextStyle( - fontFamily: 'GeneralSans', - fontSize: 16, - color: Variables.textPrimary, - ), + onChanged: (val) => setState(() {}), + suffixIcon: + hasChanges + ? IconButton( + icon: const Icon( + Icons.check, + color: Variables.textPrimary, + ), + onPressed: _saveUserName, + tooltip: 'Save Name', + ) + : null, ), - const Spacer(), // Test Analysis diff --git a/lib/ui/pages/share_handler_page.dart b/lib/ui/pages/share_handler_page.dart @@ -5,6 +5,7 @@ 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 'package:creekui/ui/widgets/app_bar.dart'; import 'share_to_moodboard_page.dart'; import 'share_to_file_page.dart'; @@ -118,8 +119,8 @@ class _ShareHandlerPageState extends State<ShareHandlerPage> { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: Colors.white, - appBar: AppBar(title: const Text("Processing")), + backgroundColor: Variables.background, + appBar: const CustomAppBar(title: "Processing", showBack: false), body: Center( child: _hasError @@ -158,7 +159,7 @@ class _ShareHandlerPageState extends State<ShareHandlerPage> { : const Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - CircularProgressIndicator(), + CircularProgressIndicator(color: Variables.textPrimary), SizedBox(height: 20), Text("Downloading media..."), ], diff --git a/lib/ui/pages/share_to_file_page.dart b/lib/ui/pages/share_to_file_page.dart @@ -11,6 +11,7 @@ import 'package:creekui/ui/widgets/search_bar.dart'; import 'package:creekui/ui/widgets/file_card.dart'; import 'package:creekui/ui/widgets/section_header.dart'; import 'package:creekui/ui/widgets/empty_state.dart'; +import 'package:creekui/ui/widgets/app_bar.dart'; import 'create_file_page.dart'; import 'canvas_page.dart'; @@ -279,25 +280,17 @@ class _ShareToFilePageState extends State<ShareToFilePage> { @override Widget build(BuildContext context) { - final theme = Theme.of(context); return Scaffold( - backgroundColor: theme.scaffoldBackgroundColor, - appBar: AppBar( - backgroundColor: theme.scaffoldBackgroundColor, - elevation: 0, + backgroundColor: Variables.surfaceBackground, + appBar: CustomAppBar( + title: 'Files', leading: IconButton( - icon: Icon(Icons.arrow_back, color: theme.colorScheme.onSurface), + icon: const Icon(Icons.arrow_back, color: Variables.textPrimary), onPressed: () => Navigator.pop(context), ), - title: Text( - 'Files', - style: Variables.headerStyle.copyWith( - color: theme.colorScheme.onSurface, - ), - ), actions: [ IconButton( - icon: Icon(Icons.add, color: theme.colorScheme.onSurface, size: 28), + icon: const Icon(Icons.add, color: Variables.textPrimary, size: 28), onPressed: _onAddPressed, tooltip: "Create New File", ), @@ -305,7 +298,9 @@ class _ShareToFilePageState extends State<ShareToFilePage> { ), body: _isLoading - ? const Center(child: CircularProgressIndicator()) + ? const Center( + child: CircularProgressIndicator(color: Variables.textPrimary), + ) : _allFiles.isEmpty ? _buildEmptyState() : Column( @@ -398,11 +393,11 @@ class _ShareToFilePageState extends State<ShareToFilePage> { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.folder_open, size: 80, color: Colors.grey[300]), + const Icon(Icons.folder_open, size: 80, color: Colors.grey), const SizedBox(height: 16), - Text( + const Text( "No files yet", - style: TextStyle(color: Colors.grey[500], fontSize: 16), + style: TextStyle(color: Colors.grey, fontSize: 16), ), const SizedBox(height: 8), TextButton( diff --git a/lib/ui/pages/share_to_moodboard_page.dart b/lib/ui/pages/share_to_moodboard_page.dart @@ -4,6 +4,9 @@ import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:creekui/services/project_service.dart'; import 'package:creekui/ui/styles/variables.dart'; import 'package:creekui/ui/widgets/project_selector.dart'; +import 'package:creekui/ui/widgets/app_bar.dart'; +import 'package:creekui/ui/widgets/dialog.dart'; +import 'package:creekui/ui/widgets/text_field.dart'; import 'image_save_page.dart'; class ShareToMoodboardPage extends StatefulWidget { @@ -20,40 +23,27 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> { Future<void> _createNewProject() async { final controller = TextEditingController(); - final String? title = await showDialog<String>( - context: context, - builder: - (context) => AlertDialog( - title: const Text( - "New Project", - style: TextStyle(fontFamily: 'GeneralSans'), - ), - content: TextField( - controller: controller, - decoration: const InputDecoration(hintText: "Project Title"), - autofocus: true, - textCapitalization: TextCapitalization.sentences, - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text("Cancel"), - ), - FilledButton( - onPressed: () => Navigator.pop(context, controller.text.trim()), - child: const Text("Create"), - ), - ], - ), + await ShowDialog.show( + context, + title: "New Project", + primaryButtonText: "Create", + content: CommonTextField( + hintText: "Project Title", + controller: controller, + autoFocus: true, + ), + onPrimaryPressed: () async { + final title = controller.text.trim(); + if (title.isNotEmpty) { + Navigator.pop(context); // Close dialog + final newId = await _projectService.createProject(title); + setState(() { + _selectorKey = UniqueKey(); + }); + _navigateToSavePage(newId, title); + } + }, ); - - if (title != null && title.isNotEmpty) { - final newId = await _projectService.createProject(title); - setState(() { - _selectorKey = UniqueKey(); - }); - _navigateToSavePage(newId, title); - } } void _navigateToSavePage( @@ -80,26 +70,17 @@ class _ShareToMoodboardPageState extends State<ShareToMoodboardPage> { @override Widget build(BuildContext context) { - final theme = Theme.of(context); - return Scaffold( - backgroundColor: theme.scaffoldBackgroundColor, - appBar: AppBar( - backgroundColor: theme.scaffoldBackgroundColor, - elevation: 0, + backgroundColor: Variables.surfaceBackground, + appBar: CustomAppBar( + title: "MoodBoards", leading: IconButton( - icon: Icon(Icons.arrow_back, color: theme.colorScheme.onSurface), + icon: const Icon(Icons.arrow_back, color: Variables.textPrimary), onPressed: () => Navigator.pop(context), ), - title: Text( - "MoodBoards", - style: Variables.headerStyle.copyWith( - color: theme.colorScheme.onSurface, - ), - ), actions: [ IconButton( - icon: Icon(Icons.add, color: theme.colorScheme.onSurface, size: 28), + icon: const Icon(Icons.add, color: Variables.textPrimary, size: 28), onPressed: _createNewProject, tooltip: "Create New Project", ), diff --git a/lib/ui/pages/stylesheet_page.dart b/lib/ui/pages/stylesheet_page.dart @@ -15,6 +15,7 @@ import 'package:creekui/ui/widgets/bottom_bar.dart'; import 'package:creekui/ui/widgets/top_bar.dart'; import 'package:creekui/ui/styles/variables.dart'; import 'package:creekui/ui/widgets/section_header.dart'; +import 'package:creekui/ui/widgets/primary_button.dart'; class StylesheetPage extends StatefulWidget { final int projectId; @@ -201,7 +202,7 @@ class _StylesheetPageState extends State<StylesheetPage> { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: Variables.background, + backgroundColor: Variables.surfaceBackground, appBar: TopBar( currentProjectId: _currentProjectId, onBack: () => Navigator.of(context).pop(), @@ -232,17 +233,24 @@ class _StylesheetPageState extends State<StylesheetPage> { Widget _buildEmptyState() { return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - "Are you ready to start building\nyour visual identity", - style: Variables.headerStyle.copyWith(fontSize: 18), - textAlign: TextAlign.center, - ), - const SizedBox(height: 24), - _buildGenerateButton("Generate Stylesheet"), - ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 40), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Are you ready to start building\nyour visual identity", + style: Variables.headerStyle.copyWith(fontSize: 18), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + PrimaryButton( + text: "Generate Stylesheet", + iconPath: 'assets/icons/generate_icon.svg', + onPressed: _generateStylesheet, + ), + ], + ), ), ); } @@ -258,7 +266,16 @@ class _StylesheetPageState extends State<StylesheetPage> { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Center(child: _buildGenerateButton("Regenerate Stylesheet")), + Center( + child: SizedBox( + width: 200, + child: PrimaryButton( + text: "Regenerate Stylesheet", + iconPath: 'assets/icons/generate_icon.svg', + onPressed: _generateStylesheet, + ), + ), + ), const SizedBox(height: 24), _buildLogosSection(null), @@ -283,36 +300,6 @@ class _StylesheetPageState extends State<StylesheetPage> { ); } - Widget _buildGenerateButton(String label) { - return GestureDetector( - onTap: _generateStylesheet, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), - decoration: BoxDecoration( - color: Variables.textPrimary, - borderRadius: BorderRadius.circular(112), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(label, style: Variables.buttonTextStyle), - const SizedBox(width: 8), - SvgPicture.asset( - 'assets/icons/generate_icon.svg', - width: 18, - height: 18, - colorFilter: const ColorFilter.mode( - Colors.white, - BlendMode.srcIn, - ), - ), - ], - ), - ), - ); - } - // Logos Widget _buildLogosSection(dynamic data) { List<String> logoPaths = List.from(_logoPaths); diff --git a/lib/ui/styles/variables.dart b/lib/ui/styles/variables.dart @@ -8,9 +8,13 @@ class Variables { static const Color textDisabled = Color(0xFFA1A1AA); static const Color surfaceSubtle = Color(0xFFF4F4F5); + static const Color surfaceBackground = Color(0xFFFAFAFA); static const Color background = Colors.white; static const Color borderSubtle = Color(0xFFE4E4E7); + static const Color chipBackground = Color(0xFFE0E7FF); + static const Color chipText = Color(0xFF7C86FF); + // Dark Mode static const Color surfaceDark = Color(0xFF27272A); static const Color backgroundDark = Color(0xFF18181B); @@ -36,6 +40,7 @@ class Variables { // Radius static const double radiusSmall = 8.0; static const double radiusMedium = 12.0; + static const double radiusLarge = 16.0; // Added // Text Styles static TextStyle get headerStyle => const TextStyle( diff --git a/lib/ui/widgets/app_bar.dart b/lib/ui/widgets/app_bar.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:creekui/ui/styles/variables.dart'; + +class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { + final String? title; + final Widget? titleWidget; + final bool showBack; + final VoidCallback? onBack; + final List<Widget>? actions; + final Widget? leading; + final double? leadingWidth; + final bool centerTitle; + + const CustomAppBar({ + super.key, + this.title, + this.titleWidget, + this.showBack = true, + this.onBack, + this.actions, + this.leading, + this.leadingWidth, + this.centerTitle = false, + }); + + @override + Widget build(BuildContext context) { + return AppBar( + title: + titleWidget ?? + (title != null ? Text(title!, style: Variables.headerStyle) : null), + backgroundColor: Variables.surfaceBackground, + elevation: 0, + centerTitle: centerTitle, + leadingWidth: leadingWidth ?? (showBack ? 50 : 0), + titleSpacing: showBack ? 0 : 16, + automaticallyImplyLeading: false, + leading: + leading ?? + (showBack + ? IconButton( + icon: const Icon( + Icons.arrow_back, + size: 20, + color: Variables.textPrimary, + ), + onPressed: onBack ?? () => Navigator.pop(context), + ) + : null), + actions: actions, + ); + } + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); +} diff --git a/lib/ui/widgets/bottom_bar.dart b/lib/ui/widgets/bottom_bar.dart @@ -1,4 +1,3 @@ -import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:creekui/ui/styles/variables.dart'; @@ -18,26 +17,26 @@ class BottomBar extends StatelessWidget { required this.projectId, }); - void _onTap(BuildContext context, BottomBarItem item) { + void _onItemTapped(BuildContext context, BottomBarItem item) { if (item == currentTab) return; - Widget nextPage; + Widget page; switch (item) { case BottomBarItem.moodboard: - nextPage = ProjectBoardPage(projectId: projectId); + page = ProjectBoardPage(projectId: projectId); break; case BottomBarItem.stylesheet: - nextPage = StylesheetPage(projectId: projectId); + page = StylesheetPage(projectId: projectId); break; case BottomBarItem.files: - nextPage = ProjectFilePage(projectId: projectId); + page = ProjectFilePage(projectId: projectId); break; } Navigator.pushReplacement( context, PageRouteBuilder( - pageBuilder: (context, anim1, anim2) => nextPage, + pageBuilder: (_, __, ___) => page, transitionDuration: Duration.zero, reverseTransitionDuration: Duration.zero, ), @@ -46,82 +45,78 @@ class BottomBar extends StatelessWidget { @override Widget build(BuildContext context) { - // System Safe Area Padding - final double safeBottom = MediaQuery.of(context).padding.bottom; - final double effectiveBottomPadding = max(safeBottom, 24.0); - return Container( - decoration: const BoxDecoration( + height: 80, // Fixed height for consistency + decoration: BoxDecoration( color: Variables.background, - border: Border( - top: BorderSide(color: Variables.borderSubtle, width: 1), - ), + border: Border(top: BorderSide(color: Variables.borderSubtle)), ), - padding: EdgeInsets.only(bottom: effectiveBottomPadding, top: 12), - child: SizedBox( - height: 54, - child: Row( - children: [ - Expanded( - child: _buildNavItem( - context, - BottomBarItem.moodboard, - "Moodboard", - "assets/icons/moodboard_icon.svg", - ), - ), - Expanded( - child: _buildNavItem( - context, - BottomBarItem.stylesheet, - "Stylesheet", - "assets/icons/stylesheet_icon.svg", - ), - ), - Expanded( - child: _buildNavItem( - context, - BottomBarItem.files, - "Files", - "assets/icons/files_icon.svg", - ), - ), - ], - ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _NavBarItem( + iconPath: 'assets/icons/moodboard_icon.svg', + label: 'Moodboard', + isActive: currentTab == BottomBarItem.moodboard, + onTap: () => _onItemTapped(context, BottomBarItem.moodboard), + ), + _NavBarItem( + iconPath: 'assets/icons/stylesheet.svg', + label: 'Stylesheet', + isActive: currentTab == BottomBarItem.stylesheet, + onTap: () => _onItemTapped(context, BottomBarItem.stylesheet), + ), + _NavBarItem( + iconPath: 'assets/icons/files_icon.svg', + label: 'Files', + isActive: currentTab == BottomBarItem.files, + onTap: () => _onItemTapped(context, BottomBarItem.files), + ), + ], ), ); } +} + +class _NavBarItem extends StatelessWidget { + final String iconPath; + final String label; + final bool isActive; + final VoidCallback onTap; - Widget _buildNavItem( - BuildContext context, - BottomBarItem item, - String label, - String assetPath, - ) { - final bool isSelected = item == currentTab; - final Color color = - isSelected ? Variables.textPrimary : Variables.textDisabled; + const _NavBarItem({ + required this.iconPath, + required this.label, + required this.isActive, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final color = isActive ? Variables.iconActive : Variables.iconInactive; - return GestureDetector( - onTap: () => _onTap(context, item), - behavior: HitTestBehavior.opaque, - child: Center( + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Column( mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, children: [ SvgPicture.asset( - assetPath, + iconPath, width: 24, height: 24, colorFilter: ColorFilter.mode(color, BlendMode.srcIn), ), - const SizedBox(height: 6), + const SizedBox(height: 4), Text( label, style: Variables.captionStyle.copyWith( color: color, - fontSize: 11, - fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, + fontWeight: isActive ? FontWeight.w600 : FontWeight.w500, + fontSize: 10, ), ), ], diff --git a/lib/ui/widgets/canvas/asset_picker_sheet.dart b/lib/ui/widgets/canvas/asset_picker_sheet.dart @@ -4,6 +4,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as p; import 'package:creekui/data/repos/project_repo.dart'; import 'package:creekui/ui/styles/variables.dart'; +import 'package:creekui/ui/widgets/primary_button.dart'; class AssetPickerSheet extends StatefulWidget { final int projectId; @@ -188,24 +189,9 @@ class _AssetPickerSheetState extends State<AssetPickerSheet> { child: Container( width: double.infinity, margin: const EdgeInsets.only(top: 16, bottom: 16), - child: ElevatedButton( + child: PrimaryButton( + text: "Add to File", onPressed: () => widget.onAddAssets(_selectedPaths.toList()), - style: ElevatedButton.styleFrom( - backgroundColor: Variables.surfaceDark, - foregroundColor: Variables.textWhite, - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(30), - ), - ), - child: const Text( - "Add to File", - style: TextStyle( - fontFamily: 'GeneralSans', - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), ), ), ), diff --git a/lib/ui/widgets/canvas/canvas_bottom_bar.dart b/lib/ui/widgets/canvas/canvas_bottom_bar.dart @@ -111,28 +111,30 @@ class _BottomBarItem extends StatelessWidget { }); @override Widget build(BuildContext context) { + final color = isActive ? Variables.iconActive : Variables.iconInactive; + return InkWell( onTap: onTap, + borderRadius: BorderRadius.circular(8), child: Padding( padding: const EdgeInsets.all(8.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, children: [ SvgPicture.asset( iconPath, width: 24, - colorFilter: ColorFilter.mode( - isActive ? Variables.iconActive : Variables.iconInactive, - BlendMode.srcIn, - ), + height: 24, + colorFilter: ColorFilter.mode(color, BlendMode.srcIn), ), const SizedBox(height: 6), Text( label, - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w500, - color: isActive ? Variables.iconActive : Variables.iconInactive, + style: Variables.captionStyle.copyWith( + color: color, + fontWeight: isActive ? FontWeight.w600 : FontWeight.w500, + fontSize: 10, ), ), ], diff --git a/lib/ui/widgets/canvas/manipulating_box.dart b/lib/ui/widgets/canvas/manipulating_box.dart @@ -153,8 +153,7 @@ class _ManipulatingBoxState extends State<ManipulatingBox> { final rotated = _rotateVector(scaledDelta, -_rot); setState(() { _pos += scaledDelta; - _previousFocalPoint = - currentFocalPoint; // Update for next frame + _previousFocalPoint = currentFocalPoint; }); widget.onUpdate(_pos, _size, _rot); } diff --git a/lib/ui/widgets/dialog.dart b/lib/ui/widgets/dialog.dart @@ -0,0 +1,108 @@ +import 'package:flutter/material.dart'; +import 'package:creekui/ui/styles/variables.dart'; +import 'package:creekui/ui/widgets/primary_button.dart'; +import 'package:creekui/ui/widgets/secondary_button.dart'; + +class ShowDialog extends StatelessWidget { + final String title; + final String? description; + final Widget? content; + final String primaryButtonText; + final VoidCallback onPrimaryPressed; + final String secondaryButtonText; + final VoidCallback? onSecondaryPressed; + final bool + isDestructive; // Makes primary button red if true (future enhancement) + final bool isLoading; + + const ShowDialog({ + super.key, + required this.title, + this.description, + this.content, + required this.primaryButtonText, + required this.onPrimaryPressed, + this.secondaryButtonText = "Cancel", + this.onSecondaryPressed, + this.isDestructive = false, + this.isLoading = false, + }); + + static Future<T?> show<T>( + BuildContext context, { + required String title, + String? description, + Widget? content, + required String primaryButtonText, + required VoidCallback onPrimaryPressed, + String secondaryButtonText = "Cancel", + VoidCallback? onSecondaryPressed, + bool isDestructive = false, + bool isLoading = false, + }) { + return showDialog<T>( + context: context, + builder: + (context) => Dialog( + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(Variables.radiusLarge), + ), + child: ShowDialog( + title: title, + description: description, + content: content, + primaryButtonText: primaryButtonText, + onPrimaryPressed: onPrimaryPressed, + secondaryButtonText: secondaryButtonText, + onSecondaryPressed: onSecondaryPressed, + isDestructive: isDestructive, + isLoading: isLoading, + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: Variables.headerStyle.copyWith(fontSize: 18)), + if (description != null) ...[ + const SizedBox(height: 8), + Text( + description!, + style: Variables.bodyStyle.copyWith( + color: Variables.textSecondary, + ), + ), + ], + if (content != null) ...[const SizedBox(height: 16), content!], + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: SecondaryButton( + text: secondaryButtonText, + onPressed: onSecondaryPressed ?? () => Navigator.pop(context), + ), + ), + const SizedBox(width: 12), + Expanded( + child: PrimaryButton( + text: primaryButtonText, + onPressed: onPrimaryPressed, + isLoading: isLoading, + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/ui/widgets/empty_state.dart b/lib/ui/widgets/empty_state.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:creekui/ui/styles/variables.dart'; class EmptyState extends StatelessWidget { final IconData icon; @@ -14,30 +15,23 @@ class EmptyState extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(vertical: 48), + return Center( child: Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(icon, size: 64, color: Colors.grey[400]), + Icon(icon, size: 48, color: Variables.textDisabled), const SizedBox(height: 16), Text( title, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - fontFamily: 'GeneralSans', - color: Colors.grey[600], + style: Variables.bodyStyle.copyWith( + fontWeight: FontWeight.w600, + color: Variables.textPrimary, ), ), const SizedBox(height: 8), Text( subtitle, - style: TextStyle( - fontSize: 14, - fontFamily: 'GeneralSans', - color: Colors.grey[500], - ), + style: Variables.captionStyle, textAlign: TextAlign.center, ), ], diff --git a/lib/ui/widgets/file_card.dart b/lib/ui/widgets/file_card.dart @@ -27,129 +27,119 @@ class FileCard extends StatelessWidget { @override Widget build(BuildContext context) { - // Resolve valid image path - final bool hasPreview = - previewPath.isNotEmpty && File(previewPath).existsSync(); - final ImageProvider? imageProvider = - hasPreview ? FileImage(File(previewPath)) : null; - return GestureDetector( onTap: onTap, child: Container( - margin: const EdgeInsets.only(bottom: 12), + height: 100, decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: const Color(0xFFE4E4E7)), + color: Variables.background, + borderRadius: BorderRadius.circular(Variables.radiusMedium), + border: Border.all(color: Variables.borderSubtle), ), + padding: const EdgeInsets.all(12), child: Row( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Thumbnail - 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(), + // Preview Image + Container( + width: 76, + height: 76, + decoration: BoxDecoration( + color: Variables.surfaceSubtle, + borderRadius: BorderRadius.circular(Variables.radiusSmall), ), + clipBehavior: Clip.antiAlias, + child: + previewPath.isNotEmpty + ? Image.file( + File(previewPath), + fit: BoxFit.cover, + errorBuilder: + (context, error, stackTrace) => const Center( + child: Icon( + Icons.broken_image, + color: Variables.textDisabled, + ), + ), + ) + : const Center( + child: Icon(Icons.image, color: Variables.textDisabled), + ), ), - - const SizedBox(width: 12), - - // Info Column + const SizedBox(width: 16), + // Info Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 14), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (breadcrumb.isNotEmpty) - Text( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (breadcrumb.isNotEmpty) + Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Text( breadcrumb, - style: const TextStyle( - fontSize: 11, - color: Color(0xFF71717B), - fontFamily: 'GeneralSans', - ), + style: Variables.captionStyle, 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: const TextStyle( - fontSize: 13, - color: Color(0xFF71717B), - fontFamily: 'GeneralSans', - ), + Text( + file.name, + style: Variables.bodyStyle.copyWith( + fontWeight: FontWeight.w600, ), - const SizedBox(height: 6), - Text( - timeAgo, - style: TextStyle( - fontSize: 12, - color: const Color(0xFF71717B).withValues(alpha: 0.8), - fontFamily: 'GeneralSans', + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Row( + children: [ + Text(dimensions, style: Variables.captionStyle), + const SizedBox(width: 8), + Container( + width: 3, + height: 3, + decoration: const BoxDecoration( + color: Variables.textDisabled, + shape: BoxShape.circle, + ), ), - ), - ], - ), + const SizedBox(width: 8), + Text(timeAgo, style: Variables.captionStyle), + ], + ), + ], ), ), - - // Menu + // Menu Action 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), + color: Variables.textSecondary, ), - ) - else - const SizedBox(width: 40), + onSelected: onMenuAction, + itemBuilder: + (BuildContext context) => <PopupMenuEntry<String>>[ + const PopupMenuItem<String>( + value: 'open', + child: Text('Open'), + ), + const PopupMenuItem<String>( + value: 'rename', + child: Text('Rename'), + ), + const PopupMenuItem<String>( + value: 'delete', + child: Text( + 'Delete', + style: TextStyle(color: Colors.red), + ), + ), + ], + ), ], ), ), ); } - - 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/image_context_menu.dart b/lib/ui/widgets/image_context_menu.dart @@ -312,7 +312,7 @@ class _FloatingCircleButtonState extends State<_FloatingCircleButton> { shape: BoxShape.circle, boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.15), + color: Colors.black.withOpacity(0.15), blurRadius: 10, offset: const Offset(0, 4), ), diff --git a/lib/ui/widgets/note_input_sheet.dart b/lib/ui/widgets/note_input_sheet.dart @@ -61,7 +61,7 @@ class _NoteInputSheetState extends State<NoteInputSheet> { decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( - color: const Color(0xFFFAFAFA), + color: Variables.surfaceBackground, width: 1.25, ), ), @@ -74,11 +74,9 @@ class _NoteInputSheetState extends State<NoteInputSheet> { const SizedBox(width: 10), Text( _userName, - style: const TextStyle( - fontFamily: 'GeneralSans', + style: Variables.bodyStyle.copyWith( fontSize: 12, fontWeight: FontWeight.w500, - color: Colors.black, ), ), const Spacer(), @@ -88,7 +86,7 @@ class _NoteInputSheetState extends State<NoteInputSheet> { vertical: 6, ), decoration: BoxDecoration( - color: const Color(0xFFE0E7FF), + color: Variables.chipBackground, borderRadius: BorderRadius.circular(1000), ), child: DropdownButtonHideUnderline( @@ -97,21 +95,17 @@ class _NoteInputSheetState extends State<NoteInputSheet> { widget.categories.contains(_selectedCategory) ? _selectedCategory : null, - hint: const Text( + hint: Text( "Type", - style: TextStyle(fontFamily: 'GeneralSans', fontSize: 12), + style: Variables.bodyStyle.copyWith(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), + color: Variables.textPrimary, ), + style: Variables.bodyStyle.copyWith(fontSize: 12), dropdownColor: Colors.white, items: widget.categories @@ -135,18 +129,15 @@ class _NoteInputSheetState extends State<NoteInputSheet> { Expanded( child: Container( decoration: BoxDecoration( - color: const Color(0xFFF4F4F5), - border: Border.all(color: const Color(0xFFE4E4E7)), + color: Variables.surfaceSubtle, + border: Border.all(color: Variables.borderSubtle), borderRadius: BorderRadius.circular(8), ), child: TextField( controller: _controller, autofocus: true, maxLines: null, - style: const TextStyle( - fontFamily: 'GeneralSans', - fontSize: 12, - ), + style: Variables.bodyStyle.copyWith(fontSize: 12), decoration: const InputDecoration( hintText: "Enter note details...", border: InputBorder.none, @@ -162,7 +153,7 @@ class _NoteInputSheetState extends State<NoteInputSheet> { IconButton( icon: const Icon( Icons.send, - color: Color(0xFF27272A), + color: Variables.textPrimary, size: 24, ), onPressed: () { diff --git a/lib/ui/widgets/project_card.dart b/lib/ui/widgets/project_card.dart @@ -73,10 +73,9 @@ class ProjectCard extends StatelessWidget { Expanded( child: Text( project.title, - style: TextStyle( + style: Variables.bodyStyle.copyWith( fontSize: 13, fontWeight: FontWeight.w600, - fontFamily: 'GeneralSans', color: theme.colorScheme.onSurface, ), maxLines: 1, diff --git a/lib/ui/widgets/project_selector.dart b/lib/ui/widgets/project_selector.dart @@ -103,9 +103,7 @@ class _ProjectSelectorState extends State<ProjectSelector> { final parent = await _projectRepo.getProjectById(item.parentId!); parentTitle = parent?.title; } - final cover = await _getProjectCover(item.id!); - recents.add( ProjectItemViewModel( item: item, @@ -124,9 +122,7 @@ class _ProjectSelectorState extends State<ProjectSelector> { 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)); } @@ -180,9 +176,6 @@ class _ProjectSelectorState extends State<ProjectSelector> { @override Widget build(BuildContext context) { - final theme = Theme.of(context); - final isDark = theme.brightness == Brightness.dark; - if (_isLoading) { return const Center(child: CircularProgressIndicator()); } @@ -198,7 +191,6 @@ class _ProjectSelectorState extends State<ProjectSelector> { hintText: widget.searchHint, ), ), - Expanded( child: SingleChildScrollView( controller: widget.scrollController, @@ -228,7 +220,7 @@ class _ProjectSelectorState extends State<ProjectSelector> { child: Column( children: _recentViewModels - .map((vm) => _buildRecentItem(vm, theme, isDark)) + .map((vm) => _buildRecentItem(vm)) .toList(), ), ), @@ -256,8 +248,6 @@ class _ProjectSelectorState extends State<ProjectSelector> { itemBuilder: (context, index) => _buildProjectGroup( _filteredGroupedProjects[index], - theme, - isDark, ), ), ), @@ -271,12 +261,7 @@ class _ProjectSelectorState extends State<ProjectSelector> { ); } - // Widgets - Widget _buildRecentItem( - ProjectItemViewModel vm, - ThemeData theme, - bool isDark, - ) { + Widget _buildRecentItem(ProjectItemViewModel vm) { return Container( margin: const EdgeInsets.only(bottom: 8), child: InkWell( @@ -285,12 +270,9 @@ class _ProjectSelectorState extends State<ProjectSelector> { child: Container( padding: const EdgeInsets.fromLTRB(4, 4, 0, 4), decoration: BoxDecoration( - color: theme.scaffoldBackgroundColor, + color: Variables.background, borderRadius: BorderRadius.circular(Variables.radiusMedium), - border: Border.all( - color: isDark ? Variables.borderDark : Variables.borderSubtle, - width: 1, - ), + border: Border.all(color: Variables.borderSubtle, width: 1), ), child: Row( children: [ @@ -299,8 +281,7 @@ class _ProjectSelectorState extends State<ProjectSelector> { width: 56, height: 56, decoration: BoxDecoration( - color: - isDark ? Variables.surfaceDark : Variables.surfaceSubtle, + color: Variables.surfaceSubtle, borderRadius: BorderRadius.circular(Variables.radiusSmall), image: vm.coverPath != null @@ -312,11 +293,9 @@ class _ProjectSelectorState extends State<ProjectSelector> { ), child: vm.coverPath == null - ? Icon( + ? const Icon( Icons.image, - color: theme.colorScheme.onSurface.withValues( - alpha: 0.3, - ), + color: Variables.textDisabled, size: 28, ) : null, @@ -333,11 +312,7 @@ class _ProjectSelectorState extends State<ProjectSelector> { padding: const EdgeInsets.only(bottom: 2), child: Text( vm.parentTitle!, - style: Variables.captionStyle.copyWith( - color: theme.colorScheme.onSurface.withValues( - alpha: 0.6, - ), - ), + style: Variables.captionStyle, maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -346,7 +321,6 @@ class _ProjectSelectorState extends State<ProjectSelector> { vm.title, style: Variables.bodyStyle.copyWith( fontWeight: FontWeight.w600, - color: theme.colorScheme.onSurface, ), maxLines: 1, overflow: TextOverflow.ellipsis, @@ -361,7 +335,7 @@ class _ProjectSelectorState extends State<ProjectSelector> { ); } - Widget _buildProjectGroup(ProjectGroup g, ThemeData theme, bool isDark) { + Widget _buildProjectGroup(ProjectGroup g) { final project = g.project; final hasEvents = g.events.isNotEmpty; @@ -369,11 +343,9 @@ class _ProjectSelectorState extends State<ProjectSelector> { margin: const EdgeInsets.only(bottom: 8), clipBehavior: Clip.antiAlias, decoration: BoxDecoration( - color: theme.scaffoldBackgroundColor, + color: Variables.background, borderRadius: BorderRadius.circular(Variables.radiusMedium), - border: Border.all( - color: isDark ? Variables.borderDark : Variables.borderSubtle, - ), + border: Border.all(color: Variables.borderSubtle), ), child: Column( children: [ @@ -397,7 +369,7 @@ class _ProjectSelectorState extends State<ProjectSelector> { width: 48, height: 48, decoration: BoxDecoration( - color: isDark ? Variables.surfaceDark : Variables.surfaceSubtle, + color: Variables.surfaceSubtle, borderRadius: BorderRadius.circular(Variables.radiusMedium), image: g.coverPath != null @@ -409,12 +381,7 @@ class _ProjectSelectorState extends State<ProjectSelector> { ), child: g.coverPath == null - ? Icon( - Icons.folder, - color: theme.colorScheme.onSurface.withValues( - alpha: 0.4, - ), - ) + ? const Icon(Icons.folder, color: Variables.textDisabled) : null, ), title: Text( @@ -422,7 +389,6 @@ class _ProjectSelectorState extends State<ProjectSelector> { style: Variables.bodyStyle.copyWith( fontWeight: FontWeight.w600, fontSize: 16, - color: theme.colorScheme.onSurface, ), ), trailing: @@ -432,9 +398,7 @@ class _ProjectSelectorState extends State<ProjectSelector> { g.isExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down, - color: theme.colorScheme.onSurface.withValues( - alpha: 0.6, - ), + color: Variables.textSecondary, ), onPressed: () { setState(() => g.isExpanded = !g.isExpanded); @@ -449,10 +413,7 @@ class _ProjectSelectorState extends State<ProjectSelector> { firstChild: const SizedBox.shrink(), secondChild: Container( width: double.infinity, - color: - isDark - ? Colors.black26 - : Variables.surfaceSubtle.withValues(alpha: 0.5), + color: Variables.surfaceSubtle.withOpacity(0.5), child: Column( children: g.events.map((e) { @@ -472,16 +433,11 @@ class _ProjectSelectorState extends State<ProjectSelector> { width: 40, height: 40, decoration: BoxDecoration( - color: theme.cardColor, + color: Variables.background, borderRadius: BorderRadius.circular( Variables.radiusSmall, ), - border: Border.all( - color: - isDark - ? Variables.borderDark - : Variables.borderSubtle, - ), + border: Border.all(color: Variables.borderSubtle), image: e.coverPath != null ? DecorationImage( @@ -492,11 +448,10 @@ class _ProjectSelectorState extends State<ProjectSelector> { ), child: e.coverPath == null - ? Icon( + ? const Icon( Icons.event, size: 20, - color: theme.colorScheme.onSurface - .withValues(alpha: 0.4), + color: Variables.textDisabled, ) : null, ), @@ -505,7 +460,6 @@ class _ProjectSelectorState extends State<ProjectSelector> { style: Variables.bodyStyle.copyWith( fontSize: 15, fontWeight: FontWeight.w500, - color: theme.colorScheme.onSurface, ), ), ); diff --git a/lib/ui/widgets/search_bar.dart b/lib/ui/widgets/search_bar.dart @@ -15,30 +15,26 @@ class CommonSearchBar extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = Theme.of(context); - final isDark = theme.brightness == Brightness.dark; - return TextField( controller: controller, onChanged: onChanged, + style: Variables.bodyStyle, decoration: InputDecoration( hintText: hintText, - hintStyle: TextStyle( - fontSize: 12, - color: theme.colorScheme.onSurface.withValues(alpha: 0.5), - fontFamily: 'GeneralSans', + hintStyle: Variables.bodyStyle.copyWith( + color: Variables.textSecondary.withOpacity(0.5), ), prefixIcon: Icon( Icons.search, size: 18, - color: theme.colorScheme.onSurface.withValues(alpha: 0.5), + color: Variables.textSecondary.withOpacity(0.5), ), prefixIconConstraints: const BoxConstraints( minWidth: 50, minHeight: 18, ), filled: true, - fillColor: isDark ? Variables.surfaceDark : Variables.surfaceSubtle, + fillColor: Variables.surfaceSubtle, border: OutlineInputBorder( borderRadius: BorderRadius.circular(Variables.radiusSmall), borderSide: BorderSide.none, @@ -56,11 +52,6 @@ class CommonSearchBar extends StatelessWidget { vertical: 12, ), ), - style: TextStyle( - fontSize: 12, - fontFamily: 'GeneralSans', - color: theme.colorScheme.onSurface, - ), ); } } diff --git a/lib/ui/widgets/secondary_button.dart b/lib/ui/widgets/secondary_button.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:creekui/ui/styles/variables.dart'; + +class SecondaryButton extends StatelessWidget { + final String text; + final VoidCallback? onPressed; + final bool isLoading; + + const SecondaryButton({ + super.key, + required this.text, + this.onPressed, + this.isLoading = false, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: isLoading ? null : onPressed, + style: ElevatedButton.styleFrom( + backgroundColor: Variables.borderSubtle, + foregroundColor: Variables.textPrimary, + 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: Variables.textPrimary, + strokeWidth: 2, + ), + ) + : Text( + text, + style: Variables.buttonTextStyle.copyWith( + color: Variables.textPrimary, + ), + ), + ), + ); + } +} diff --git a/lib/ui/widgets/selection_overlay_painter.dart b/lib/ui/widgets/selection_overlay_painter.dart @@ -1,18 +1,6 @@ -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, - ); - } -} +import 'package:creekui/data/models/canvas_models.dart'; +import 'package:creekui/ui/styles/variables.dart'; class SelectionOverlayPainter extends CustomPainter { final Rect rect; @@ -21,95 +9,68 @@ class SelectionOverlayPainter extends CustomPainter { SelectionOverlayPainter({ required this.rect, - required this.isResizing, + this.isResizing = false, 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 = + // 1. Draw the Selection Border + final paint = Paint() - ..color = const Color(0xFF448AFF) - ..strokeWidth = 2.0 - ..style = PaintingStyle.stroke; + ..color = Variables.selectionBorder + ..style = PaintingStyle.stroke + ..strokeWidth = 2.0; - double dashWidth = 6; - double dashSpace = 4; - Path borderPath = Path()..addRect(rect); + // Use a dash effect for the selection box + _drawDashedRect(canvas, rect, paint); - 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); - } - } + // 2. If Resizing, Draw Handles + if (isResizing) { + final handlePaint = + Paint() + ..color = Colors.white + ..style = PaintingStyle.fill; - // 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 = [ + final handleBorderPaint = + Paint() + ..color = Variables.selectionBorder + ..style = PaintingStyle.stroke + ..strokeWidth = 2.0; + + final double handleRadius = 6.0; + + final handles = [ 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; + for (final handle in handles) { + canvas.drawCircle(handle, handleRadius, handlePaint); + canvas.drawCircle(handle, handleRadius, handleBorderPaint); + } - const double handleRadius = 8; + // 3. Draw Center Handle (Move) + final centerPaint = + Paint() + ..color = Variables.accentMagic.withOpacity(0.5) + ..style = PaintingStyle.fill; - for (final corner in corners) { - canvas.drawCircle(corner, handleRadius, handleShadow); - canvas.drawCircle(corner, handleRadius, handleFill); - canvas.drawCircle(corner, handleRadius, handleBorder); - } + canvas.drawCircle(rect.center, 8.0, centerPaint); } } + void _drawDashedRect(Canvas canvas, Rect rect, Paint paint) { + final path = Path()..addRect(rect); + canvas.drawPath(path, paint); + } + @override - bool shouldRepaint(covariant SelectionOverlayPainter oldDelegate) => - rect != oldDelegate.rect || - isResizing != oldDelegate.isResizing || - activeHandle != oldDelegate.activeHandle; + bool shouldRepaint(covariant SelectionOverlayPainter oldDelegate) { + return oldDelegate.rect != rect || + oldDelegate.isResizing != isResizing || + oldDelegate.activeHandle != activeHandle; + } } diff --git a/lib/ui/widgets/tag_chip.dart b/lib/ui/widgets/tag_chip.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:creekui/ui/styles/variables.dart'; + +class TagChip extends StatelessWidget { + final String label; + final VoidCallback onDelete; + final Widget? icon; + + const TagChip({ + super.key, + required this.label, + required this.onDelete, + this.icon, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: Variables.chipBackground, + borderRadius: BorderRadius.circular(48), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (icon != null) ...[icon!, const SizedBox(width: 8)], + Text( + label, + style: Variables.captionStyle.copyWith( + fontSize: 12, + color: Variables.textPrimary, + ), + ), + const SizedBox(width: 8), + GestureDetector( + onTap: onDelete, + child: const Icon( + Icons.close, + size: 16, + color: Variables.textPrimary, + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui/widgets/text_field.dart b/lib/ui/widgets/text_field.dart @@ -2,21 +2,27 @@ import 'package:flutter/material.dart'; import 'package:creekui/ui/styles/variables.dart'; class CommonTextField extends StatelessWidget { - final String label; + final String? label; final String hintText; final TextEditingController controller; final int maxLines; final bool isRequired; final ValueChanged<String>? onSubmitted; + final ValueChanged<String>? onChanged; + final Widget? suffixIcon; + final bool autoFocus; const CommonTextField({ super.key, - required this.label, + this.label, required this.hintText, required this.controller, this.maxLines = 1, this.isRequired = false, this.onSubmitted, + this.onChanged, + this.suffixIcon, + this.autoFocus = false, }); @override @@ -24,23 +30,27 @@ class CommonTextField extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - Text( - label, - style: Variables.bodyStyle.copyWith(fontWeight: FontWeight.w500), - ), - if (isRequired) + if (label != null) ...[ + Row( + children: [ Text( - '*', + label!, style: Variables.bodyStyle.copyWith( - color: const Color(0xFF4F39F6), - fontSize: 12, + fontWeight: FontWeight.w500, ), ), - ], - ), - const SizedBox(height: 6), + if (isRequired) + Text( + '*', + style: Variables.bodyStyle.copyWith( + color: const Color(0xFF4F39F6), + fontSize: 12, + ), + ), + ], + ), + const SizedBox(height: 6), + ], Container( decoration: BoxDecoration( color: Variables.borderSubtle, @@ -49,7 +59,9 @@ class CommonTextField extends StatelessWidget { child: TextField( controller: controller, maxLines: maxLines, + autofocus: autoFocus, onSubmitted: onSubmitted, + onChanged: onChanged, style: Variables.bodyStyle, decoration: InputDecoration( hintText: hintText, @@ -61,6 +73,7 @@ class CommonTextField extends StatelessWidget { horizontal: 16, vertical: 12, ), + suffixIcon: suffixIcon, ), ), ), diff --git a/lib/ui/widgets/top_bar.dart b/lib/ui/widgets/top_bar.dart @@ -170,12 +170,9 @@ class _TopBarState extends State<TopBar> { widget.titleOverride != null ? Text( widget.titleOverride!, - style: const TextStyle( - fontFamily: 'GeneralSans', + style: Variables.headerStyle.copyWith( fontSize: 20, - fontWeight: FontWeight.w500, height: 24 / 20, - color: Variables.textPrimary, ), overflow: TextOverflow.ellipsis, ) @@ -184,12 +181,9 @@ class _TopBarState extends State<TopBar> { _rootProject != null) ? Text( _rootProject!.title, - style: const TextStyle( - fontFamily: 'GeneralSans', + style: Variables.headerStyle.copyWith( fontSize: 20, - fontWeight: FontWeight.w500, height: 24 / 20, - color: Variables.textPrimary, ), overflow: TextOverflow.ellipsis, ) @@ -283,7 +277,6 @@ class _TopBarState extends State<TopBar> { child: Text( isRoot ? "Global" : project.title, style: Variables.bodyStyle.copyWith( - fontFamily: 'GeneralSans', fontWeight: isSelected ? FontWeight.bold diff --git a/lib/utils/image_actions_helper.dart b/lib/utils/image_actions_helper.dart @@ -4,7 +4,8 @@ import 'package:share_plus/share_plus.dart'; import 'package:creekui/data/models/image_model.dart'; import 'package:creekui/services/image_service.dart'; import 'package:creekui/ui/pages/share_to_file_page.dart'; -import 'package:creekui/ui/styles/variables.dart'; +import 'package:creekui/ui/widgets/dialog.dart'; +import 'package:creekui/ui/widgets/text_field.dart'; class ImageActionsHelper { static Future<void> shareImage(BuildContext context, String filePath) async { @@ -31,67 +32,26 @@ class ImageActionsHelper { final TextEditingController nameController = TextEditingController( text: image.name, ); - await showDialog( - context: context, - builder: - (ctx) => AlertDialog( - backgroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - title: const Text( - "Rename Image", - style: TextStyle( - fontFamily: 'GeneralSans', - fontWeight: FontWeight.w600, - ), - ), - content: TextField( - controller: nameController, - autofocus: true, - decoration: InputDecoration( - hintText: "Enter new name", - filled: true, - fillColor: Variables.surfaceSubtle, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide.none, - ), - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(ctx), - child: const Text( - "Cancel", - style: TextStyle( - color: Variables.textSecondary, - fontFamily: 'GeneralSans', - ), - ), - ), - TextButton( - onPressed: () async { - if (nameController.text.isNotEmpty) { - await ImageService().renameImage( - image.id, - nameController.text.trim(), - ); - onSuccess(); - if (ctx.mounted) Navigator.pop(ctx); - } - }, - child: const Text( - "Save", - style: TextStyle( - color: Variables.textPrimary, - fontFamily: 'GeneralSans', - fontWeight: FontWeight.w600, - ), - ), - ), - ], - ), + + await ShowDialog.show( + context, + title: "Rename Image", + primaryButtonText: "Save", + content: CommonTextField( + hintText: "Enter new name", + controller: nameController, + autoFocus: true, + ), + onPrimaryPressed: () async { + if (nameController.text.isNotEmpty) { + await ImageService().renameImage( + image.id, + nameController.text.trim(), + ); + onSuccess(); + if (context.mounted) Navigator.pop(context); + } + }, ); } @@ -100,57 +60,17 @@ class ImageActionsHelper { ImageModel image, VoidCallback onSuccess, ) async { - final confirm = await showDialog<bool>( - context: context, - builder: - (ctx) => AlertDialog( - backgroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - title: const Text( - "Delete Image?", - style: TextStyle( - fontFamily: 'GeneralSans', - fontWeight: FontWeight.w600, - ), - ), - content: const Text( - "This action cannot be undone.", - style: TextStyle( - fontFamily: 'GeneralSans', - color: Variables.textSecondary, - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(ctx, false), - child: const Text( - "Cancel", - style: TextStyle( - color: Variables.textSecondary, - fontFamily: 'GeneralSans', - ), - ), - ), - TextButton( - onPressed: () => Navigator.pop(ctx, true), - child: const Text( - "Delete", - style: TextStyle( - color: Colors.red, - fontFamily: 'GeneralSans', - fontWeight: FontWeight.w600, - ), - ), - ), - ], - ), + await ShowDialog.show( + context, + title: "Delete Image?", + description: "This action cannot be undone.", + primaryButtonText: "Delete", + isDestructive: true, + onPrimaryPressed: () async { + await ImageService().deleteImage(image.id); + onSuccess(); + if (context.mounted) Navigator.pop(context); + }, ); - - if (confirm == true) { - await ImageService().deleteImage(image.id); - onSuccess(); - } } } diff --git a/lib/utils/image_utils.dart b/lib/utils/image_utils.dart @@ -1,4 +1,17 @@ import 'dart:math'; +import 'dart:ui'; + +// Extension to normalize Rect +extension RectNormalize on Rect { + Rect normalize() { + return Rect.fromLTRB( + left < right ? left : right, + top < bottom ? top : bottom, + left < right ? right : left, + top < bottom ? bottom : top, + ); + } +} List<double> l2Normalize(List<double> vec) { double sum = vec.fold(0, (p, c) => p + c * c);