creek

The AI Image Editor of 2030

lib/ui/pages/create_file_page.dart (13745B)


  1import 'dart:io';
  2import 'package:flutter/material.dart';
  3import 'package:flutter_svg/flutter_svg.dart';
  4import 'package:creekui/services/project_service.dart';
  5import 'package:creekui/ui/styles/variables.dart';
  6import 'package:creekui/ui/widgets/search_bar.dart';
  7import 'package:creekui/ui/widgets/project_selector.dart';
  8import 'package:creekui/ui/widgets/app_bar.dart';
  9import 'canvas_page.dart';
 10import 'define_brand_page.dart';
 11
 12class CreateFilePage extends StatefulWidget {
 13  final File? file;
 14  final int? projectId;
 15
 16  const CreateFilePage({super.key, this.file, this.projectId});
 17
 18  @override
 19  State<CreateFilePage> createState() => _CreateFilePageState();
 20}
 21
 22class _CreateFilePageState extends State<CreateFilePage> {
 23  final TextEditingController _searchController = TextEditingController();
 24
 25  int? _selectedProjectId;
 26  String _selectedProjectTitle = "Select Project";
 27
 28  final List<CanvasPreset> _allPresets = [
 29    CanvasPreset(
 30      name: 'Custom',
 31      width: 0,
 32      height: 0,
 33      displaySize: 'Custom Size',
 34    ),
 35    CanvasPreset(
 36      name: 'Poster',
 37      width: 2304,
 38      height: 3456,
 39      displaySize: '24 x 36 in',
 40    ),
 41    CanvasPreset(
 42      name: 'Instagram Post',
 43      width: 1080,
 44      height: 1080,
 45      displaySize: '1080 x 1080 px',
 46      svgPath: 'assets/icons/instagram_poster.svg',
 47    ),
 48    CanvasPreset(
 49      name: 'Invitation',
 50      width: 480,
 51      height: 672,
 52      displaySize: '5 x 7 in',
 53      svgPath: 'assets/icons/invitation.svg',
 54    ),
 55    CanvasPreset(
 56      name: 'Flyer - A5',
 57      width: 560,
 58      height: 794,
 59      displaySize: '148 x 210 mm',
 60    ),
 61    CanvasPreset(
 62      name: 'Business Card',
 63      width: 336,
 64      height: 192,
 65      displaySize: '3.5 x 2 in',
 66      svgPath: 'assets/icons/group.svg',
 67    ),
 68    CanvasPreset(
 69      name: 'Photo Collage',
 70      width: 1800,
 71      height: 1800,
 72      displaySize: '1800 x 1800 px',
 73    ),
 74    CanvasPreset(
 75      name: 'Menu',
 76      width: 794,
 77      height: 1123,
 78      displaySize: '210 x 297 mm',
 79    ),
 80    CanvasPreset(
 81      name: 'Menu Book',
 82      width: 794,
 83      height: 1123,
 84      displaySize: '210 x 297 mm',
 85    ),
 86  ];
 87
 88  List<CanvasPreset> _filteredPresets = [];
 89
 90  @override
 91  void initState() {
 92    super.initState();
 93    _filteredPresets = _allPresets;
 94
 95    // If an existing projectId was passed, lock to that project
 96    _selectedProjectId = widget.projectId;
 97    if (_selectedProjectId != null) {
 98      _selectedProjectTitle = "Current Project";
 99    }
100  }
101
102  @override
103  void dispose() {
104    _searchController.dispose();
105    super.dispose();
106  }
107
108  void _runFilter(String keyword) {
109    if (keyword.isEmpty) {
110      setState(() => _filteredPresets = _allPresets);
111      return;
112    }
113    setState(() {
114      _filteredPresets =
115          _allPresets
116              .where(
117                (p) => p.name.toLowerCase().contains(keyword.toLowerCase()),
118              )
119              .toList();
120    });
121  }
122
123  // Select project
124  void _openProjectSelection() {
125    showModalBottomSheet(
126      context: context,
127      backgroundColor: Colors.transparent,
128      isScrollControlled: true,
129      builder: (_) {
130        return DraggableScrollableSheet(
131          initialChildSize: 0.85,
132          maxChildSize: 0.95,
133          minChildSize: 0.5,
134          builder: (_, controller) {
135            return Container(
136              decoration: const BoxDecoration(
137                color: Colors.white,
138                borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
139              ),
140              child: _ProjectSelectionModalContent(
141                scrollController: controller,
142                onProjectSelected: (id, title) {
143                  setState(() {
144                    _selectedProjectId = id;
145                    _selectedProjectTitle = title;
146                  });
147                  Navigator.pop(context);
148                },
149              ),
150            );
151          },
152        );
153      },
154    );
155  }
156
157  // Go to canvas with selected project
158  void _navigateToEditor(int width, int height) {
159    if (_selectedProjectId == null) {
160      ScaffoldMessenger.of(context).showSnackBar(
161        const SnackBar(content: Text("Please select a destination project")),
162      );
163      return;
164    }
165
166    Navigator.push(
167      context,
168      MaterialPageRoute(
169        builder:
170            (_) => CanvasPage(
171              projectId: _selectedProjectId!,
172              width: width.toDouble(),
173              height: height.toDouble(),
174              initialImage: widget.file,
175            ),
176      ),
177    );
178  }
179
180  @override
181  Widget build(BuildContext context) {
182    final theme = Theme.of(context);
183    final isDark = theme.brightness == Brightness.dark;
184
185    return Scaffold(
186      backgroundColor: theme.scaffoldBackgroundColor,
187      appBar: CommonAppBar(
188        title: 'Create Files',
189        showBack: true,
190        leading: IconButton(
191          icon: Icon(Icons.arrow_back, color: theme.colorScheme.onSurface),
192          onPressed: () => Navigator.pop(context),
193        ),
194        actions: [
195          if (widget.projectId == null)
196            Padding(
197              padding: const EdgeInsets.only(right: 16),
198              child: TextButton.icon(
199                onPressed: _openProjectSelection,
200                style: TextButton.styleFrom(
201                  backgroundColor: isDark ? Colors.grey[800] : Colors.white,
202                  padding: const EdgeInsets.symmetric(
203                    horizontal: 12,
204                    vertical: 8,
205                  ),
206                  shape: RoundedRectangleBorder(
207                    borderRadius: BorderRadius.circular(20),
208                  ),
209                ),
210                icon: Icon(
211                  _selectedProjectId == null
212                      ? Icons.create_new_folder_outlined
213                      : Icons.folder_open,
214                  size: 18,
215                  color: theme.colorScheme.onSurface,
216                ),
217                label: Text(
218                  _selectedProjectTitle,
219                  style: Variables.bodyStyle.copyWith(
220                    fontSize: 12,
221                    color: theme.colorScheme.onSurface,
222                  ),
223                ),
224              ),
225            ),
226        ],
227      ),
228      body: Column(
229        crossAxisAlignment: CrossAxisAlignment.start,
230        children: [
231          // Search Bar
232          Padding(
233            padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
234            child: CommonSearchBar(
235              controller: _searchController,
236              hintText: 'Search sizes',
237              onChanged: _runFilter,
238            ),
239          ),
240
241          Padding(
242            padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
243            child: Text(
244              'Canvas Sizes',
245              style: Variables.bodyStyle.copyWith(
246                color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
247                fontSize: 15,
248                fontWeight: FontWeight.w600,
249              ),
250            ),
251          ),
252
253          // Grid
254          Expanded(
255            child: GridView.builder(
256              padding: const EdgeInsets.symmetric(horizontal: 16),
257              gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
258                crossAxisCount: 3,
259                crossAxisSpacing: 12,
260                mainAxisSpacing: 12,
261                childAspectRatio: 0.75,
262              ),
263              itemCount: _filteredPresets.length,
264              itemBuilder:
265                  (_, i) => _buildPresetCard(_filteredPresets[i], theme),
266            ),
267          ),
268        ],
269      ),
270    );
271  }
272
273  Widget _buildPresetCard(CanvasPreset preset, ThemeData theme) {
274    final bool isCustom = preset.name == 'Custom';
275    final isDark = theme.brightness == Brightness.dark;
276
277    return InkWell(
278      onTap:
279          () => _navigateToEditor(
280            isCustom ? 1000 : preset.width,
281            isCustom ? 1000 : preset.height,
282          ),
283      borderRadius: BorderRadius.circular(12),
284      child: Container(
285        decoration: BoxDecoration(
286          color: theme.cardColor,
287          borderRadius: BorderRadius.circular(12),
288          border: Border.all(
289            color: isDark ? Variables.borderDark : Colors.transparent,
290          ),
291        ),
292        padding: const EdgeInsets.all(8),
293        child: Column(
294          crossAxisAlignment: CrossAxisAlignment.start,
295          children: [
296            Expanded(
297              child: Container(
298                decoration: BoxDecoration(
299                  color: isDark ? Colors.grey[800] : const Color(0xFFE0E7FF),
300                  borderRadius: BorderRadius.circular(8),
301                ),
302                child: Center(
303                  child:
304                      isCustom
305                          ? const Icon(Icons.add, color: Colors.blue, size: 30)
306                          : Padding(
307                            padding: const EdgeInsets.all(12),
308                            child: AspectRatio(
309                              aspectRatio: preset.width / preset.height,
310                              child: Container(
311                                decoration: BoxDecoration(
312                                  color: theme.cardColor,
313                                  borderRadius: BorderRadius.circular(4),
314                                  boxShadow: [
315                                    BoxShadow(
316                                      color: Colors.black.withOpacity(0.05),
317                                      blurRadius: 4,
318                                      offset: const Offset(0, 2),
319                                    ),
320                                  ],
321                                ),
322                                child:
323                                    preset.svgPath != null
324                                        ? SvgPicture.asset(
325                                          preset.svgPath!,
326                                          fit: BoxFit.contain,
327                                        )
328                                        : null,
329                              ),
330                            ),
331                          ),
332                ),
333              ),
334            ),
335            const SizedBox(height: 8),
336            Text(
337              preset.name,
338              maxLines: 1,
339              overflow: TextOverflow.ellipsis,
340              style: Variables.bodyStyle.copyWith(
341                fontSize: 11,
342                fontWeight: FontWeight.w600,
343                color: theme.colorScheme.onSurface,
344              ),
345            ),
346            Text(
347              preset.displaySize,
348              style: Variables.captionStyle.copyWith(
349                fontSize: 9,
350                color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
351              ),
352            ),
353          ],
354        ),
355      ),
356    );
357  }
358}
359
360class CanvasPreset {
361  final String name;
362  final int width;
363  final int height;
364  final String displaySize;
365  final String? svgPath;
366
367  CanvasPreset({
368    required this.name,
369    required this.width,
370    required this.height,
371    required this.displaySize,
372    this.svgPath,
373  });
374}
375
376// Project Selection Modal
377class _ProjectSelectionModalContent extends StatefulWidget {
378  final Function(int id, String title) onProjectSelected;
379  final ScrollController scrollController;
380
381  const _ProjectSelectionModalContent({
382    required this.onProjectSelected,
383    required this.scrollController,
384  });
385
386  @override
387  State<_ProjectSelectionModalContent> createState() =>
388      _ProjectSelectionModalContentState();
389}
390
391class _ProjectSelectionModalContentState
392    extends State<_ProjectSelectionModalContent> {
393  Key _selectorKey = UniqueKey();
394
395  Future<void> _createNewProject() async {
396    final result = await Navigator.push(
397      context,
398      MaterialPageRoute(builder: (_) => const DefineBrandPage(projectName: "")),
399    );
400
401    if (result != null && result is Map) {
402      widget.onProjectSelected(result["id"], result["title"]);
403    } else {
404      // If project is created but not selected immediately, refreshing the list is a safe bet
405      setState(() {
406        _selectorKey = UniqueKey();
407      });
408    }
409  }
410
411  @override
412  Widget build(BuildContext context) {
413    return Column(
414      children: [
415        Center(
416          child: Container(
417            width: 40,
418            height: 4,
419            margin: const EdgeInsets.only(top: 12, bottom: 8),
420            decoration: BoxDecoration(
421              color: Colors.grey[300],
422              borderRadius: BorderRadius.circular(2),
423            ),
424          ),
425        ),
426
427        // Header row
428        Padding(
429          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
430          child: Row(
431            mainAxisAlignment: MainAxisAlignment.spaceBetween,
432            children: [
433              Text(
434                "Select Destination",
435                style: Variables.headerStyle.copyWith(fontSize: 18),
436              ),
437              IconButton(
438                icon: const Icon(Icons.add),
439                onPressed: _createNewProject,
440                tooltip: "Create New Project",
441              ),
442            ],
443          ),
444        ),
445        Divider(color: Colors.grey[300], height: 1),
446        // Selector
447        Expanded(
448          child: ProjectSelector(
449            key: _selectorKey,
450            scrollController: widget.scrollController,
451            searchHint: "Search Projects",
452            onProjectSelected: (id, title, parentTitle) {
453              final displayTitle =
454                  parentTitle != null ? "$parentTitle / $title" : title;
455              widget.onProjectSelected(id, displayTitle);
456            },
457          ),
458        ),
459      ],
460    );
461  }
462}