creek
The AI Image Editor of 2030git clone https://github.com/maydayv7/creek
lib/ui/widgets/canvas/asset_picker_sheet.dart (8477B)
1import 'dart:io';
2import 'package:flutter/material.dart';
3import 'package:path_provider/path_provider.dart';
4import 'package:path/path.dart' as p;
5import 'package:creekui/data/repos/project_repo.dart';
6import 'package:creekui/ui/styles/variables.dart';
7import 'package:creekui/ui/widgets/primary_button.dart';
8
9class AssetPickerSheet extends StatefulWidget {
10 final int projectId;
11 final ScrollController scrollController;
12 final Function(List<String>) onAddAssets;
13
14 const AssetPickerSheet({
15 super.key,
16 required this.projectId,
17 required this.scrollController,
18 required this.onAddAssets,
19 });
20
21 @override
22 State<AssetPickerSheet> createState() => _AssetPickerSheetState();
23}
24
25class _AssetPickerSheetState extends State<AssetPickerSheet> {
26 List<String> _assets = [];
27 bool _isLoading = true;
28 Set<String> _selectedPaths = {};
29
30 @override
31 void initState() {
32 super.initState();
33 _loadAssets();
34 }
35
36 Future<void> _loadAssets() async {
37 try {
38 final project = await ProjectRepo().getProjectById(widget.projectId);
39 if (mounted) {
40 setState(() {
41 _assets = project?.assetsPath ?? [];
42 _isLoading = false;
43 });
44 }
45 } catch (e) {
46 debugPrint("Error loading assets: $e");
47 if (mounted) setState(() => _isLoading = false);
48 }
49 }
50
51 Future<File?> _resolveFile(String path) async {
52 final file = File(path);
53 if (await file.exists()) return file;
54 try {
55 final filename = p.basename(path);
56 final dir = await getApplicationDocumentsDirectory();
57 final fixedPath = '${dir.path}/generated_images/$filename';
58 final fixedFile = File(fixedPath);
59 if (await fixedFile.exists()) return fixedFile;
60 } catch (e) {
61 debugPrint("Error resolving file: $e");
62 }
63 return null;
64 }
65
66 @override
67 Widget build(BuildContext context) {
68 return Container(
69 decoration: const BoxDecoration(
70 color: Variables.background,
71 borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
72 ),
73 padding: const EdgeInsets.fromLTRB(20, 12, 20, 0),
74 child: Column(
75 children: [
76 // Handle
77 Container(
78 width: 40,
79 height: 4,
80 margin: const EdgeInsets.only(bottom: 20),
81 decoration: BoxDecoration(
82 color: Variables.borderSubtle,
83 borderRadius: BorderRadius.circular(2),
84 ),
85 ),
86
87 // Search Bar
88 Container(
89 height: 40,
90 margin: const EdgeInsets.only(bottom: 16),
91 decoration: BoxDecoration(
92 color: Variables.surfaceSubtle,
93 borderRadius: BorderRadius.circular(8),
94 ),
95 child: Row(
96 children: [
97 const SizedBox(width: 12),
98 const Icon(Icons.search, color: Variables.textSecondary),
99 const SizedBox(width: 8),
100 const Text(
101 "Search Stylesheet",
102 style: TextStyle(
103 fontFamily: 'GeneralSans',
104 color: Variables.textSecondary,
105 ),
106 ),
107 ],
108 ),
109 ),
110
111 // Category Tabs
112 Container(
113 margin: const EdgeInsets.only(bottom: 16),
114 child: Row(
115 children: [
116 _buildFilterChip("Assets", true),
117 const SizedBox(width: 12),
118 _buildFilterChip("Backgrounds & Texture", false),
119 ],
120 ),
121 ),
122
123 // Grid
124 Expanded(
125 child:
126 _isLoading
127 ? const Center(child: CircularProgressIndicator())
128 : _assets.isEmpty
129 ? Center(
130 child: Text(
131 "No assets found in stylesheet",
132 style: TextStyle(color: Variables.textSecondary),
133 ),
134 )
135 : Stack(
136 children: [
137 GridView.builder(
138 controller: widget.scrollController,
139 gridDelegate:
140 const SliverGridDelegateWithFixedCrossAxisCount(
141 crossAxisCount: 3,
142 crossAxisSpacing: 12,
143 mainAxisSpacing: 12,
144 childAspectRatio: 1.0,
145 ),
146 itemCount: _assets.length,
147 itemBuilder: (context, index) {
148 final assetPath = _assets[index];
149 final isSelected = _selectedPaths.contains(
150 assetPath,
151 );
152
153 return FutureBuilder<File?>(
154 future: _resolveFile(assetPath),
155 builder: (context, snapshot) {
156 final file = snapshot.data;
157 return _buildAssetTile(
158 child:
159 file != null
160 ? Image.file(file, fit: BoxFit.cover)
161 : const Icon(
162 Icons.broken_image,
163 color: Colors.grey,
164 ),
165 isSelected: isSelected,
166 onTap: () {
167 if (file != null) {
168 setState(() {
169 if (isSelected) {
170 _selectedPaths.remove(assetPath);
171 } else {
172 _selectedPaths.add(assetPath);
173 }
174 });
175 }
176 },
177 );
178 },
179 );
180 },
181 ),
182 ],
183 ),
184 ),
185
186 // Bottom CTA
187 SafeArea(
188 top: false,
189 child: Container(
190 width: double.infinity,
191 margin: const EdgeInsets.only(top: 16, bottom: 16),
192 child: PrimaryButton(
193 text: "Add to File",
194 onPressed: () => widget.onAddAssets(_selectedPaths.toList()),
195 ),
196 ),
197 ),
198 ],
199 ),
200 );
201 }
202
203 Widget _buildFilterChip(String label, bool isSelected) {
204 return Container(
205 padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
206 decoration: BoxDecoration(
207 color: isSelected ? Variables.surfaceSubtle : Colors.transparent,
208 borderRadius: BorderRadius.circular(20),
209 border: Border.all(
210 color: isSelected ? Colors.transparent : Variables.borderSubtle,
211 ),
212 ),
213 child: Text(
214 label,
215 style: TextStyle(
216 fontFamily: 'GeneralSans',
217 fontSize: 14,
218 fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
219 color: isSelected ? Variables.textPrimary : Variables.textSecondary,
220 ),
221 ),
222 );
223 }
224
225 Widget _buildAssetTile({
226 required Widget child,
227 required VoidCallback onTap,
228 bool isSelected = false,
229 }) {
230 return GestureDetector(
231 onTap: onTap,
232 child: Container(
233 decoration: BoxDecoration(
234 color: Colors.white,
235 borderRadius: BorderRadius.circular(12),
236 border: Border.all(
237 color: isSelected ? Colors.blue : Variables.borderSubtle,
238 width: isSelected ? 2 : 1,
239 ),
240 ),
241 clipBehavior: Clip.antiAlias,
242 child: Stack(
243 fit: StackFit.expand,
244 children: [
245 child,
246 if (isSelected)
247 Container(
248 color: Colors.blue.withOpacity(0.1),
249 child: const Center(
250 child: Icon(Icons.check_circle, color: Colors.blue),
251 ),
252 ),
253 ],
254 ),
255 ),
256 );
257 }
258}