creek

The AI Image Editor of 2030

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}