creek

The AI Image Editor of 2030
commit bdd266f5a84762ed3d6d804e73604f530ea67046
parent 90ffcf2dc055b6ca5c9c3da4cd9676fb88e8b704
Author: maydayv7 <maydayv7@gmail.com>
Date:   Sat, 29 Nov 2025 13:20:24 +0530

Add top and bottom bar widgets

Also start work on stylesheet page, and delete obsolete pages

Diffstat:
Aassets/icons/files_icon.svg | 3+++
Aassets/icons/generate_icon.svg | 3+++
Aassets/icons/moodboard_icon.svg | 3+++
Aassets/icons/stylesheet_icon.svg | 3+++
Dlib/ui/pages/all_image_page.dart | 128-------------------------------------------------------------------------------
Dlib/ui/pages/board_detail_page.dart | 196-------------------------------------------------------------------------------
Dlib/ui/pages/board_list_page.dart | 468-------------------------------------------------------------------------------
Dlib/ui/pages/board_page.dart | 191-------------------------------------------------------------------------------
Dlib/ui/pages/moodboards_page.dart | 327-------------------------------------------------------------------------------
Mlib/ui/pages/project_detail_page.dart | 16+++++++++++++---
Alib/ui/pages/stylesheet_page.dart | 104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/ui/styles/variables.dart | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dlib/ui/widgets/.gitkeep | 0
Alib/ui/widgets/bottom_bar.dart | 131+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/ui/widgets/top_bar.dart | 188+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpubspec.yaml | 2++
16 files changed, 508 insertions(+), 1313 deletions(-)

