this repo has no description

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

+392 -231
+2 -1
.vscode/settings.json
··· 2 "editor.codeActionsOnSave": { 3 "source.organizeImports": "always" 4 }, 5 - "dart.lineLength": 100 6 }
··· 2 "editor.codeActionsOnSave": { 3 "source.organizeImports": "always" 4 }, 5 + "dart.lineLength": 100, 6 + "dart.flutterHotReloadOnSave": "all" 7 }
+15 -10
lib/providers/gallery_cache_provider.dart
··· 1 import 'dart:async'; 2 import 'dart:io'; 3 4 - import 'package:bluesky_text/bluesky_text.dart'; 5 import 'package:flutter/foundation.dart'; 6 import 'package:grain/models/gallery_photo.dart'; 7 import 'package:grain/models/procedures/apply_alts_update.dart'; ··· 43 44 void setGalleriesForActor(String did, List<Gallery> galleries) { 45 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 } 55 56 Future<void> toggleFavorite(String uri) async { ··· 109 required List<XFile> xfiles, 110 int? startPosition, 111 bool includeExif = true, 112 }) async { 113 // Fetch the latest gallery from the API to avoid stale state 114 final latestGallery = await apiService.getGallery(uri: galleryUri); ··· 120 final int positionOffset = startPosition ?? initialCount; 121 final List<String> photoUris = []; 122 int position = positionOffset; 123 - for (final xfile in xfiles) { 124 final file = File(xfile.path); 125 // Parse EXIF if requested 126 final exif = includeExif ? await parseAndNormalizeExif(file: file) : null; 127 // Resize the image 128 final resizedResult = await compute<File, ResizeResult>((f) => resizeImage(file: f), file); 129 // Upload the blob ··· 174 required String description, 175 required List<XFile> xfiles, 176 bool includeExif = true, 177 }) async { 178 final res = await apiService.createGallery( 179 request: CreateGalleryRequest(title: title, description: description), ··· 183 galleryUri: res.galleryUri, 184 xfiles: xfiles, 185 includeExif: includeExif, 186 ); 187 return (res.galleryUri, photoUris); 188 }
··· 1 import 'dart:async'; 2 import 'dart:io'; 3 4 import 'package:flutter/foundation.dart'; 5 import 'package:grain/models/gallery_photo.dart'; 6 import 'package:grain/models/procedures/apply_alts_update.dart'; ··· 42 43 void setGalleriesForActor(String did, List<Gallery> galleries) { 44 setGalleries(galleries); 45 } 46 47 Future<void> toggleFavorite(String uri) async { ··· 100 required List<XFile> xfiles, 101 int? startPosition, 102 bool includeExif = true, 103 + void Function(int imageIndex, double progress)? onProgress, 104 }) async { 105 // Fetch the latest gallery from the API to avoid stale state 106 final latestGallery = await apiService.getGallery(uri: galleryUri); ··· 112 final int positionOffset = startPosition ?? initialCount; 113 final List<String> photoUris = []; 114 int position = positionOffset; 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 + 120 final file = File(xfile.path); 121 // Parse EXIF if requested 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 + 130 // Resize the image 131 final resizedResult = await compute<File, ResizeResult>((f) => resizeImage(file: f), file); 132 // Upload the blob ··· 177 required String description, 178 required List<XFile> xfiles, 179 bool includeExif = true, 180 + void Function(int imageIndex, double progress)? onProgress, 181 }) async { 182 final res = await apiService.createGallery( 183 request: CreateGalleryRequest(title: title, description: description), ··· 187 galleryUri: res.galleryUri, 188 xfiles: xfiles, 189 includeExif: includeExif, 190 + onProgress: onProgress, 191 ); 192 return (res.galleryUri, photoUris); 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 + }