creek
The AI Image Editor of 2030git clone https://github.com/maydayv7/creek
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}