diff --git a/assets/icons/files_icon.svg b/assets/icons/files_icon.svg @@ -0,0 +1,3 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M9.00002 2.003V2H19.998C20.55 2 21 2.455 21 2.992V21.008C20.9998 21.2712 20.895 21.5235 20.7088 21.7095C20.5226 21.8955 20.2702 22 20.007 22H3.99302C3.8617 21.9991 3.73185 21.9723 3.61087 21.9212C3.48989 21.8701 3.38017 21.7957 3.28796 21.7022C3.19575 21.6087 3.12286 21.4979 3.07346 21.3762C3.02406 21.2545 2.9991 21.1243 3.00002 20.993V8L9.00002 2.003ZM5.83002 8H9.00002V4.83L5.83002 8ZM11 4V9C11 9.26522 10.8947 9.51957 10.7071 9.70711C10.5196 9.89464 10.2652 10 10 10H5.00002V20H19V4H11Z" fill="#9F9FA9"/> +</svg> diff --git a/assets/icons/generate_icon.svg b/assets/icons/generate_icon.svg @@ -0,0 +1,3 @@ +<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M7.82391 13.7376L8.5426 12.0914C9.18227 10.6265 10.3335 9.46033 11.7696 8.82287L13.7479 7.94474C14.3768 7.66557 14.3768 6.75056 13.7479 6.47138L11.8313 5.62065C10.3583 4.96681 9.18644 3.75749 8.55775 2.24258L7.82973 0.488263C7.55956 -0.162755 6.66005 -0.162754 6.38989 0.488263L5.66184 2.24256C5.03315 3.75749 3.86123 4.96681 2.38824 5.62065L0.471729 6.47138C-0.157243 6.75056 -0.157243 7.66557 0.471729 7.94474L2.45003 8.82287C3.88609 9.46033 5.03736 10.6265 5.67697 12.0914L6.3957 13.7376C6.67198 14.3703 7.54761 14.3703 7.82391 13.7376ZM15.0198 17.7457L15.2219 17.2824C15.5823 16.4564 16.2313 15.7987 17.041 15.439L17.6637 15.1623C18.0006 15.0127 18.0006 14.5232 17.6637 14.3736L17.0759 14.1123C16.2453 13.7433 15.5846 13.0613 15.2305 12.2073L15.0229 11.7066C14.8782 11.3578 14.3959 11.3578 14.2512 11.7066L14.0437 12.2073C13.6896 13.0613 13.0289 13.7433 12.1983 14.1123L11.6104 14.3736C11.2736 14.5232 11.2736 15.0127 11.6104 15.1623L12.2331 15.439C13.0429 15.7987 13.6919 16.4564 14.0522 17.2824L14.2544 17.7457C14.4023 18.0848 14.8718 18.0848 15.0198 17.7457Z" fill="#FAFAFA"/> +</svg> diff --git a/assets/icons/moodboard_icon.svg b/assets/icons/moodboard_icon.svg @@ -0,0 +1,3 @@ +<svg width="20" height="18" viewBox="0 0 20 18" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M20 6.999V17C20 17.2652 19.8946 17.5196 19.7071 17.7071C19.5196 17.8946 19.2652 18 19 18H11V6.999H20ZM9 12.999V18H1C0.734784 18 0.48043 17.8946 0.292893 17.7071C0.105357 17.5196 0 17.2652 0 17V12.999H9ZM9 0V10.999H0V1C0 0.734784 0.105357 0.48043 0.292893 0.292893C0.48043 0.105357 0.734784 0 1 0H9ZM19 0C19.2652 0 19.5196 0.105357 19.7071 0.292893C19.8946 0.48043 20 0.734784 20 1V4.999H11V0H19Z" fill="#27272A"/> +</svg> diff --git a/assets/icons/stylesheet_icon.svg b/assets/icons/stylesheet_icon.svg @@ -0,0 +1,3 @@ +<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M1.02512 2.5254C2.39197 1.15872 4.60756 1.15861 5.97434 2.5254L6.14622 2.6963C6.34149 2.89145 6.65802 2.89152 6.85325 2.6963L7.02512 2.5254C8.30771 1.24294 10.3371 1.16412 11.7126 2.28809L10.447 3.83594C9.85871 3.35525 8.98831 3.39054 8.43921 3.93946L8.26731 4.11133C7.29111 5.08721 5.7084 5.08713 4.73216 4.11133L4.56028 3.93946C3.97455 3.35373 3.02499 3.35383 2.43919 3.93946C1.8534 4.52525 1.8534 5.47477 2.43919 6.06055L2.61106 6.23243C3.58681 7.20862 3.58678 8.79137 2.61106 9.7676L2.43919 9.9395C1.8534 10.5252 1.8534 11.4748 2.43919 12.0606L2.61106 12.2324C3.58681 13.2086 3.58678 14.7914 2.61106 15.7676L2.43919 15.9395C1.8534 16.5252 1.8534 17.4748 2.43919 18.0606C3.02498 18.6462 3.97453 18.6463 4.56028 18.0606L4.73216 17.8887C5.7084 16.9129 7.29113 16.9128 8.26731 17.8887L8.43921 18.0606C9.02501 18.6462 9.97451 18.6463 10.5603 18.0606L10.7321 17.8887C11.7084 16.9129 13.2911 16.9128 14.2673 17.8887L14.4392 18.0606C15.025 18.6462 15.9745 18.6463 16.5603 18.0606C17.1459 17.4748 17.1459 16.5252 16.5603 15.9395L16.3884 15.7676C15.4125 14.7914 15.4125 13.2087 16.3884 12.2324L16.5603 12.0606C17.1092 11.5115 17.1444 10.641 16.6638 10.0527L18.2116 8.78712C19.3355 10.1626 19.2568 12.192 17.9743 13.4746L17.8034 13.6465C17.6082 13.8418 17.6082 14.1583 17.8034 14.3535L17.9743 14.5254C19.341 15.8922 19.341 18.1078 17.9743 19.4746C16.6075 20.8414 14.392 20.8413 13.0251 19.4746L12.8532 19.3037C12.658 19.1085 12.3415 19.1086 12.1462 19.3037L11.9743 19.4746C10.6075 20.8414 8.39201 20.8413 7.02512 19.4746L6.85325 19.3037C6.65804 19.1085 6.34149 19.1086 6.14622 19.3037L5.97434 19.4746C4.60754 20.8414 2.39197 20.8413 1.02512 19.4746C-0.341707 18.1078 -0.341707 15.8922 1.02512 14.5254L1.19602 14.3535C1.39109 14.1583 1.39112 13.8417 1.19602 13.6465L1.02512 13.4746C-0.341707 12.1078 -0.341707 9.8922 1.02512 8.5254L1.19602 8.35352C1.39109 8.15828 1.39112 7.84171 1.19602 7.64649L1.02512 7.47462C-0.341707 6.10778 -0.341707 3.89223 1.02512 2.5254ZM11.9997 6.50001C13.1042 6.50015 13.9997 7.39555 13.9997 8.50001V13.5C13.9997 14.6045 13.1042 15.4999 11.9997 15.5H6.99973C5.89516 15.5 4.99973 14.6046 4.99973 13.5V8.50001C4.99976 7.39546 5.89518 6.50001 6.99973 6.50001H11.9997ZM6.99973 13.5H11.9997V8.50001H6.99973V13.5ZM16.029 0.319341C16.2057 -0.106379 16.794 -0.106515 16.9704 0.319341L17.2234 0.930671C17.6553 1.97354 18.4612 2.8062 19.4743 3.25684L20.1911 3.57618C20.6019 3.75887 20.6019 4.35638 20.1911 4.53907L19.4314 4.87696C18.4437 5.3163 17.6521 6.11944 17.2126 7.12794L16.9665 7.69337C16.7861 8.1075 16.2134 8.1075 16.0329 7.69337L15.7859 7.12794C15.3463 6.11943 14.5548 5.31625 13.5671 4.87696L12.8073 4.53907C12.3969 4.35629 12.3968 3.7589 12.8073 3.57618L13.5251 3.25684C14.5381 2.80616 15.3433 1.9734 15.7751 0.930671L16.029 0.319341Z" fill="#27272A"/> +</svg> diff --git a/lib/ui/pages/all_image_page.dart b/lib/ui/pages/all_image_page.dart @@ -1,128 +0,0 @@ -// BROKEN - -import 'dart:io'; -import 'package:flutter/material.dart'; -import 'package:adobe/data/repos/image_repo.dart'; -import 'package:adobe/services/image_service.dart'; -import 'package:adobe/services/theme_service.dart'; -import 'package:adobe/data/models/image_model.dart'; // Import Model -import '../widgets/analysis_dialog.dart'; // Import Dialog - -class AllImagesPage extends StatefulWidget { - const AllImagesPage({super.key}); - - @override - State<AllImagesPage> createState() => _AllImagesPageState(); -} - -class _AllImagesPageState extends State<AllImagesPage> { - final _imageRepo = ImageRepository(); - final _imageService = ImageService(); - late Future<List<Map<String, dynamic>>> _imagesFuture; - - @override - void initState() { - super.initState(); - _refreshImages(); - } - - void _refreshImages() { - setState(() { - _imagesFuture = _imageRepo.getAllImages(); - }); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text("All Images", style: TextStyle(fontWeight: FontWeight.bold)), - Text("Tap for analysis • Long press for options", style: TextStyle(fontSize: 12)), - ], - ), - centerTitle: true, - leading: IconButton( - icon: Icon(themeService.mode == ThemeMode.dark ? Icons.light_mode : Icons.dark_mode), - onPressed: () { themeService.toggleTheme(); }, - ), - ), - body: FutureBuilder<List<Map<String, dynamic>>>( - future: _imagesFuture, - builder: (context, snapshot) { - if (!snapshot.hasData) return const Center(child: CircularProgressIndicator()); - - final imagesData = snapshot.data!; - if (imagesData.isEmpty) return const Center(child: Text("No images saved yet.")); - - return GridView.builder( - padding: const EdgeInsets.all(8), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, crossAxisSpacing: 8, mainAxisSpacing: 8, - ), - itemCount: imagesData.length, - itemBuilder: (context, index) { - // Convert raw Map to ImageModel for the dialog - final imgModel = ImageModel.fromMap(imagesData[index]); - final file = File(imgModel.filePath); - - return GestureDetector( - // ADDED: Tap to view analysis - onTap: () { - showDialog( - context: context, - builder: (c) => AnalysisDialog(image: imgModel), - ); - }, - onLongPress: () => _confirmDeleteForever(imgModel.id), - child: Stack( - fit: StackFit.expand, - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(12), - child: Image.file(file, fit: BoxFit.cover, errorBuilder: (_,__,___) => const Icon(Icons.broken_image)), - ), - // ADDED: Icon indicator if analysis exists - if (imgModel.analysisData != null) - Positioned( - top: 8, right: 8, - child: Container( - padding: const EdgeInsets.all(4), - decoration: const BoxDecoration(color: Colors.black54, shape: BoxShape.circle), - child: const Icon(Icons.analytics, size: 14, color: Colors.white), - ), - ) - ], - ), - ); - }, - ); - }, - ), - ); - } - - void _confirmDeleteForever(String imageId) { - showDialog( - context: context, - builder: (c) => AlertDialog( - title: const Text("Delete Permanently?"), - content: const Text("This will remove the image from your device and ALL boards."), - actions: [ - TextButton(onPressed: () => Navigator.pop(c), child: const Text("Cancel")), - ElevatedButton( - style: ElevatedButton.styleFrom(backgroundColor: Colors.red), - onPressed: () async { - await _imageService.deleteImagePermanently(imageId); - if (c.mounted) Navigator.pop(c); - _refreshImages(); - }, - child: const Text("Delete"), - ), - ], - ), - ); - } -} diff --git a/lib/ui/pages/board_detail_page.dart b/lib/ui/pages/board_detail_page.dart @@ -1,196 +0,0 @@ -// BROKEN - -import 'dart:io'; -import 'dart:convert'; -import 'package:flutter/material.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:uuid/uuid.dart'; -import 'package:adobe/data/models/board_model.dart'; -import 'package:adobe/data/models/image_model.dart'; -import 'package:adobe/data/repos/board_image_repo.dart'; -import 'package:adobe/data/repos/image_repo.dart'; -import 'package:adobe/services/image_service.dart'; -import 'package:adobe/services/theme_service.dart'; -import 'package:adobe/services/image_analyzer_service.dart'; // Master Service -import '../widgets/analysis_dialog.dart'; // Dialog Widget - -class BoardDetailPage extends StatefulWidget { - final Board board; - const BoardDetailPage({super.key, required this.board}); - - @override - State<BoardDetailPage> createState() => _BoardDetailPageState(); -} - -class _BoardDetailPageState extends State<BoardDetailPage> { - final _boardImageRepo = BoardImageRepository(); - final _imageRepo = ImageRepository(); - final _imageService = ImageService(); - final _imagePicker = ImagePicker(); - final _uuid = const Uuid(); - - late Future<List<ImageModel>> _imagesFuture; - - @override - void initState() { - super.initState(); - _imagesFuture = _fetchImages(); - } - - Future<List<ImageModel>> _fetchImages() async { - final rawData = await _boardImageRepo.getImagesOfBoard(widget.board.id); - return rawData.map((e) => ImageModel.fromMap(e)).toList(); - } - - Future<void> _pickImageFromGallery() async { - try { - final XFile? image = await _imagePicker.pickImage(source: ImageSource.gallery); - if (image == null) return; - - final dir = await getApplicationDocumentsDirectory(); - final imagesDir = Directory('${dir.path}/images'); - if (!await imagesDir.exists()) await imagesDir.create(recursive: true); - - final extension = image.path.split('.').last; - final imageId = _uuid.v4(); - final targetPath = '${imagesDir.path}/$imageId.$extension'; - - await File(image.path).copy(targetPath); - await _imageRepo.insertImage(imageId, targetPath); - await _boardImageRepo.saveToBoard(widget.board.id, imageId); - - // Trigger analysis - _analyzeImage(imageId, targetPath); - - if (mounted) setState(() { _imagesFuture = _fetchImages(); }); - } catch (e) { - if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error: $e'))); - } - } - - Future<void> _analyzeImage(String imageId, String imagePath) async { - try { - // Changed to use Master Runner - final result = await ImageAnalyzerService.analyzeFullSuite(imagePath); - if (result['success'] == true) { - await _imageRepo.updateImageAnalysis(imageId, json.encode(result)); - if (mounted) setState(() { _imagesFuture = _fetchImages(); }); - } - } catch (e) { - debugPrint('Analysis Error: $e'); - } - } - - void _showAnalysisDialog(ImageModel image) { - showDialog( - context: context, - builder: (context) => AnalysisDialog(image: image), - ); - } - - void _showImageOptionsDialog(ImageModel img) { - showModalBottomSheet( - context: context, - builder: (context) => Container( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - leading: const Icon(Icons.remove_circle_outline, color: Colors.orange), - title: const Text("Remove from this Board"), - onTap: () async { - await _imageService.removeImageFromSpecificBoard(img.id, widget.board.id); - if (context.mounted) Navigator.pop(context); - setState(() { _imagesFuture = _fetchImages(); }); - }, - ), - ListTile( - leading: const Icon(Icons.delete_forever, color: Colors.red), - title: const Text("Delete Completely"), - onTap: () { Navigator.pop(context); _confirmDeleteForever(img.id); }, - ), - ], - ), - ), - ); - } - - void _confirmDeleteForever(String imageId) { - showDialog( - context: context, - builder: (c) => AlertDialog( - title: const Text("Delete Permanently?"), - actions: [ - TextButton(onPressed: () => Navigator.pop(c), child: const Text("Cancel")), - ElevatedButton( - style: ElevatedButton.styleFrom(backgroundColor: Colors.red), - onPressed: () async { - await _imageService.deleteImagePermanently(imageId); - if (c.mounted) Navigator.pop(c); - setState(() { _imagesFuture = _fetchImages(); }); - }, - child: const Text("Delete"), - ), - ], - ), - ); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(widget.board.name), - leading: IconButton( - icon: Icon(themeService.mode == ThemeMode.dark ? Icons.light_mode : Icons.dark_mode), - onPressed: () { themeService.toggleTheme(); }, - ), - actions: [ - IconButton(icon: const Icon(Icons.photo_library), onPressed: _pickImageFromGallery), - ], - ), - body: FutureBuilder<List<ImageModel>>( - future: _imagesFuture, - builder: (context, snapshot) { - if (!snapshot.hasData) return const Center(child: CircularProgressIndicator()); - final images = snapshot.data!; - if (images.isEmpty) return const Center(child: Text("No images.")); - - return GridView.builder( - padding: const EdgeInsets.all(8), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, crossAxisSpacing: 8, mainAxisSpacing: 8, childAspectRatio: 0.8, - ), - itemCount: images.length, - itemBuilder: (context, index) { - final img = images[index]; - final file = File(img.filePath); - - return GestureDetector( - onTap: () => _showAnalysisDialog(img), - onLongPress: () => _showImageOptionsDialog(img), - child: Stack( - fit: StackFit.expand, - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(12), - child: Image.file(file, fit: BoxFit.cover, errorBuilder: (_,__,___) => const Icon(Icons.error)), - ), - if (img.analysisData != null) - Positioned(top: 8, right: 8, child: Container( - padding: const EdgeInsets.all(4), - decoration: const BoxDecoration(color: Colors.black54, shape: BoxShape.circle), - child: const Icon(Icons.analytics, color: Colors.white, size: 16), - )), - ], - ), - ); - }, - ); - }, - ), - ); - } -} diff --git a/lib/ui/pages/board_list_page.dart b/lib/ui/pages/board_list_page.dart @@ -1,468 +0,0 @@ -// BROKEN - -import 'dart:io'; -import 'package:flutter/material.dart'; -import '../../data/repos/board_repo.dart'; -import '../../data/repos/board_category_repo.dart'; -import '../../data/repos/board_image_repo.dart'; -import '../../data/models/board_model.dart'; -import '../../data/models/category_model.dart'; -import 'board_detail_page.dart'; -import 'image_analysis_page.dart'; - -class BoardListPage extends StatefulWidget { - const BoardListPage({super.key}); - - @override - State<BoardListPage> createState() => _BoardListPageState(); -} - -class _BoardListPageState extends State<BoardListPage> { - final _categoryRepo = BoardCategoryRepository(); - final _boardRepo = BoardRepository(); - final _boardImageRepo = BoardImageRepository(); - - Map<BoardCategory, List<Board>> _categorizedBoards = {}; - Map<int, List<String>> _boardPreviews = {}; - bool _isLoading = true; - - @override - void initState() { - super.initState(); - _loadData(); - } - - Future<void> _loadData() async { - setState(() => _isLoading = true); - try { - final categoriesRaw = await _categoryRepo.getAllCategories(); - final categories = - categoriesRaw.map((e) => BoardCategory.fromMap(e)).toList(); - - final Map<BoardCategory, List<Board>> newCategorizedBoards = {}; - final Map<int, List<String>> newPreviews = {}; - - for (final category in categories) { - final boardsRaw = await _categoryRepo.getBoardsByCategory(category.id); - final boards = boardsRaw.map((e) => Board.fromMap(e)).toList(); - newCategorizedBoards[category] = boards; - - for (final board in boards) { - final imagesRaw = await _boardImageRepo.getImagesOfBoard(board.id); - final paths = - imagesRaw.map((e) => e['filePath'] as String).take(5).toList(); - newPreviews[board.id] = paths; - } - } - - if (mounted) { - setState(() { - _categorizedBoards = newCategorizedBoards; - _boardPreviews = newPreviews; - _isLoading = false; - }); - } - } catch (e) { - debugPrint('Error loading boards: $e'); - if (mounted) setState(() => _isLoading = false); - } - } - - Future<void> _createCategoryDialog() async { - final nameController = TextEditingController(); - if (!mounted) return; - await showDialog( - context: context, - builder: - (c) => AlertDialog( - title: const Text("Create Category"), - content: TextField( - controller: nameController, - decoration: const InputDecoration(hintText: "Category Name"), - autofocus: true, - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(c), - child: const Text("Cancel"), - ), - ElevatedButton( - onPressed: () async { - if (nameController.text.isNotEmpty) { - await _categoryRepo.createCategory(nameController.text); - if (c.mounted) Navigator.pop(c); - _loadData(); - } - }, - child: const Text("Create"), - ), - ], - ), - ); - } - - Future<void> _createBoardDialog({BoardCategory? preSelectedCategory}) async { - final nameController = TextEditingController(); - final categories = _categorizedBoards.keys.toList(); - BoardCategory? selectedCategory = - preSelectedCategory ?? - (categories.isNotEmpty ? categories.first : null); - if (!mounted) return; - await showDialog( - context: context, - builder: - (c) => StatefulBuilder( - builder: - (context, setState) => AlertDialog( - title: const Text("Create New Board"), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextField( - controller: nameController, - decoration: const InputDecoration( - hintText: "Board Name", - ), - autofocus: true, - ), - const SizedBox(height: 16), - if (categories.isNotEmpty) - DropdownButton<BoardCategory>( - value: selectedCategory, - isExpanded: true, - hint: const Text("Select Category"), - items: - categories - .map( - (cat) => DropdownMenuItem( - value: cat, - child: Text(cat.name), - ), - ) - .toList(), - onChanged: - (val) => setState(() => selectedCategory = val), - ), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(c), - child: const Text("Cancel"), - ), - ElevatedButton( - onPressed: () async { - if (nameController.text.isNotEmpty && - selectedCategory != null) { - await _boardRepo.createBoard( - nameController.text, - categoryId: selectedCategory!.id, - ); - if (c.mounted) Navigator.pop(c); - _loadData(); - } - }, - child: const Text("Create"), - ), - ], - ), - ), - ); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.white, - appBar: AppBar( - backgroundColor: Colors.white, - elevation: 0, - title: const Text( - "Cookie point", - style: TextStyle( - color: Colors.black, - fontWeight: FontWeight.w600, - fontSize: 24, - fontFamily: 'GeneralSans', - ), - ), - actions: [ - IconButton( - icon: const Icon(Icons.hexagon_outlined, color: Colors.black), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute(builder: (_) => const ImageAnalysisPage()), - ); - }, - ), - ], - ), - body: - _isLoading - ? const Center(child: CircularProgressIndicator()) - : Column( - children: [ - // Header Controls - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 12.0, - ), - child: Row( - children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - decoration: BoxDecoration( - color: Colors.grey[200], - borderRadius: BorderRadius.circular(8), - ), - child: Row( - children: const [ - Text( - "Global", - style: TextStyle( - fontWeight: FontWeight.w500, - fontFamily: 'GeneralSans', - color: Colors.black87, - ), - ), - SizedBox(width: 8), - Icon( - Icons.keyboard_arrow_down, - size: 20, - color: Colors.black87, - ), - ], - ), - ), - const Spacer(), - const Icon( - Icons.splitscreen_outlined, - size: 24, - color: Colors.black87, - ), - const SizedBox(width: 12), - const Icon( - Icons.grid_view, - size: 24, - color: Colors.black87, - ), - ], - ), - ), - - // Main Content - Expanded( - child: - _categorizedBoards.isEmpty - ? Center( - child: ElevatedButton( - onPressed: _createCategoryDialog, - child: const Text("Create Your First Category"), - ), - ) - : ListView.builder( - padding: const EdgeInsets.only(bottom: 40), - itemCount: _categorizedBoards.length, - itemBuilder: (context, index) { - final category = _categorizedBoards.keys - .elementAt(index); - final boards = _categorizedBoards[category]!; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Category Header - Padding( - padding: const EdgeInsets.fromLTRB( - 20, - 0, - 20, - 8, - ), - child: Row( - children: [ - Expanded( - child: Text( - category.name, - style: const TextStyle( - fontSize: 22, - fontWeight: FontWeight.w500, - color: Color.fromARGB( - 255, - 38, - 56, - 255, - ), - fontFamily: 'GeneralSans', - ), - ), - ), - ], - ), - ), - - if (boards.isEmpty) - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, - ), - child: GestureDetector( - onTap: - () => _createBoardDialog( - preSelectedCategory: category, - ), - child: const Text( - "+ Add Board", - style: TextStyle( - color: Colors.blue, - fontWeight: FontWeight.w500, - ), - ), - ), - ) - else - ...boards.map( - (board) => _buildBoardPreviewCard( - board, - category.name, - ), - ), - - const SizedBox(height: 12), - ], - ); - }, - ), - ), - ], - ), - floatingActionButton: FloatingActionButton( - onPressed: _createCategoryDialog, - child: const Icon(Icons.add), - ), - ); - } - - Widget _buildBoardPreviewCard(Board board, String categoryName) { - final previewImages = _boardPreviews[board.id] ?? []; - - return GestureDetector( - onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (_) => BoardDetailPage(board: board)), - ).then((_) => _loadData()); - }, - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), - // Clip behavior for the MAIN card corners - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(24), - border: Border.all(color: Colors.grey.shade200), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header inside (with padding) - Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - children: [ - Text( - board.name, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - fontFamily: 'GeneralSans', - ), - ), - const Spacer(), - GestureDetector( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => BoardDetailPage(board: board), - ), - ).then((_) => _loadData()); - }, - child: const Icon(Icons.add, color: Colors.black, size: 24), - ), - const SizedBox(width: 8), - const Icon( - Icons.chevron_right, - color: Colors.black, - size: 24, - ), - ], - ), - ), - - // Images List - SizedBox( - height: 140, - child: ListView.separated( - padding: EdgeInsets.zero, - scrollDirection: Axis.horizontal, - itemCount: previewImages.isEmpty ? 1 : previewImages.length, - // FIXED: Changed VerticalDivider to a Container to ensure it renders visible - separatorBuilder: - (_, __) => Container( - width: 1, - color: const Color.fromARGB( - 255, - 169, - 169, - 169, - ), // Visible grey divider - ), - itemBuilder: (context, index) { - if (previewImages.isEmpty) { - return Container( - width: 100, - color: Colors.grey[50], - child: const Center( - child: Text( - "Empty", - style: TextStyle(color: Colors.grey), - ), - ), - ); - } - - final imagePath = previewImages[index]; - return SizedBox( - width: 110, - // ADDED: ClipRRect for "very small image radius" - child: ClipRRect( - borderRadius: BorderRadius.circular(8.0), // Small radius - child: Image.file( - File(imagePath), - fit: BoxFit.cover, - errorBuilder: - (c, e, s) => Container( - color: Colors.grey[100], - child: const Icon( - Icons.broken_image, - color: Colors.grey, - ), - ), - ), - ), - ); - }, - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/ui/pages/board_page.dart b/lib/ui/pages/board_page.dart @@ -1,191 +0,0 @@ -// BROKEN - -import 'dart:io'; -import 'package:flutter/material.dart'; - -import '../../data/repos/board_repo.dart'; -import '../../data/repos/board_image_repo.dart'; -import '../../data/models/board_model.dart'; -import '../../data/models/image_model.dart'; - -class BoardListPage extends StatefulWidget { - const BoardListPage({super.key}); - - @override - State<BoardListPage> createState() => _BoardListPageState(); -} - -class _BoardListPageState extends State<BoardListPage> { - final boardRepo = BoardRepository(); - List<Board> boards = []; - - @override - void initState() { - super.initState(); - loadBoards(); - } - - Future<void> loadBoards() async { - final result = await boardRepo.getBoards(); - boards = result.map((e) => Board.fromMap(e)).toList(); - if (mounted) setState(() {}); - } - - Future<void> createBoardDialog() async { - final nameController = TextEditingController(); - if (!mounted) return; - - await showDialog( - context: context, - builder: (c) => AlertDialog( - title: Text("Create New Board"), - content: TextField( - controller: nameController, - decoration: InputDecoration(hintText: "Board name"), - ), - actions: [ - TextButton( - onPressed: () { - Navigator.pop(c); - }, - child: Text("Cancel"), - ), - ElevatedButton( - onPressed: () async { - if (nameController.text.isNotEmpty) { - await boardRepo.createBoard(nameController.text); - if (!c.mounted) return; - Navigator.pop(c); - await loadBoards(); - } - }, - child: Text("Create"), - ) - ], - ), - ); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text("Boards"), - actions: [ - IconButton( - icon: Icon(Icons.add), - onPressed: createBoardDialog, - ) - ], - ), - - body: boards.isEmpty - ? Center(child: Text("No boards yet")) - : GridView.builder( - padding: EdgeInsets.all(12), - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - childAspectRatio: 1, - crossAxisSpacing: 10, - mainAxisSpacing: 10, - ), - itemCount: boards.length, - itemBuilder: (c, i) { - final board = boards[i]; - - return GestureDetector( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => BoardDetailPage(board: board), - ), - ); - }, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: Colors.grey.shade300, - ), - child: Center( - child: Text( - board.name, - style: TextStyle(fontSize: 18), - ), - ), - ), - ); - }, - ), - ); - } -} - -// --------------------------------------------------------- -// BOARD DETAIL PAGE -// --------------------------------------------------------- - -class BoardDetailPage extends StatefulWidget { - final Board board; - - const BoardDetailPage({super.key, required this.board}); - - @override - State<BoardDetailPage> createState() => _BoardDetailPageState(); -} - -class _BoardDetailPageState extends State<BoardDetailPage> { - final boardImageRepo = BoardImageRepository(); - List<ImageModel> images = []; - - @override - void initState() { - super.initState(); - loadImages(); - } - - Future<void> loadImages() async { - final result = await boardImageRepo.getImagesOfBoard(widget.board.id); - - images = result - .map((e) => ImageModel( - id: e['id'], - filePath: e['filePath'], - )) - .toList(); - - if (mounted) setState(() {}); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(widget.board.name), - ), - body: images.isEmpty - ? Center(child: Text("No images in this board yet")) - : GridView.builder( - padding: EdgeInsets.all(12), - itemCount: images.length, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - mainAxisSpacing: 10, - crossAxisSpacing: 10, - childAspectRatio: 1, - ), - itemBuilder: (context, index) { - final img = images[index]; - - return ClipRRect( - borderRadius: BorderRadius.circular(10), - child: Image.file( - File(img.filePath), - fit: BoxFit.cover, - ), - ); - }, - ), - ); - } -} diff --git a/lib/ui/pages/moodboards_page.dart b/lib/ui/pages/moodboards_page.dart @@ -1,327 +0,0 @@ -//BROKEN - -import 'dart:io'; -import 'package:flutter/material.dart'; -import 'package:adobe/data/repos/board_repo.dart'; -import 'package:adobe/data/repos/board_image_repo.dart'; - -class MoodboardsPage extends StatefulWidget { - const MoodboardsPage({super.key}); - - @override - State<MoodboardsPage> createState() => _MoodboardsPageState(); -} - -class _MoodboardsPageState extends State<MoodboardsPage> { - final _boardRepo = BoardRepository(); - final _boardImageRepo = BoardImageRepository(); - final _searchController = TextEditingController(); - - List<Map<String, dynamic>> _allBoards = []; - List<Map<String, dynamic>> _filteredBoards = []; - bool _isLoading = true; - - @override - void initState() { - super.initState(); - _loadData(); - _searchController.addListener(_onSearchChanged); - } - - Future<void> _loadData() async { - // 1. Fetch Boards - final boards = await _boardRepo.getBoards(); - - // 2. (Optional) Fetch a thumbnail image for each board to make it look good - // This is a simulated join. In a real app, do this in the SQL query. - List<Map<String, dynamic>> enrichedBoards = []; - - for (var board in boards) { - // Get the first image of this board to serve as cover - final images = await _boardImageRepo.getImagesForBoard(board['id']); - String? coverPath; - if (images.isNotEmpty) { - coverPath = images.first['filePath']; - } - - enrichedBoards.add({ - ...board, - 'coverPath': coverPath, - // If your DB doesn't have a 'project' column yet, we mock it for the UI demo - // You should add 'project' TEXT to your board_repo.dart create table - 'project': board['project'] ?? 'General Projects', - }); - } - - if (mounted) { - setState(() { - _allBoards = enrichedBoards; - _filteredBoards = enrichedBoards; - _isLoading = false; - }); - } - } - - void _onSearchChanged() { - final query = _searchController.text.toLowerCase(); - setState(() { - _filteredBoards = - _allBoards.where((board) { - final name = (board['name'] as String).toLowerCase(); - final project = (board['project'] as String).toLowerCase(); - return name.contains(query) || project.contains(query); - }).toList(); - }); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.white, // Match screenshot background - appBar: AppBar( - backgroundColor: Colors.white, - elevation: 0, - leading: IconButton( - icon: const Icon( - Icons.arrow_back_ios_new, - size: 20, - color: Colors.black, - ), - onPressed: () => Navigator.pop(context), - ), - title: const Text( - "MoodBoards", - style: TextStyle(color: Colors.black, fontWeight: FontWeight.bold), - ), - actions: [ - IconButton( - icon: const Icon(Icons.add, color: Colors.black), - onPressed: () { - // Add new board logic - }, - ), - ], - ), - body: - _isLoading - ? const Center(child: CircularProgressIndicator()) - : Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: ListView( - physics: const BouncingScrollPhysics(), - children: [ - const SizedBox(height: 10), - - // --- 1. SEARCH BAR --- - Container( - decoration: BoxDecoration( - color: Colors.grey[200], // Match Screenshot grey - borderRadius: BorderRadius.circular(12), - ), - child: TextField( - controller: _searchController, - decoration: const InputDecoration( - hintText: "Search", - prefixIcon: Icon(Icons.search, color: Colors.grey), - border: InputBorder.none, - contentPadding: EdgeInsets.symmetric(vertical: 14), - ), - ), - ), - - const SizedBox(height: 24), - - // --- 2. RECENT PROJECTS --- - const Text( - "Recent Projects/Events", - style: TextStyle( - fontSize: 14, - color: Colors.black54, - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 12), - - // Take top 3 items for "Recent" - ..._filteredBoards - .take(3) - .map((board) => _buildRecentCard(board)), - - const SizedBox(height: 24), - - // --- 3. ALL PROJECTS (Accordion Style) --- - const Text( - "All Projects/Events", - style: TextStyle( - fontSize: 14, - color: Colors.black54, - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 12), - - _buildAllProjectsList(), - - const SizedBox(height: 40), - ], - ), - ), - ); - } - - // --- WIDGET: The Card for Recent Items --- - Widget _buildRecentCard(Map<String, dynamic> board) { - return Container( - margin: const EdgeInsets.only(bottom: 12), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: Colors.grey.shade200), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.03), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: ListTile( - contentPadding: const EdgeInsets.all(8), - leading: Container( - width: 60, - height: 60, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: Colors.grey[100], - image: - board['coverPath'] != null - ? DecorationImage( - image: FileImage(File(board['coverPath'])), - fit: BoxFit.cover, - ) - : null, - ), - child: - board['coverPath'] == null - ? Icon(Icons.dashboard, color: Colors.grey[400]) - : null, - ), - title: Text( - board['project'].toString(), - style: TextStyle(fontSize: 12, color: Colors.grey[600]), - ), - subtitle: Text( - board['name'], - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - ), - onTap: () { - // Navigate to Board Details - }, - ), - ); - } - - // --- WIDGET: The Accordion List for "All Projects" --- - Widget _buildAllProjectsList() { - // 1. Group boards by 'Project' - // { "Bakery": [board1, board2], "Run Club": [board3] } - Map<String, List<Map<String, dynamic>>> grouped = {}; - - for (var board in _filteredBoards) { - String project = board['project'] ?? "Uncategorized"; - if (!grouped.containsKey(project)) { - grouped[project] = []; - } - grouped[project]!.add(board); - } - - // 2. Build Expansion Tiles - return Column( - children: - grouped.entries.map((entry) { - String projectName = entry.key; - List<Map<String, dynamic>> projectBoards = entry.value; - - // Generate a random-ish color for the avatar based on name length - Color avatarColor = - Colors - .primaries[projectName.length % Colors.primaries.length] - .shade100; - Color textColor = - Colors - .primaries[projectName.length % Colors.primaries.length] - .shade700; - - return Container( - margin: const EdgeInsets.only(bottom: 10), - decoration: BoxDecoration( - color: - Colors.grey[50], // Very light grey background for the group - borderRadius: BorderRadius.circular(16), - border: Border.all(color: Colors.grey.shade200), - ), - child: ExpansionTile( - shape: const Border(), // Remove default divider borders - collapsedShape: const Border(), - leading: Container( - width: 48, - height: 48, - decoration: BoxDecoration( - color: avatarColor, - borderRadius: BorderRadius.circular(12), - ), - child: Center( - child: Text( - projectName.substring(0, 1).toUpperCase(), // First letter - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: textColor, - ), - ), - ), - ), - title: Text( - projectName, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - children: - projectBoards.map((board) { - return Container( - decoration: const BoxDecoration( - border: Border(top: BorderSide(color: Colors.white)), - ), - child: ListTile( - contentPadding: const EdgeInsets.only( - left: 70, - right: 20, - bottom: 8, - ), - title: Text( - board['name'], - style: const TextStyle(fontWeight: FontWeight.w500), - ), - trailing: const Icon( - Icons.arrow_forward_ios, - size: 14, - color: Colors.grey, - ), - onTap: () { - // Navigate to Board Details - }, - ), - ); - }).toList(), - ), - ); - }).toList(), - ); - } -} diff --git a/lib/ui/pages/project_detail_page.dart b/lib/ui/pages/project_detail_page.dart @@ -3,7 +3,8 @@ import 'package:flutter/material.dart'; import '../../data/models/project_model.dart'; import '../../data/repos/project_repo.dart'; import '../../services/project_service.dart'; -import 'project_board_page.dart'; // Import the board page +import 'project_board_page.dart'; +import 'stylesheet_page.dart'; class ProjectDetailPage extends StatefulWidget { final int projectId; @@ -144,7 +145,16 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { MaterialPageRoute( builder: (_) => ProjectBoardPage(projectId: projectId), ), - ).then((_) => _loadData()); // Refresh on return (e.g. last accessed update) + ).then((_) => _loadData()); + } + + void _navigateToStylesheet(int projectId) { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => StylesheetPage(projectId: projectId), + ), + ).then((_) => _loadData()); } void _showPlaceholder(String feature) { @@ -357,7 +367,7 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { theme: theme, isDark: isDark, isSmall: isSmall, - onTap: () => _showPlaceholder("Stylesheet"), + onTap: () => _navigateToStylesheet(targetId), ), ), const SizedBox(width: 8), diff --git a/lib/ui/pages/stylesheet_page.dart b/lib/ui/pages/stylesheet_page.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:adobe/ui/styles/variables.dart'; +import 'package:adobe/ui/widgets/bottom_bar.dart'; +import 'package:adobe/ui/widgets/top_bar.dart'; + +class StylesheetPage extends StatefulWidget { + final int projectId; + + const StylesheetPage({ + super.key, + required this.projectId, + }); + + @override + State<StylesheetPage> createState() => _StylesheetPageState(); +} + +class _StylesheetPageState extends State<StylesheetPage> { + late int _currentProjectId; + + @override + void initState() { + super.initState(); + _currentProjectId = widget.projectId; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Variables.background, + + appBar: TopBar( + currentProjectId: _currentProjectId, + onBack: () => Navigator.of(context).pop(), + onProjectChanged: (newProject) { + setState(() { + _currentProjectId = newProject.id!; + // If required, reload any page-specific content + }); + }, + ), + + // Body + body: Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Are you ready to start\nbuilding the visual identity?", + textAlign: TextAlign.center, + style: Variables.headerStyle, + ), + const SizedBox(height: 32), + + // Pill Button + GestureDetector( + onTap: () { + // TODO + }, + child: Container( + width: 274, + height: 44, + decoration: BoxDecoration( + color: Variables.textPrimary, + borderRadius: BorderRadius.circular(112), + ), + alignment: Alignment.center, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Generate Stylesheet", + style: Variables.buttonTextStyle, + ), + const SizedBox(width: 8), + SvgPicture.asset( + "assets/icons/generate_icon.svg", + width: 18, + height: 18, + colorFilter: const ColorFilter.mode( + Variables.textWhite, + BlendMode.srcIn, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + + bottomNavigationBar: BottomBar( + currentTab: BottomBarItem.stylesheet, + projectId: _currentProjectId, + ), + ); + } +} diff --git a/lib/ui/styles/variables.dart b/lib/ui/styles/variables.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; + +class Variables { + // Colors + static const Color textWhite = Color(0xFFFAFAFA); + static const Color textPrimary = Color(0xFF27272A); + static const Color textSecondary = Color(0xFF71717A); + static const Color textDisabled = Color(0xFFA1A1AA); + + static const Color surfaceSubtle = Color(0xFFF4F4F5); + static const Color background = Colors.white; + + static const Color borderSubtle = Color(0xFFE4E4E7); + + // Dimensions + static const double fontSizeHeader = 20.0; + static const double lineHeightHeader = 24.0; + + static const double fontSizeBody = 14.0; + static const double lineHeightBody = 20.0; + static const double trackingBody = 0.25; + + static const double fontSizeSmall = 12.0; + + static const double fontSizeCaption = 10.0; + + // Text Styles + static TextStyle get headerStyle => const TextStyle( + fontFamily: 'GeneralSans', + fontSize: fontSizeHeader, + height: lineHeightHeader / fontSizeHeader, + fontWeight: FontWeight.w500, + color: textPrimary, + ); + + static TextStyle get bodyStyle => const TextStyle( + fontFamily: 'GeneralSans', + fontSize: fontSizeBody, + height: lineHeightBody / fontSizeBody, + letterSpacing: trackingBody, + color: textPrimary, + ); + + static TextStyle get captionStyle => const TextStyle( + fontFamily: 'GeneralSans', + fontSize: fontSizeCaption, + color: textSecondary, + ); + + static TextStyle get buttonTextStyle => const TextStyle( + fontFamily: 'GeneralSans', + fontSize: fontSizeBody, + height: lineHeightBody / fontSizeBody, + letterSpacing: trackingBody, + fontWeight: FontWeight.w500, + color: textWhite, + ); +} diff --git a/lib/ui/widgets/.gitkeep b/lib/ui/widgets/.gitkeep diff --git a/lib/ui/widgets/bottom_bar.dart b/lib/ui/widgets/bottom_bar.dart @@ -0,0 +1,131 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:adobe/ui/styles/variables.dart'; +import 'package:adobe/ui/pages/project_board_page.dart'; +import 'package:adobe/ui/pages/stylesheet_page.dart'; + +enum BottomBarItem { + moodboard, + stylesheet, + files +} + +class BottomBar extends StatelessWidget { + final BottomBarItem currentTab; + final int projectId; + + const BottomBar({ + super.key, + required this.currentTab, + required this.projectId, + }); + + void _onTap(BuildContext context, BottomBarItem item) { + if (item == currentTab) return; + + Widget nextPage; + switch (item) { + case BottomBarItem.moodboard: + nextPage = ProjectBoardPage(projectId: projectId); + break; + case BottomBarItem.stylesheet: + nextPage = StylesheetPage(projectId: projectId); + break; + case BottomBarItem.files: + // TODO + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Files page not implemented yet")), + ); + return; + } + + Navigator.pushReplacement( + context, + PageRouteBuilder( + pageBuilder: (context, anim1, anim2) => nextPage, + transitionDuration: Duration.zero, + reverseTransitionDuration: Duration.zero, + ), + ); + } + + @override + Widget build(BuildContext context) { + return Container( + height: 90, + decoration: const BoxDecoration( + color: Variables.background, + border: Border( + top: BorderSide(color: Variables.borderSubtle, width: 1), + ), + ), + padding: const EdgeInsets.only(bottom: 24, top: 12), + 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", + ), + ), + ], + ), + ); + } + + Widget _buildNavItem( + BuildContext context, + BottomBarItem item, + String label, + String assetPath, + ) { + final bool isSelected = item == currentTab; + final Color color = + isSelected ? Variables.textPrimary : Variables.textDisabled; + + return GestureDetector( + onTap: () => _onTap(context, item), + behavior: HitTestBehavior.opaque, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + assetPath, + width: 24, + height: 24, + colorFilter: ColorFilter.mode(color, BlendMode.srcIn), + ), + const SizedBox(height: 6), + Text( + label, + style: Variables.captionStyle.copyWith( + color: color, + fontSize: 11, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/ui/widgets/top_bar.dart b/lib/ui/widgets/top_bar.dart @@ -0,0 +1,188 @@ +import 'package:flutter/material.dart'; +import 'package:adobe/data/models/project_model.dart'; +import 'package:adobe/data/repos/project_repo.dart'; +import 'package:adobe/ui/styles/variables.dart'; + +class TopBar extends StatefulWidget implements PreferredSizeWidget { + final int currentProjectId; + final VoidCallback? onBack; + final Function(ProjectModel)? onProjectChanged; + final VoidCallback? onSettingsPressed; + + const TopBar({ + super.key, + required this.currentProjectId, + this.onBack, + this.onProjectChanged, + this.onSettingsPressed, + }); + + @override + State<TopBar> createState() => _TopBarState(); + + @override + Size get preferredSize => const Size.fromHeight(80); +} + +class _TopBarState extends State<TopBar> { + final ProjectRepo _projectRepo = ProjectRepo(); + + bool _isLoading = true; + ProjectModel? _currentProject; + ProjectModel? _rootProject; + List<ProjectModel> _contextList = []; + + @override + void initState() { + super.initState(); + _loadData(); + } + + @override + void didUpdateWidget(covariant TopBar oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.currentProjectId != widget.currentProjectId) { + _loadData(); + } + } + + Future<void> _loadData() async { + try { + final current = await _projectRepo.getProjectById(widget.currentProjectId); + if (current == null) return; + + ProjectModel? root; + List<ProjectModel> events = []; + + if (current.parentId == null) { + // Current IS Project + root = current; + events = await _projectRepo.getEvents(current.id!); + } else { + // Current IS Event + root = await _projectRepo.getProjectById(current.parentId!); + if (root != null) { + events = await _projectRepo.getEvents(root.id!); + } + } + root ??= current; + final List<ProjectModel> fullList = [root, ...events]; + if (mounted) { + setState(() { + _currentProject = current; + _rootProject = root; + _contextList = fullList; + _isLoading = false; + }); + } + } catch (e) { + debugPrint("TopBar Error: $e"); + } + } + + @override + Widget build(BuildContext context) { + return Container( + color: Variables.background, + child: SafeArea( + bottom: false, + child: Container( + height: 80, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + // 1. Back Button + if (widget.onBack != null) + IconButton( + icon: const Icon(Icons.arrow_back, color: Variables.textPrimary), + onPressed: widget.onBack, + ), + if (widget.onBack != null) const SizedBox(width: 8), + + // 2. Title & Selector + if (!_isLoading && _currentProject != null && _rootProject != null) + Expanded( + child: Row( + children: [ + Expanded( + child: Text( + _rootProject!.title, + style: const TextStyle( + fontSize: 26, + fontWeight: FontWeight.w600, + color: Variables.textPrimary, + ), + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 12), + PopupMenuButton<ProjectModel>( + onSelected: (project) { + widget.onProjectChanged?.call(project); + }, + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + offset: const Offset(0, 40), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 14, vertical: 10), + decoration: BoxDecoration( + color: Variables.surfaceSubtle, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + _currentProject!.id == _rootProject!.id + ? "Main Project" + : _currentProject!.title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Variables.textPrimary, + ), + ), + const SizedBox(width: 4), + const Icon( + Icons.keyboard_arrow_down_rounded, + color: Variables.textPrimary, + ) + ], + ), + ), + itemBuilder: (context) { + return _contextList.map((ProjectModel project) { + final isRoot = project.id == _rootProject!.id; + final isSelected = project.id == _currentProject!.id; + return PopupMenuItem<ProjectModel>( + value: project, + child: Text( + isRoot ? "Main Project" : project.title, + style: Variables.bodyStyle.copyWith( + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + color: isSelected ? Variables.textPrimary : Variables.textSecondary, + ), + ), + ); + }).toList(); + }, + ), + ], + ), + ), + + // 3. Settings Icon + IconButton( + icon: const Icon(Icons.settings_outlined, color: Variables.textPrimary), + onPressed: widget.onSettingsPressed, + ), + ], + ), + ), + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml @@ -24,6 +24,7 @@ dependencies: onnxruntime: ^1.4.1 intl: ^0.20.2 google_mlkit_text_recognition: ^0.15.0 + flutter_svg: ^2.2.3 dev_dependencies: flutter_test: @@ -36,6 +37,7 @@ flutter: assets: - assets/fonts/ + - assets/icons/ - assets/dinov2.onnx - assets/dinov2.onnx.data - assets/texture_centroids.json