creek

The AI Image Editor of 2030

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}