creek

The AI Image Editor of 2030
commit cd2c717d14b122d25b38c46653920d40c7fd85fa
parent eae4ae8cd3cb542a29f583e3478f063bb241ec6f
Author: Sanjeebani Parida <83271316+sanjeebani14@users.noreply.github.com>
Date:   Sun, 30 Nov 2025 20:25:16 +0530

Merge pull request #23 from nilotpal-n7/sanjeebani

Added create file function
Diffstat:
Mandroid/app/src/main/kotlin/com/example/adobe/MainActivity.kt | 14++++++++++++++
Aassets/icons/Flyer.svg | 4++++
Aassets/icons/Group.svg | 3+++
Aassets/icons/add-line.svg | 3+++
Aassets/icons/instagram_poster.svg | 10++++++++++
Aassets/icons/invitation.svg | 10++++++++++
Mlib/main.dart | 22++++++++++++++++++++--
Mlib/services/file_service.dart | 4++++
Alib/ui/pages/create_file_page.dart | 389+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/ui/pages/project_detail_page.dart | 10+++++++++-
Alib/ui/pages/project_file_page.dart | 406+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/ui/pages/share_handler_page.dart | 63+++++++++++++++++++++++++++++++++++++++++++--------------------
Alib/ui/pages/share_to_file_page.dart | 468+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
13 files changed, 1383 insertions(+), 23 deletions(-)

