creek
The AI Image Editor of 2030git clone https://github.com/maydayv7/creek
lib/ui/pages/share_to_file_page.dart (12183B)
1import 'dart:io';
2import 'dart:convert';
3import 'package:flutter/material.dart';
4import 'package:image/image.dart' as img;
5import 'package:creekui/services/file_service.dart';
6import 'package:creekui/services/project_service.dart';
7import 'package:creekui/data/models/file_model.dart';
8import 'package:creekui/data/models/project_model.dart';
9import 'package:creekui/ui/styles/variables.dart';
10import 'package:creekui/ui/widgets/search_bar.dart';
11import 'package:creekui/ui/widgets/file_card.dart';
12import 'package:creekui/ui/widgets/section_header.dart';
13import 'package:creekui/ui/widgets/empty_state.dart';
14import 'package:creekui/ui/widgets/app_bar.dart';
15import 'create_file_page.dart';
16import 'canvas_page.dart';
17
18class ShareToFilePage extends StatefulWidget {
19 final File sharedImage;
20 const ShareToFilePage({super.key, required this.sharedImage});
21
22 @override
23 State<ShareToFilePage> createState() => _ShareToFilePageState();
24}
25
26class _ShareToFilePageState extends State<ShareToFilePage> {
27 final FileService _fileService = FileService();
28 final ProjectService _projectService = ProjectService();
29 final TextEditingController _searchController = TextEditingController();
30
31 List<FileModel> _allFiles = [];
32 List<FileModel> _filteredFiles = [];
33 List<FileModel> _recentFiles = [];
34
35 // file.id -> { 'preview': path, 'dimensions': str }
36 Map<String, Map<String, String>> _fileMetadata = {};
37
38 // Avoid repeated fetches
39 final Map<int, ProjectModel> _projectCache = {};
40
41 bool _isLoading = true;
42 String _searchQuery = "";
43
44 @override
45 void initState() {
46 super.initState();
47 _fetchFiles();
48 }
49
50 @override
51 void dispose() {
52 _searchController.dispose();
53 super.dispose();
54 }
55
56 Future<void> _fetchFiles() async {
57 setState(() => _isLoading = true);
58 try {
59 final List<FileModel> files = await _fileService.getAllFiles();
60 final List<FileModel> recent = await _fileService.getRecentFiles(
61 limit: 10,
62 );
63
64 // Sort by lastUpdated descending
65 files.sort((a, b) => b.lastUpdated.compareTo(a.lastUpdated));
66 recent.sort((a, b) => b.lastUpdated.compareTo(a.lastUpdated));
67
68 _allFiles = files;
69 _filteredFiles = List.from(files);
70 _recentFiles = recent.take(3).toList();
71
72 await _loadFileMetadata(files);
73 setState(() => _isLoading = false);
74 } catch (e) {
75 debugPrint('Error fetching files in ShareToFilePage: $e');
76 setState(() => _isLoading = false);
77 }
78 }
79
80 Future<void> _loadFileMetadata(List<FileModel> files) async {
81 final Map<String, Map<String, String>> meta = {};
82 for (final fmodel in files) {
83 final info = await _fileService.getFileMetadata(fmodel.filePath);
84 meta[fmodel.id] = {
85 'preview': info.previewPath ?? '',
86 'dimensions': info.dimensions,
87 };
88 }
89 _fileMetadata = meta;
90 }
91
92 void _filterFiles(String query) {
93 setState(() {
94 _searchQuery = query;
95 if (query.trim().isEmpty) {
96 _filteredFiles = List.from(_allFiles);
97 } else {
98 final q = query.toLowerCase();
99 _filteredFiles =
100 _allFiles.where((file) {
101 final nameMatch = file.name.toLowerCase().contains(q);
102 final breadcrumb = _getProjectBreadcrumbSync(file).toLowerCase();
103 final projectMatch = breadcrumb.contains(q);
104 return nameMatch || projectMatch;
105 }).toList();
106 }
107 });
108 }
109
110 // Synchronous breadcrumb using cached project (returns empty if not cached)
111 String _getProjectBreadcrumbSync(FileModel file) {
112 final proj = _projectCache[file.projectId];
113 if (proj == null) return '';
114 if (proj.parentId != null) {
115 final parent = _projectCache[proj.parentId!];
116 if (parent != null) return '${parent.title} / ${proj.title}';
117 }
118 return proj.title;
119 }
120
121 // Async breadcrumb loader that fetches projects into cache as needed
122 Future<String> _getProjectEventLabel(FileModel file) async {
123 if (!_projectCache.containsKey(file.projectId)) {
124 try {
125 final p = await _projectService.getProjectById(file.projectId);
126 if (p != null) _projectCache[file.projectId] = p;
127 } catch (e) {
128 debugPrint('Project load failed for ${file.projectId}: $e');
129 }
130 }
131 final project = _projectCache[file.projectId];
132 if (project == null) return "Unknown";
133
134 if (project.parentId == null) {
135 return project.title;
136 }
137
138 final parentId = project.parentId!;
139 if (!_projectCache.containsKey(parentId)) {
140 try {
141 final parent = await _projectService.getProjectById(parentId);
142 if (parent != null) _projectCache[parentId] = parent;
143 } catch (e) {
144 debugPrint('Parent project load failed for $parentId: $e');
145 }
146 }
147
148 final parentProject = _projectCache[parentId];
149 if (parentProject == null) return project.title;
150 return "${parentProject.title} / ${project.title}";
151 }
152
153 void _onAddPressed() {
154 Navigator.push(
155 context,
156 MaterialPageRoute(
157 builder: (_) => CreateFilePage(file: widget.sharedImage),
158 ),
159 );
160 }
161
162 void _onFileSelected(FileModel file) async {
163 try {
164 final f = File(file.filePath);
165 if (!await f.exists()) {
166 ScaffoldMessenger.of(
167 context,
168 ).showSnackBar(const SnackBar(content: Text("File not found")));
169 return;
170 }
171
172 // Default values
173 double width = 1080;
174 double height = 1080;
175
176 // File Metadata
177 final meta = await _fileService.getFileMetadata(file.filePath);
178 if (meta.width > 0 && meta.height > 0) {
179 width = meta.width;
180 height = meta.height;
181 }
182
183 Navigator.push(
184 context,
185 MaterialPageRoute(
186 builder:
187 (_) => CanvasPage(
188 projectId: file.projectId,
189 width: width,
190 height: height,
191 existingFile: file,
192 injectedMedia: widget.sharedImage,
193 ),
194 ),
195 );
196 } catch (e) {
197 debugPrint("Error opening file: $e");
198 }
199 }
200
201 String _formatDate(DateTime date) {
202 final diff = DateTime.now().difference(date);
203 if (diff.inDays == 0) return 'Today';
204 if (diff.inDays == 1) return 'Yesterday';
205 if (diff.inDays < 7) return '${diff.inDays} days ago';
206 return '${date.day}/${date.month}/${date.year}';
207 }
208
209 @override
210 Widget build(BuildContext context) {
211 return Scaffold(
212 backgroundColor: Variables.surfaceBackground,
213 appBar: CommonAppBar(
214 title: 'Files',
215 leading: IconButton(
216 icon: const Icon(Icons.arrow_back, color: Variables.textPrimary),
217 onPressed: () => Navigator.pop(context),
218 ),
219 actions: [
220 IconButton(
221 icon: const Icon(Icons.add, color: Variables.textPrimary, size: 28),
222 onPressed: _onAddPressed,
223 tooltip: "Create New File",
224 ),
225 ],
226 ),
227 body:
228 _isLoading
229 ? const Center(
230 child: CircularProgressIndicator(color: Variables.textPrimary),
231 )
232 : _allFiles.isEmpty
233 ? _buildEmptyState()
234 : Column(
235 children: [
236 // Search bar
237 Padding(
238 padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
239 child: CommonSearchBar(
240 controller: _searchController,
241 onChanged: _filterFiles,
242 ),
243 ),
244 Expanded(
245 child: SingleChildScrollView(
246 child: Column(
247 crossAxisAlignment: CrossAxisAlignment.start,
248 children: [
249 if (_filteredFiles.isEmpty && _searchQuery.isNotEmpty)
250 const Padding(
251 padding: EdgeInsets.all(32.0),
252 child: EmptyState(
253 icon: Icons.search_off,
254 title: "No results found",
255 subtitle: "Try adjusting your search",
256 ),
257 ),
258
259 // Recent Files
260 if (_recentFiles.isNotEmpty &&
261 _searchQuery.isEmpty) ...[
262 const Padding(
263 padding: EdgeInsets.symmetric(horizontal: 16),
264 child: SectionHeader(title: "Recent Files"),
265 ),
266 const SizedBox(height: 12),
267 Padding(
268 padding: const EdgeInsets.symmetric(
269 horizontal: 16,
270 ),
271 child: Column(
272 children:
273 _recentFiles
274 .map((file) => _buildFileCard(file))
275 .toList(),
276 ),
277 ),
278 const SizedBox(height: 24),
279 ],
280
281 // All files header
282 if (_filteredFiles.isNotEmpty) ...[
283 Padding(
284 padding: const EdgeInsets.symmetric(
285 horizontal: 16,
286 ),
287 child: SectionHeader(
288 title:
289 _searchQuery.isEmpty
290 ? "All Files"
291 : "Search Results",
292 ),
293 ),
294 const SizedBox(height: 12),
295 Padding(
296 padding: const EdgeInsets.symmetric(
297 horizontal: 16,
298 ),
299 child: ListView.builder(
300 shrinkWrap: true,
301 physics: const NeverScrollableScrollPhysics(),
302 itemCount: _filteredFiles.length,
303 itemBuilder:
304 (context, index) =>
305 _buildFileCard(_filteredFiles[index]),
306 ),
307 ),
308 ],
309 const SizedBox(height: 40),
310 ],
311 ),
312 ),
313 ),
314 ],
315 ),
316 );
317 }
318
319 Widget _buildEmptyState() {
320 return Center(
321 child: Column(
322 mainAxisAlignment: MainAxisAlignment.center,
323 children: [
324 const Icon(Icons.folder_open, size: 80, color: Colors.grey),
325 const SizedBox(height: 16),
326 const Text(
327 "No files yet",
328 style: TextStyle(color: Colors.grey, fontSize: 16),
329 ),
330 const SizedBox(height: 8),
331 TextButton(
332 onPressed: _onAddPressed,
333 child: const Text("Create your first file"),
334 ),
335 ],
336 ),
337 );
338 }
339
340 Widget _buildFileCard(FileModel file) {
341 final meta = _fileMetadata[file.id] ?? {};
342 final preview = meta['preview'] ?? '';
343 final dimensions = meta['dimensions'] ?? 'Unknown';
344
345 return FutureBuilder<String>(
346 future: _getProjectEventLabel(file),
347 builder: (context, snapshot) {
348 return Padding(
349 padding: const EdgeInsets.only(bottom: 12),
350 child: FileCard(
351 file: file,
352 breadcrumb: snapshot.data ?? "",
353 dimensions: dimensions,
354 previewPath: preview,
355 timeAgo: _formatDate(file.lastUpdated),
356 onTap: () => _onFileSelected(file),
357 onMenuAction: null,
358 ),
359 );
360 },
361 );
362 }
363}