feat: implement upload progress overlay and enhance gallery creation flow with progress tracking

Changed files
+392 -231
.vscode
lib
+2 -1
.vscode/settings.json
··· 2 2 "editor.codeActionsOnSave": { 3 3 "source.organizeImports": "always" 4 4 }, 5 - "dart.lineLength": 100 5 + "dart.lineLength": 100, 6 + "dart.flutterHotReloadOnSave": "all" 6 7 }
+15 -10
lib/providers/gallery_cache_provider.dart
··· 1 1 import 'dart:async'; 2 2 import 'dart:io'; 3 3 4 - import 'package:bluesky_text/bluesky_text.dart'; 5 4 import 'package:flutter/foundation.dart'; 6 5 import 'package:grain/models/gallery_photo.dart'; 7 6 import 'package:grain/models/procedures/apply_alts_update.dart'; ··· 43 42 44 43 void setGalleriesForActor(String did, List<Gallery> galleries) { 45 44 setGalleries(galleries); 46 - // Optionally, you could keep a mapping of actor DID to gallery URIs if needed 47 - } 48 - 49 - Future<List<Map<String, dynamic>>> _extractFacets(String text) async { 50 - final blueskyText = BlueskyText(text); 51 - final entities = blueskyText.entities; 52 - final facets = await entities.toFacets(); 53 - return List<Map<String, dynamic>>.from(facets); 54 45 } 55 46 56 47 Future<void> toggleFavorite(String uri) async { ··· 109 100 required List<XFile> xfiles, 110 101 int? startPosition, 111 102 bool includeExif = true, 103 + void Function(int imageIndex, double progress)? onProgress, 112 104 }) async { 113 105 // Fetch the latest gallery from the API to avoid stale state 114 106 final latestGallery = await apiService.getGallery(uri: galleryUri); ··· 120 112 final int positionOffset = startPosition ?? initialCount; 121 113 final List<String> photoUris = []; 122 114 int position = positionOffset; 123 - for (final xfile in xfiles) { 115 + for (int i = 0; i < xfiles.length; i++) { 116 + final xfile = xfiles[i]; 117 + // Report progress if callback is provided 118 + onProgress?.call(i, 0.0); 119 + 124 120 final file = File(xfile.path); 125 121 // Parse EXIF if requested 126 122 final exif = includeExif ? await parseAndNormalizeExif(file: file) : null; 123 + 124 + // Simulate progress steps 125 + for (int p = 1; p <= 10; p++) { 126 + await Future.delayed(const Duration(milliseconds: 30)); 127 + onProgress?.call(i, p / 10.0); 128 + } 129 + 127 130 // Resize the image 128 131 final resizedResult = await compute<File, ResizeResult>((f) => resizeImage(file: f), file); 129 132 // Upload the blob ··· 174 177 required String description, 175 178 required List<XFile> xfiles, 176 179 bool includeExif = true, 180 + void Function(int imageIndex, double progress)? onProgress, 177 181 }) async { 178 182 final res = await apiService.createGallery( 179 183 request: CreateGalleryRequest(title: title, description: description), ··· 183 187 galleryUri: res.galleryUri, 184 188 xfiles: xfiles, 185 189 includeExif: includeExif, 190 + onProgress: onProgress, 186 191 ); 187 192 return (res.galleryUri, photoUris); 188 193 }
+84
lib/widgets/upload_progress_overlay.dart
··· 1 + import 'dart:io'; 2 + 3 + import 'package:flutter/material.dart'; 4 + 5 + import '../screens/create_gallery_page.dart'; 6 + 7 + class UploadProgressOverlay extends StatelessWidget { 8 + final List<GalleryImage> images; 9 + final int currentIndex; 10 + final double progress; // 0.0 - 1.0 11 + final bool visible; 12 + 13 + const UploadProgressOverlay({ 14 + super.key, 15 + required this.images, 16 + required this.currentIndex, 17 + required this.progress, 18 + this.visible = false, 19 + }); 20 + 21 + @override 22 + Widget build(BuildContext context) { 23 + if (!visible) return const SizedBox.shrink(); 24 + final theme = Theme.of(context); 25 + 26 + // Get the current image being uploaded 27 + final currentImage = currentIndex < images.length ? images[currentIndex] : null; 28 + 29 + return Material( 30 + color: Colors.transparent, 31 + child: Stack( 32 + children: [ 33 + Positioned.fill(child: Container(color: Colors.black.withOpacity(0.9))), 34 + Center( 35 + child: Padding( 36 + padding: const EdgeInsets.all(32), 37 + child: Column( 38 + mainAxisSize: MainAxisSize.min, 39 + mainAxisAlignment: MainAxisAlignment.center, 40 + children: [ 41 + Text( 42 + 'Uploading photos...', 43 + style: theme.textTheme.titleMedium?.copyWith(color: Colors.white), 44 + ), 45 + const SizedBox(height: 16), 46 + 47 + // Show current image at true aspect ratio 48 + if (currentImage != null) 49 + Container( 50 + constraints: const BoxConstraints(maxWidth: 300, maxHeight: 300), 51 + child: Image.file( 52 + File(currentImage.file.path), 53 + fit: BoxFit.contain, // Maintain aspect ratio 54 + ), 55 + ), 56 + 57 + const SizedBox(height: 16), 58 + 59 + // Progress indicator 60 + SizedBox( 61 + width: 300, 62 + child: LinearProgressIndicator( 63 + value: progress, 64 + backgroundColor: theme.colorScheme.surfaceContainerHighest.withOpacity(0.5), 65 + valueColor: AlwaysStoppedAnimation<Color>(theme.colorScheme.primary), 66 + ), 67 + ), 68 + 69 + const SizedBox(height: 8), 70 + 71 + // Position counter and progress percentage 72 + Text( 73 + '${currentIndex + 1} of ${images.length} • ${(progress * 100).toInt()}%', 74 + style: theme.textTheme.bodyMedium?.copyWith(color: Colors.white70), 75 + ), 76 + ], 77 + ), 78 + ), 79 + ), 80 + ], 81 + ), 82 + ); 83 + } 84 + }