diff --git a/android/app/src/main/kotlin/com/example/adobe/MainActivity.kt b/android/app/src/main/kotlin/com/example/adobe/MainActivity.kt @@ -7,11 +7,17 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import android.content.Intent class MainActivity : FlutterActivity() { private val CHANNEL = "com.example.adobe/methods" private val scope = CoroutineScope(Dispatchers.Default) + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + setIntent(intent) // This updates the activity's intent to the new one + } + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) @@ -42,6 +48,14 @@ class MainActivity : FlutterActivity() { val jsonList = call.argument<List<String>>("jsonList")!! ImageAnalyzer.generateStylesheet(jsonList) } + "getShareSource" -> { + val componentName = intent.component?.className + when { + componentName?.contains("ShareToFiles") == true -> "files" + componentName?.contains("ShareToMoodboards") == true -> "moodboards" + else -> "moodboards" + } + } else -> null } diff --git a/assets/icons/Flyer.svg b/assets/icons/Flyer.svg @@ -0,0 +1,4 @@ +<svg width="57" height="38" viewBox="0 0 57 38" fill="none" xmlns="http://www.w3.org/2000/svg"> +<line x1="0.270379" y1="0.420475" x2="56.2704" y2="36.4205" stroke="#9F9FA9"/> +<line y1="-0.5" x2="66.5583" y2="-0.5" transform="matrix(-0.8411 0.54088 -0.554451 -0.832217 55.9822 0.841064)" stroke="#9F9FA9"/> +</svg> diff --git a/assets/icons/Group.svg b/assets/icons/Group.svg @@ -0,0 +1,3 @@ +<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M11.7857 12.9644H2.35718V11.7858C2.35718 11.0043 2.6676 10.2549 3.22017 9.70234C3.77273 9.14978 4.52216 8.83936 5.30361 8.83936H8.83932C9.62076 8.83936 10.3702 9.14978 10.9228 9.70234C11.4753 10.2549 11.7857 11.0043 11.7857 11.7858V12.9644ZM7.07146 7.66078C6.60715 7.66078 6.14738 7.56933 5.7184 7.39164C5.28943 7.21396 4.89966 6.95352 4.57134 6.6252C4.24301 6.29688 3.98258 5.9071 3.80489 5.47813C3.6272 5.04916 3.53575 4.58939 3.53575 4.12507C3.53575 3.66075 3.6272 3.20098 3.80489 2.77201C3.98258 2.34304 4.24301 1.95326 4.57134 1.62494C4.89966 1.29662 5.28943 1.03618 5.7184 0.858496C6.14738 0.680809 6.60715 0.589355 7.07146 0.589355C8.00919 0.589355 8.90852 0.961867 9.57159 1.62494C10.2347 2.28802 10.6072 3.18734 10.6072 4.12507C10.6072 5.0628 10.2347 5.96212 9.57159 6.6252C8.90852 7.28827 8.00919 7.66078 7.07146 7.66078Z" fill="#9F9FA9"/> +</svg> diff --git a/assets/icons/add-line.svg b/assets/icons/add-line.svg @@ -0,0 +1,3 @@ +<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M9.16663 9.16675V4.16675H10.8333V9.16675H15.8333V10.8334H10.8333V15.8334H9.16663V10.8334H4.16663V9.16675H9.16663Z" fill="#FAFAFA"/> +</svg> diff --git a/assets/icons/instagram_poster.svg b/assets/icons/instagram_poster.svg @@ -0,0 +1,10 @@ +<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g clip-path="url(#clip0_1401_22552)"> +<path d="M3.82221 16.625L3.80637 16.6408L3.78975 16.625H2.36871C2.16035 16.6248 1.9606 16.5419 1.81334 16.3945C1.66609 16.2471 1.58337 16.0472 1.58337 15.8389V3.16113C1.58482 2.95322 1.668 2.75422 1.81495 2.60713C1.96189 2.46004 2.1608 2.37666 2.36871 2.375H16.6314C17.0652 2.375 17.4167 2.72729 17.4167 3.16113V15.8389C17.4153 16.0468 17.3321 16.2458 17.1851 16.3929C17.0382 16.54 16.8393 16.6233 16.6314 16.625H3.82221ZM15.8334 11.875V3.95833H3.16671V15.0417L11.0834 7.125L15.8334 11.875ZM15.8334 14.1138L11.0834 9.36383L5.40554 15.0417H15.8334V14.1138ZM6.33337 8.70833C5.91345 8.70833 5.51072 8.54152 5.21379 8.24459C4.91686 7.94765 4.75004 7.54493 4.75004 7.125C4.75004 6.70507 4.91686 6.30235 5.21379 6.00541C5.51072 5.70848 5.91345 5.54167 6.33337 5.54167C6.7533 5.54167 7.15603 5.70848 7.45296 6.00541C7.74989 6.30235 7.91671 6.70507 7.91671 7.125C7.91671 7.54493 7.74989 7.94765 7.45296 8.24459C7.15603 8.54152 6.7533 8.70833 6.33337 8.70833Z" fill="#9F9FA9"/> +</g> +<defs> +<clipPath id="clip0_1401_22552"> +<rect width="19" height="19" fill="white"/> +</clipPath> +</defs> +</svg> diff --git a/assets/icons/invitation.svg b/assets/icons/invitation.svg @@ -0,0 +1,10 @@ +<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g clip-path="url(#clip0_1401_22574)"> +<path d="M1.39054 4.24631L7.11966 0.811439C7.21595 0.753664 7.32614 0.723145 7.43843 0.723145C7.55072 0.723145 7.66091 0.753664 7.7572 0.811439L13.4807 4.24693C13.5267 4.27445 13.5647 4.31339 13.5911 4.35998C13.6174 4.40656 13.6313 4.45918 13.6313 4.51272V12.3911C13.6313 12.5554 13.566 12.713 13.4498 12.8292C13.3336 12.9454 13.176 13.0107 13.0117 13.0107H1.85956C1.69524 13.0107 1.53765 12.9454 1.42146 12.8292C1.30527 12.713 1.23999 12.5554 1.23999 12.3911V4.5121C1.23998 4.45857 1.25384 4.40594 1.28023 4.35936C1.30661 4.31278 1.34462 4.27383 1.39054 4.24631ZM2.47912 5.03873V11.7715H12.3922V5.03811L7.43812 2.0642L2.47912 5.03811V5.03873ZM7.47282 8.48661L10.754 5.72149L11.552 6.66943L7.48149 10.1L3.32421 6.67377L4.11229 5.71716L7.47282 8.48661Z" fill="#9F9FA9"/> +</g> +<defs> +<clipPath id="clip0_1401_22574"> +<rect width="14.8696" height="14.8696" fill="white"/> +</clipPath> +</defs> +</svg> diff --git a/lib/main.dart b/lib/main.dart @@ -5,6 +5,7 @@ import 'package:adobe/services/theme_service.dart'; import 'package:adobe/ui/pages/share_handler_page.dart'; import 'package:adobe/ui/pages/home_page.dart'; import 'package:adobe/services/analysis_queue_manager.dart'; +import 'package:flutter/services.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -67,12 +68,29 @@ class _MyAppState extends State<MyApp> { if (mounted) setState(() => _isReady = true); } - void _handleShare(String content) { + Future<void> _handleShare(String content) async { final context = _navigatorKey.currentState?.overlay?.context; if (context == null) return; + // 1. Determine destination (Files vs Moodboards) + String shareDestination = 'moodboards'; // Default + try { + const platform = MethodChannel('com.example.adobe/methods'); + final result = await platform.invokeMethod<String>('getShareSource'); + if (result != null) shareDestination = result; + } catch (e) { + debugPrint("Error getting share source: $e"); + } + + // 2. Pass destination to the Handler Page _navigatorKey.currentState?.push( - MaterialPageRoute(builder: (_) => ShareHandlerPage(sharedText: content)), + MaterialPageRoute( + builder: + (_) => ShareHandlerPage( + sharedText: content, + destination: shareDestination, // Pass the destination here + ), + ), ); } diff --git a/lib/services/file_service.dart b/lib/services/file_service.dart @@ -37,6 +37,10 @@ class FileService { return id; } + Future<List<FileModel>> getFiles(int projectId) async { + return await _repo.getFiles(projectId); + } + Future<void> openFile(String id) async { await _repo.touchFile(id); } diff --git a/lib/ui/pages/create_file_page.dart b/lib/ui/pages/create_file_page.dart @@ -0,0 +1,389 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class CreateFilePage extends StatefulWidget { + final File? file; // Made optional for blank canvas creation + + const CreateFilePage({super.key, this.file}); + + @override + State<CreateFilePage> createState() => _CreateFilePageState(); +} + +class _CreateFilePageState extends State<CreateFilePage> { + final TextEditingController _searchController = TextEditingController(); + + // Master list of presets + final List<CanvasPreset> _allPresets = [ + CanvasPreset( + name: 'Custom', + width: 0, + height: 0, + displaySize: 'Custom Size', + ), + CanvasPreset( + name: 'Poster', + width: 2304, + height: 3456, + displaySize: '24 x 36 in', + ), + CanvasPreset( + name: 'Instagram Post', + width: 1080, + height: 1080, + displaySize: '1080 x 1080 px', + svgPath: 'assets/icons/instagram_poster.svg', + ), + CanvasPreset( + name: 'Invitation', + width: 480, + height: 672, + displaySize: '5 x 7 in', + svgPath: 'assets/icons/invitation.svg', + ), + CanvasPreset( + name: 'Flyer - A5', + width: 560, + height: 794, + displaySize: '148 x 210 mm', + ), + CanvasPreset( + name: 'Business Card', + width: 336, + height: 192, + displaySize: '3.5 x 2 in', + svgPath: 'assets/icons/Group.svg', + ), + CanvasPreset( + name: 'Photo Collage', + width: 1800, + height: 1800, + displaySize: '1800 x 1800 px', + ), + CanvasPreset( + name: 'Menu', + width: 794, + height: 1123, + displaySize: '210 x 297 mm', + ), + CanvasPreset( + name: 'Menu Book', + width: 794, + height: 1123, + displaySize: '210 x 297 mm', + ), + ]; + + List<CanvasPreset> _filteredPresets = []; + + @override + void initState() { + super.initState(); + _filteredPresets = _allPresets; + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + void _runFilter(String enteredKeyword) { + List<CanvasPreset> results = []; + if (enteredKeyword.isEmpty) { + results = _allPresets; + } else { + results = + _allPresets + .where( + (preset) => preset.name.toLowerCase().contains( + enteredKeyword.toLowerCase(), + ), + ) + .toList(); + } + setState(() { + _filteredPresets = results; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF5F5F7), + appBar: AppBar( + backgroundColor: const Color(0xFFF5F5F7), + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.black), + onPressed: () => Navigator.pop(context), + ), + title: const Text( + 'Create Files', + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.w600, + fontSize: 16, + ), + ), + actions: [ + IconButton( + icon: const Icon(Icons.add, color: Colors.black), + onPressed: () {}, + ), + ], + ), + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Search Bar + Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + ), + child: TextField( + controller: _searchController, + onChanged: _runFilter, + decoration: InputDecoration( + hintText: 'Search', + hintStyle: TextStyle(color: Colors.grey[400], fontSize: 14), + prefixIcon: Icon( + Icons.search, + color: Colors.grey[400], + size: 20, + ), + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + suffixIcon: + _searchController.text.isNotEmpty + ? IconButton( + icon: const Icon( + Icons.clear, + color: Colors.grey, + size: 20, + ), + onPressed: () { + _searchController.clear(); + _runFilter(''); + }, + ) + : null, + ), + ), + ), + ), + + // Label + const Padding( + padding: EdgeInsets.fromLTRB(16, 0, 16, 8), + child: Text( + 'Canvas Sizes', + style: TextStyle( + color: Colors.grey, + fontSize: 15, + fontWeight: FontWeight.w600, + ), + ), + ), + + // Tabs + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + _buildTab('All', isSelected: true), + const SizedBox(width: 12), + _buildTab('Saved', isSelected: false), + const SizedBox(width: 12), + _buildTab('Photo', isSelected: false), + const SizedBox(width: 12), + _buildTab('Print', isSelected: false), + ], + ), + ), + ), + + const SizedBox(height: 16), + + // Grid + Expanded( + child: GridView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + childAspectRatio: 0.75, + ), + itemCount: _filteredPresets.length, + itemBuilder: (context, index) { + return _buildPresetCard(_filteredPresets[index]); + }, + ), + ), + ], + ), + ); + } + + Widget _buildTab(String label, {required bool isSelected}) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), + decoration: BoxDecoration( + color: isSelected ? Colors.white : Colors.transparent, + borderRadius: BorderRadius.circular(20), + ), + child: Text( + label, + style: TextStyle( + color: isSelected ? Colors.black : Colors.grey, + fontSize: 13, + fontWeight: isSelected ? FontWeight.w500 : FontWeight.normal, + ), + ), + ); + } + + Widget _buildPresetCard(CanvasPreset preset) { + final bool isCustom = preset.name == 'Custom'; + + return InkWell( + onTap: () { + if (isCustom) { + _navigateToEditor(1000, 1000); // Default custom size or show dialog + } else { + _navigateToEditor(preset.width, preset.height); + } + }, + borderRadius: BorderRadius.circular(12), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.all(8), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Top Section (Blue Box + White Box + Icon) + Expanded( + child: Container( + width: double.infinity, + decoration: BoxDecoration( + color: const Color(0xFFE0E7FF), // Blue BG + borderRadius: BorderRadius.circular(8), + ), + child: Center( + child: + isCustom + // Custom uses hardcoded Icon + ? const Icon(Icons.add, size: 30, color: Colors.blue) + // Others use AspectRatio box + SVG + : Padding( + padding: const EdgeInsets.all(12.0), + child: AspectRatio( + aspectRatio: + (preset.width > 0 && preset.height > 0) + ? preset.width / preset.height + : 1.0, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Center( + child: + preset.svgPath != null + ? Padding( + padding: const EdgeInsets.all(4.0), + child: SvgPicture.asset( + preset.svgPath!, + fit: BoxFit.contain, + ), + ) + : null, + ), + ), + ), + ), + ), + ), + ), + + const SizedBox(height: 8), + + // Bottom Text + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + preset.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 11, + color: Colors.black, + ), + ), + const SizedBox(height: 2), + Text( + preset.displaySize, + style: const TextStyle(color: Colors.grey, fontSize: 9), + ), + ], + ), + ], + ), + ), + ); + } + + void _navigateToEditor(int width, int height) { + // If widget.file is null, we create a blank canvas. + // If widget.file is not null, we can place that file on the canvas. + final mode = widget.file == null ? "Blank Canvas" : "Image Canvas"; + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Selected: $width x $height ($mode)')), + ); + + // TODO: Navigate to Editor Page passing: + // - width + // - height + // - widget.file (optional) + } +} + +class CanvasPreset { + final String name; + final int width; + final int height; + final String displaySize; + final String? svgPath; + + CanvasPreset({ + required this.name, + required this.width, + required this.height, + required this.displaySize, + this.svgPath, + }); +} diff --git a/lib/ui/pages/project_detail_page.dart b/lib/ui/pages/project_detail_page.dart @@ -5,6 +5,7 @@ import '../../data/repos/project_repo.dart'; import '../../services/project_service.dart'; import 'project_board_page.dart'; import 'stylesheet_page.dart'; +import 'project_file_page.dart'; class ProjectDetailPage extends StatefulWidget { final int projectId; @@ -155,6 +156,13 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { ).then((_) => _loadData()); } + void _navigateToFiles(int projectId) { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => ProjectFilePage(projectId: projectId)), + ).then((_) => _loadData()); + } + void _showPlaceholder(String feature) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -386,7 +394,7 @@ class _ProjectDetailPageState extends State<ProjectDetailPage> { theme: theme, isDark: isDark, isSmall: isSmall, - onTap: () => _showPlaceholder("Files"), + onTap: () => _navigateToFiles(targetId), ), ), ], diff --git a/lib/ui/pages/project_file_page.dart b/lib/ui/pages/project_file_page.dart @@ -0,0 +1,406 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:intl/intl.dart'; +import '../../data/models/file_model.dart'; +import '../../services/file_service.dart'; +import 'create_file_page.dart'; // 1. Add this import + +class ProjectFilePage extends StatefulWidget { + final int projectId; + + const ProjectFilePage({super.key, required this.projectId}); + + @override + State<ProjectFilePage> createState() => _ProjectFilePageState(); +} + +class _ProjectFilePageState extends State<ProjectFilePage> { + final _fileService = FileService(); + final _imagePicker = ImagePicker(); + + List<FileModel> _files = []; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _loadData(); + } + + Future<void> _loadData() async { + setState(() => _isLoading = true); + try { + final files = await _fileService.getFiles(widget.projectId); + if (mounted) { + setState(() { + _files = files; + _isLoading = false; + }); + } + } catch (e) { + debugPrint('Error loading files: $e'); + if (mounted) { + setState(() => _isLoading = false); + } + } + } + + Future<void> _pickAndAddFile() async { + try { + final XFile? pickedFile = await _imagePicker.pickImage( + source: ImageSource.gallery, + ); + + if (pickedFile != null && mounted) { + _showAddFileDialog(File(pickedFile.path)); + } + } catch (e) { + debugPrint("Error picking file: $e"); + } + } + + Future<void> _showAddFileDialog(File file) async { + final nameController = TextEditingController( + text: file.path.split('/').last, + ); + final descriptionController = TextEditingController(); + + if (!mounted) return; + + await showDialog( + context: context, + builder: + (context) => AlertDialog( + title: const Text( + "Save File", + style: TextStyle( + fontFamily: 'GeneralSans', + fontWeight: FontWeight.w600, + ), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: nameController, + decoration: const InputDecoration( + labelText: "File Name", + border: OutlineInputBorder(), + ), + style: const TextStyle(fontFamily: 'GeneralSans'), + ), + const SizedBox(height: 16), + TextField( + controller: descriptionController, + decoration: const InputDecoration( + labelText: "Description (optional)", + border: OutlineInputBorder(), + ), + maxLines: 2, + style: const TextStyle(fontFamily: 'GeneralSans'), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text("Cancel"), + ), + ElevatedButton( + onPressed: () async { + if (nameController.text.trim().isNotEmpty) { + try { + await _fileService.saveFile( + file, + widget.projectId, + name: nameController.text.trim(), + description: + descriptionController.text.trim().isEmpty + ? null + : descriptionController.text.trim(), + ); + if (context.mounted) { + Navigator.pop(context); + _loadData(); + } + } catch (e) { + debugPrint("Error saving file: $e"); + } + } + }, + child: const Text("Save"), + ), + ], + ), + ); + } + + Future<void> _deleteFile(String fileId) async { + final confirm = await showDialog<bool>( + context: context, + builder: + (context) => AlertDialog( + title: const Text("Delete File?"), + content: const Text("This action cannot be undone."), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text("Cancel"), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + style: TextButton.styleFrom(foregroundColor: Colors.red), + child: const Text("Delete"), + ), + ], + ), + ); + + if (confirm == true) { + await _fileService.deleteFile(fileId); + _loadData(); + } + } + + // 2. Navigation method for blank canvas + void _navigateToCreateFile() { + Navigator.push( + context, + MaterialPageRoute( + // Passing no file implies a "Blank Canvas" + builder: (_) => const CreateFilePage(), + ), + ).then((_) => _loadData()); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + + return Scaffold( + backgroundColor: theme.scaffoldBackgroundColor, + appBar: AppBar( + title: const Text("Project Files"), + backgroundColor: theme.appBarTheme.backgroundColor, + actions: [ + // 3. The + button on top right + IconButton( + icon: const Icon(Icons.add), + tooltip: "Create New File", + onPressed: _navigateToCreateFile, + ), + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: _pickAndAddFile, + child: const Icon( + Icons.upload_file, + ), // Changed to distinguish from top + + ), + body: + _isLoading + ? const Center(child: CircularProgressIndicator()) + : RefreshIndicator( + onRefresh: _loadData, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildSectionHeader("Files", theme), + Text( + "${_files.length} items", + style: TextStyle( + color: theme.colorScheme.onSurface.withOpacity( + 0.6, + ), + fontFamily: 'GeneralSans', + ), + ), + ], + ), + const SizedBox(height: 16), + if (_files.isEmpty) + _buildEmptyState(isDark) + else + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: _files.length, + separatorBuilder: + (_, __) => const SizedBox(height: 12), + itemBuilder: (context, index) { + final file = _files[index]; + return _buildFileCard(file, theme, isDark); + }, + ), + const SizedBox(height: 40), + ], + ), + ), + ), + ); + } + + Widget _buildEmptyState(bool isDark) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(32), + decoration: BoxDecoration( + color: isDark ? Colors.grey[850] : Colors.grey[100], + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isDark ? Colors.grey[800]! : Colors.grey[300]!, + ), + ), + child: Column( + children: [ + Icon(Icons.folder_open, size: 48, color: Colors.grey[400]), + const SizedBox(height: 8), + Text("No files added yet", style: TextStyle(color: Colors.grey[500])), + ], + ), + ); + } + + Widget _buildSectionHeader(String title, ThemeData theme) { + return Text( + title, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + fontFamily: 'GeneralSans', + color: theme.colorScheme.onSurface, + ), + ); + } + + Widget _buildFileCard(FileModel file, ThemeData theme, bool isDark) { + final dateStr = DateFormat.yMMMd().format(file.lastUpdated); + + return Container( + decoration: BoxDecoration( + color: isDark ? Colors.grey[850] : Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isDark ? Colors.grey[700]! : Colors.grey[300]!, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.03), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: theme.colorScheme.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.insert_drive_file, + color: theme.colorScheme.primary, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + file.name, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + fontFamily: 'GeneralSans', + ), + ), + const SizedBox(height: 4), + Text( + "Updated $dateStr", + style: TextStyle( + fontSize: 12, + color: theme.colorScheme.onSurface.withOpacity(0.5), + fontFamily: 'GeneralSans', + ), + ), + ], + ), + ), + PopupMenuButton<String>( + icon: const Icon(Icons.more_vert), + onSelected: (value) { + if (value == 'delete') { + _deleteFile(file.id); + } else if (value == 'open') { + _fileService.openFile(file.id); + } + }, + itemBuilder: + (context) => [ + const PopupMenuItem(value: 'open', child: Text("Open")), + const PopupMenuItem( + value: 'delete', + child: Text( + "Delete", + style: TextStyle(color: Colors.red), + ), + ), + ], + ), + ], + ), + if (file.description != null && file.description!.isNotEmpty) ...[ + const SizedBox(height: 12), + Text( + file.description!, + style: TextStyle( + fontSize: 13, + color: theme.colorScheme.onSurface.withOpacity(0.7), + fontFamily: 'GeneralSans', + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + if (file.tags.isNotEmpty) ...[ + const SizedBox(height: 12), + Wrap( + spacing: 8, + children: + file.tags + .map( + (tag) => Chip( + label: Text( + tag, + style: const TextStyle(fontSize: 10), + ), + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + visualDensity: VisualDensity.compact, + ), + ) + .toList(), + ), + ], + ], + ), + ); + } +} diff --git a/lib/ui/pages/share_handler_page.dart b/lib/ui/pages/share_handler_page.dart @@ -4,11 +4,17 @@ import 'package:adobe/services/download_service.dart'; import 'package:adobe/services/instagram_download_service.dart'; import 'package:adobe/services/image_service.dart'; import 'package:adobe/ui/pages/share_to_moodboard_page.dart'; +import 'package:adobe/ui/pages/share_to_file_page.dart'; class ShareHandlerPage extends StatefulWidget { final String sharedText; + final String destination; - const ShareHandlerPage({super.key, required this.sharedText}); + const ShareHandlerPage({ + super.key, + required this.sharedText, + required this.destination, + }); @override State<ShareHandlerPage> createState() => _ShareHandlerPageState(); @@ -66,32 +72,49 @@ class _ShareHandlerPageState extends State<ShareHandlerPage> { } } - // SUCCESS: Persist and Navigate + // SUCCESS: Route based on Destination if (tempFiles.isNotEmpty) { - List<File> permanentFiles = []; - - // PERSIST IMMEDIATELY - // Save to App Docs and DB (into Inbox) and status 'completed' (waiting for tags) - for (var file in tempFiles) { - final id = await _imageService.saveImage( - file, - 0, // Project 0 = Inbox - tags: [], - ); - - final savedImage = await _imageService.getImage(id); - if (savedImage != null) { - permanentFiles.add(File(savedImage.filePath)); - } - } + if (!mounted) return; - if (mounted) { + // CHECK DESTINATION + if (widget.destination == 'files') { + // --- ROUTE TO FILES --- Navigator.pushReplacement( context, MaterialPageRoute( - builder: (_) => ShareToMoodboardPage(imageFiles: permanentFiles), + builder: + (_) => ShareToFilePage( + // FIX: Use 'sharedImage' instead of 'file' to match ShareToFilePage constructor + sharedImage: tempFiles.first, + ), ), ); + } else { + // --- ROUTE TO MOODBOARDS (Default) --- + List<File> permanentFiles = []; + + for (var file in tempFiles) { + final id = await _imageService.saveImage( + file, + 0, // Project 0 = Inbox + tags: [], + ); + + final savedImage = await _imageService.getImage(id); + if (savedImage != null) { + permanentFiles.add(File(savedImage.filePath)); + } + } + + if (mounted) { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: + (_) => ShareToMoodboardPage(imageFiles: permanentFiles), + ), + ); + } } } else { throw Exception("Could not retrieve any media files."); diff --git a/lib/ui/pages/share_to_file_page.dart b/lib/ui/pages/share_to_file_page.dart @@ -0,0 +1,468 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import '../../services/file_service.dart'; +import '../../data/models/file_model.dart'; +import 'create_file_page.dart'; // Import the new creation page + +class ShareToFilePage extends StatefulWidget { + final File sharedImage; // This is the image passed from ShareHandler + + const ShareToFilePage({super.key, required this.sharedImage}); + + @override + State<ShareToFilePage> createState() => _ShareToFilePageState(); +} + +class _ShareToFilePageState extends State<ShareToFilePage> { + final FileService _fileService = FileService(); + final TextEditingController _searchController = TextEditingController(); + + List<FileModel> _allFiles = []; + List<FileModel> _filteredFiles = []; + List<FileModel> _recentFiles = []; + bool _isLoading = true; + String _searchQuery = ""; + + @override + void initState() { + super.initState(); + _fetchFiles(); + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + Future<void> _fetchFiles() async { + try { + setState(() => _isLoading = true); + + // Assuming projectId 0 is your 'Inbox' or default folder + final files = await _fileService.getFiles(0); + + // Sort by last updated to get recent files + files.sort((a, b) => b.lastUpdated.compareTo(a.lastUpdated)); + + setState(() { + _allFiles = files; + _filteredFiles = files; + _recentFiles = files.take(3).toList(); // Get 3 most recent files + _isLoading = false; + }); + } catch (e) { + debugPrint("Error fetching files: $e"); + setState(() => _isLoading = false); + } + } + + void _filterFiles(String query) { + setState(() { + _searchQuery = query; + if (query.isEmpty) { + _filteredFiles = _allFiles; + } else { + final q = query.toLowerCase(); + _filteredFiles = + _allFiles + .where((file) => file.name.toLowerCase().contains(q)) + .toList(); + } + }); + } + + void _onAddPressed() { + // Redirect to the CreateFilePage (Canvas Selection) with the shared image + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => CreateFilePage(file: widget.sharedImage), + ), + ); + } + + void _onFileSelected(FileModel file) { + // TODO: Handle file selection - maybe open editor with this file + // For now, just show a snackbar + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Selected: ${file.name}'))); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.black), + onPressed: () => Navigator.pop(context), + ), + title: const Text( + 'Your Files', + style: TextStyle( + color: Color(0xFF27272A), + fontFamily: 'GeneralSans', + fontSize: 20, + fontWeight: FontWeight.w500, + height: 1.2, + ), + ), + actions: [ + IconButton( + icon: const Icon(Icons.add, color: Colors.black, size: 28), + onPressed: _onAddPressed, + tooltip: "Create New File", + ), + ], + ), + body: + _isLoading + ? const Center(child: CircularProgressIndicator()) + : _allFiles.isEmpty + ? _buildEmptyState() + : Column( + children: [ + // --- Search Bar --- + Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: SizedBox( + height: 42, + child: TextField( + controller: _searchController, + onChanged: _filterFiles, + style: const TextStyle( + fontFamily: "GeneralSans", + fontSize: 16, + fontWeight: FontWeight.w400, + color: Color(0xFF71717B), + height: 1.4, + ), + decoration: InputDecoration( + hintText: "Search", + hintStyle: const TextStyle( + fontFamily: "GeneralSans", + fontSize: 16, + fontWeight: FontWeight.w400, + color: Color(0xFF71717B), + ), + prefixIcon: const Icon( + Icons.search, + size: 20, + color: Color(0xFF9F9FA9), + ), + filled: true, + fillColor: const Color(0xFFE4E4E7), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.symmetric( + vertical: 12, + horizontal: 16, + ), + suffixIcon: + _searchQuery.isNotEmpty + ? IconButton( + icon: const Icon( + Icons.clear, + size: 20, + color: Color(0xFF9F9FA9), + ), + padding: EdgeInsets.zero, + onPressed: () { + _searchController.clear(); + _filterFiles(""); + }, + ) + : null, + ), + ), + ), + ), + + Expanded( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // --- RECENT FILES SECTION --- + if (_searchQuery.isEmpty && + _recentFiles.isNotEmpty) ...[ + const Padding( + padding: EdgeInsets.symmetric( + horizontal: 20, + vertical: 8, + ), + child: Text( + "Recent Files", + style: TextStyle( + fontFamily: 'GeneralSans', + fontSize: 14, + fontWeight: FontWeight.w400, + color: Color(0xFF27272A), + height: 1.43, + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + ), + child: Column( + children: + _recentFiles + .map( + (file) => _buildRecentFileItem(file), + ) + .toList(), + ), + ), + const SizedBox(height: 24), + ], + + // --- ALL FILES SECTION --- + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 8, + ), + child: Text( + _searchQuery.isEmpty + ? "All Files" + : "Search Results", + style: const TextStyle( + fontFamily: 'GeneralSans', + fontSize: 14, + fontWeight: FontWeight.w400, + color: Color(0xFF27272A), + height: 1.43, + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: _filteredFiles.length, + itemBuilder: + (context, index) => + _buildFileItem(_filteredFiles[index]), + ), + ), + const SizedBox(height: 40), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildEmptyState() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.folder_open, size: 80, color: Colors.grey[300]), + const SizedBox(height: 16), + Text( + "No files yet", + style: TextStyle(color: Colors.grey[500], fontSize: 16), + ), + const SizedBox(height: 8), + TextButton( + onPressed: _onAddPressed, + child: const Text("Create your first file"), + ), + ], + ), + ); + } + + Widget _buildRecentFileItem(FileModel file) { + return Container( + margin: const EdgeInsets.only(bottom: 8), + child: InkWell( + onTap: () => _onFileSelected(file), + borderRadius: BorderRadius.circular(16), + child: Container( + padding: const EdgeInsets.fromLTRB(4, 4, 0, 4), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: const Color(0xFFE4E4E7), width: 1), + ), + child: Row( + children: [ + // File Thumbnail + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + color: const Color(0xFFFAFAFA), + borderRadius: BorderRadius.circular(8), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: + File(file.filePath).existsSync() + ? Image.file( + File(file.filePath), + fit: BoxFit.cover, + errorBuilder: + (_, __, ___) => Icon( + Icons.image, + color: Colors.grey[400], + size: 28, + ), + ) + : Icon( + Icons.image, + color: Colors.grey[400], + size: 28, + ), + ), + ), + const SizedBox(width: 10), + // File Info + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + file.name, + style: const TextStyle( + fontFamily: 'GeneralSans', + fontSize: 16, + fontWeight: FontWeight.w500, + color: Color(0xFF27272A), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + Text( + _formatDate(file.lastUpdated), + style: const TextStyle( + fontFamily: 'GeneralSans', + fontSize: 12, + color: Color(0xFF71717B), + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + const SizedBox(width: 12), + ], + ), + ), + ), + ); + } + + Widget _buildFileItem(FileModel file) { + return Container( + margin: const EdgeInsets.only(bottom: 8), + child: InkWell( + onTap: () => _onFileSelected(file), + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.fromLTRB(4, 4, 0, 4), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: const Color(0xFFE4E4E7), width: 1), + ), + child: Row( + children: [ + // File Thumbnail + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: const Color(0xFFFAFAFA), + borderRadius: BorderRadius.circular(8), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: + File(file.filePath).existsSync() + ? Image.file( + File(file.filePath), + fit: BoxFit.cover, + errorBuilder: + (_, __, ___) => Icon( + Icons.description, + color: Colors.grey[400], + size: 24, + ), + ) + : Icon( + Icons.description, + color: Colors.grey[400], + size: 24, + ), + ), + ), + const SizedBox(width: 12), + // File Info + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + file.name, + style: const TextStyle( + fontFamily: 'GeneralSans', + fontSize: 16, + fontWeight: FontWeight.w500, + color: Color(0xFF27272A), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + Text( + _formatDate(file.lastUpdated), + style: const TextStyle( + fontFamily: 'GeneralSans', + fontSize: 12, + color: Color(0xFF71717B), + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + const SizedBox(width: 12), + ], + ), + ), + ), + ); + } + + String _formatDate(DateTime date) { + final now = DateTime.now(); + final difference = now.difference(date); + + if (difference.inDays == 0) { + return "Today"; + } else if (difference.inDays == 1) { + return "Yesterday"; + } else if (difference.inDays < 7) { + return "${difference.inDays} days ago"; + } else { + return "${date.day}/${date.month}/${date.year}"; + } + } +}