+2
-1
.vscode/settings.json
+2
-1
.vscode/settings.json
+15
-10
lib/providers/gallery_cache_provider.dart
+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
}
+291
-220
lib/screens/create_gallery_page.dart
+291
-220
lib/screens/create_gallery_page.dart
···
12
12
import 'package:grain/providers/profile_provider.dart';
13
13
import 'package:grain/widgets/app_button.dart';
14
14
import 'package:grain/widgets/plain_text_field.dart';
15
+
import 'package:grain/widgets/upload_progress_overlay.dart';
15
16
import 'package:image_picker/image_picker.dart';
16
17
17
18
import '../providers/gallery_cache_provider.dart';
···
43
44
final List<GalleryImage> _images = [];
44
45
bool _submitting = false;
45
46
bool _includeExif = true;
47
+
48
+
// Upload progress state
49
+
bool _showUploadOverlay = false;
50
+
int _currentUploadIndex = 0;
51
+
double _currentUploadProgress = 0.0;
46
52
47
53
@override
48
54
void initState() {
···
150
156
setState(() => _submitting = true);
151
157
String? galleryUri;
152
158
final container = ProviderScope.containerOf(context, listen: false);
153
-
if (widget.gallery == null) {
154
-
// Use provider to create gallery and add photos
155
-
final newImages = _images.where((img) => !img.isExisting).toList();
156
-
final galleryCache = container.read(galleryCacheProvider.notifier);
157
-
final (createdUri, newPhotoUris) = await galleryCache.createGalleryAndAddPhotos(
158
-
title: _titleController.text.trim(),
159
-
description: _descController.text.trim(),
160
-
xfiles: newImages.map((img) => img.file).toList(),
161
-
includeExif: _includeExif,
162
-
);
163
-
galleryUri = createdUri;
164
-
// Update profile provider state to include new gallery
165
-
if (galleryUri != null && mounted) {
166
-
final newGallery = container.read(galleryCacheProvider)[galleryUri];
167
-
final profileNotifier = container.read(
168
-
profileNotifierProvider(apiService.currentUser!.did).notifier,
159
+
try {
160
+
if (widget.gallery == null) {
161
+
final newImages = _images.where((img) => !img.isExisting).toList();
162
+
final galleryCache = container.read(galleryCacheProvider.notifier);
163
+
if (newImages.isNotEmpty) {
164
+
setState(() {
165
+
_showUploadOverlay = true;
166
+
_currentUploadIndex = 0;
167
+
_currentUploadProgress = 0.0;
168
+
});
169
+
}
170
+
List<XFile> xfiles = newImages.map((img) => img.file).toList();
171
+
String? createdUri;
172
+
final (uri, photoUris) = await galleryCache.createGalleryAndAddPhotos(
173
+
title: _titleController.text.trim(),
174
+
description: _descController.text.trim(),
175
+
xfiles: xfiles,
176
+
includeExif: _includeExif,
177
+
onProgress: (int idx, double prog) {
178
+
setState(() {
179
+
_currentUploadIndex = idx;
180
+
_currentUploadProgress = prog;
181
+
});
182
+
},
169
183
);
170
-
if (newGallery != null) {
171
-
profileNotifier.addGalleryToProfile(newGallery);
184
+
createdUri = uri;
185
+
setState(() {
186
+
_showUploadOverlay = false;
187
+
});
188
+
galleryUri = createdUri;
189
+
// Update profile provider state to include new gallery
190
+
if (galleryUri != null && mounted) {
191
+
final newGallery = container.read(galleryCacheProvider)[galleryUri];
192
+
final profileNotifier = container.read(
193
+
profileNotifierProvider(apiService.currentUser!.did).notifier,
194
+
);
195
+
if (newGallery != null) {
196
+
profileNotifier.addGalleryToProfile(newGallery);
197
+
}
172
198
}
199
+
} else {
200
+
galleryUri = widget.gallery!.uri;
201
+
final galleryCache = container.read(galleryCacheProvider.notifier);
202
+
await galleryCache.updateGalleryDetails(
203
+
galleryUri: galleryUri,
204
+
title: _titleController.text.trim(),
205
+
description: _descController.text.trim(),
206
+
createdAt: widget.gallery!.createdAt ?? DateTime.now().toUtc().toIso8601String(),
207
+
);
173
208
}
174
-
} else {
175
-
galleryUri = widget.gallery!.uri;
176
-
final galleryCache = container.read(galleryCacheProvider.notifier);
177
-
await galleryCache.updateGalleryDetails(
178
-
galleryUri: galleryUri,
179
-
title: _titleController.text.trim(),
180
-
description: _descController.text.trim(),
181
-
createdAt: widget.gallery!.createdAt ?? DateTime.now().toUtc().toIso8601String(),
182
-
);
183
-
}
184
-
setState(() => _submitting = false);
185
-
if (mounted && galleryUri != null) {
186
-
FocusScope.of(context).unfocus(); // Force keyboard to close
187
-
Navigator.of(context).pop(galleryUri); // Pop with galleryUri if created
188
-
if (widget.gallery == null) {
189
-
Navigator.of(context).push(
190
-
MaterialPageRoute(
191
-
builder: (context) =>
192
-
GalleryPage(uri: galleryUri!, currentUserDid: apiService.currentUser?.did),
209
+
setState(() => _submitting = false);
210
+
if (mounted && galleryUri != null) {
211
+
FocusScope.of(context).unfocus(); // Force keyboard to close
212
+
Navigator.of(context).pop(galleryUri); // Pop with galleryUri if created
213
+
if (widget.gallery == null) {
214
+
Navigator.of(context).push(
215
+
MaterialPageRoute(
216
+
builder: (context) =>
217
+
GalleryPage(uri: galleryUri!, currentUserDid: apiService.currentUser?.did),
218
+
),
219
+
);
220
+
}
221
+
} else if (mounted) {
222
+
FocusScope.of(context).unfocus(); // Force keyboard to close
223
+
Navigator.of(context).pop();
224
+
}
225
+
} catch (e) {
226
+
setState(() {
227
+
_submitting = false;
228
+
_showUploadOverlay = false;
229
+
});
230
+
if (mounted) {
231
+
await showDialog(
232
+
context: context,
233
+
builder: (context) => AlertDialog(
234
+
title: const Text('Upload Failed'),
235
+
content: Text('An error occurred while uploading:\n\n${e.toString()}'),
236
+
actions: [
237
+
TextButton(child: const Text('OK'), onPressed: () => Navigator.of(context).pop()),
238
+
],
193
239
),
194
240
);
195
241
}
196
-
} else if (mounted) {
197
-
FocusScope.of(context).unfocus(); // Force keyboard to close
198
-
Navigator.of(context).pop();
199
242
}
200
243
}
201
244
···
203
246
Widget build(BuildContext context) {
204
247
final theme = Theme.of(context);
205
248
206
-
return CupertinoPageScaffold(
207
-
backgroundColor: theme.colorScheme.surface,
208
-
navigationBar: CupertinoNavigationBar(
209
-
backgroundColor: theme.colorScheme.surface,
210
-
border: Border(bottom: BorderSide(color: theme.dividerColor, width: 1)),
211
-
middle: Text(
212
-
widget.gallery == null ? 'New Gallery' : 'Edit Gallery',
213
-
style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
214
-
),
215
-
leading: CupertinoButton(
216
-
padding: EdgeInsets.zero,
217
-
onPressed: _submitting ? null : () => Navigator.of(context).pop(),
218
-
child: Text(
219
-
'Cancel',
220
-
style: TextStyle(color: theme.colorScheme.primary, fontWeight: FontWeight.w600),
221
-
),
222
-
),
223
-
trailing: CupertinoButton(
224
-
padding: EdgeInsets.zero,
225
-
onPressed: _submitting || _titleController.text.trim().isEmpty ? null : _submit,
226
-
child: Row(
227
-
mainAxisSize: MainAxisSize.min,
228
-
children: [
229
-
Text(
230
-
widget.gallery == null ? 'Create' : 'Save',
231
-
style: TextStyle(
232
-
color: (_submitting || _titleController.text.trim().isEmpty)
233
-
? theme.disabledColor
234
-
: theme.colorScheme.primary,
235
-
fontWeight: FontWeight.w600,
236
-
),
249
+
return Stack(
250
+
children: [
251
+
Positioned.fill(
252
+
child: CupertinoPageScaffold(
253
+
backgroundColor: theme.colorScheme.surface,
254
+
navigationBar: CupertinoNavigationBar(
255
+
backgroundColor: theme.colorScheme.surface,
256
+
border: Border(bottom: BorderSide(color: theme.dividerColor, width: 1)),
257
+
middle: Text(
258
+
widget.gallery == null ? 'New Gallery' : 'Edit Gallery',
259
+
style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
237
260
),
238
-
if (_submitting) ...[
239
-
const SizedBox(width: 8),
240
-
SizedBox(
241
-
width: 16,
242
-
height: 16,
243
-
child: CircularProgressIndicator(
244
-
strokeWidth: 2,
245
-
valueColor: AlwaysStoppedAnimation<Color>(theme.colorScheme.primary),
246
-
semanticsLabel: widget.gallery == null ? 'Creating' : 'Saving',
247
-
),
261
+
leading: CupertinoButton(
262
+
padding: EdgeInsets.zero,
263
+
onPressed: _submitting ? null : () => Navigator.of(context).pop(),
264
+
child: Text(
265
+
'Cancel',
266
+
style: TextStyle(color: theme.colorScheme.primary, fontWeight: FontWeight.w600),
248
267
),
249
-
],
250
-
],
251
-
),
252
-
),
253
-
),
254
-
child: SafeArea(
255
-
bottom: true,
256
-
child: SingleChildScrollView(
257
-
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
258
-
child: Column(
259
-
crossAxisAlignment: CrossAxisAlignment.start,
260
-
mainAxisSize: MainAxisSize.min,
261
-
children: [
262
-
PlainTextField(
263
-
label: 'Title',
264
-
controller: _titleController,
265
-
hintText: 'Enter a title',
266
268
),
267
-
Padding(
268
-
padding: const EdgeInsets.only(top: 4),
269
+
trailing: CupertinoButton(
270
+
padding: EdgeInsets.zero,
271
+
onPressed: _submitting || _titleController.text.trim().isEmpty ? null : _submit,
269
272
child: Row(
270
-
mainAxisAlignment: MainAxisAlignment.spaceBetween,
273
+
mainAxisSize: MainAxisSize.min,
271
274
children: [
272
-
const SizedBox(),
273
275
Text(
274
-
'${_titleController.text.characters.length}/100',
275
-
style: theme.textTheme.bodySmall?.copyWith(
276
-
color: _titleController.text.characters.length > 100
277
-
? theme.colorScheme.error
278
-
: theme.textTheme.bodySmall?.color,
276
+
widget.gallery == null ? 'Create' : 'Save',
277
+
style: TextStyle(
278
+
color: (_submitting || _titleController.text.trim().isEmpty)
279
+
? theme.disabledColor
280
+
: theme.colorScheme.primary,
281
+
fontWeight: FontWeight.w600,
279
282
),
280
283
),
284
+
if (_submitting) ...[
285
+
const SizedBox(width: 8),
286
+
SizedBox(
287
+
width: 16,
288
+
height: 16,
289
+
child: CircularProgressIndicator(
290
+
strokeWidth: 2,
291
+
valueColor: AlwaysStoppedAnimation<Color>(theme.colorScheme.primary),
292
+
semanticsLabel: widget.gallery == null ? 'Creating' : 'Saving',
293
+
),
294
+
),
295
+
],
281
296
],
282
297
),
283
298
),
284
-
const SizedBox(height: 16),
285
-
PlainTextField(
286
-
label: 'Description',
287
-
controller: _descController,
288
-
maxLines: 6,
289
-
hintText: 'Enter a description',
290
-
),
291
-
Padding(
292
-
padding: const EdgeInsets.only(top: 4),
293
-
child: Row(
294
-
mainAxisAlignment: MainAxisAlignment.spaceBetween,
299
+
),
300
+
child: SafeArea(
301
+
bottom: true,
302
+
child: SingleChildScrollView(
303
+
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
304
+
child: Column(
305
+
crossAxisAlignment: CrossAxisAlignment.start,
306
+
mainAxisSize: MainAxisSize.min,
295
307
children: [
296
-
const SizedBox(),
297
-
Text(
298
-
'${_descController.text.characters.length}/1000',
299
-
style: theme.textTheme.bodySmall?.copyWith(
300
-
color: _descController.text.characters.length > 1000
301
-
? theme.colorScheme.error
302
-
: theme.textTheme.bodySmall?.color,
303
-
),
308
+
PlainTextField(
309
+
label: 'Title',
310
+
controller: _titleController,
311
+
hintText: 'Enter a title',
304
312
),
305
-
],
306
-
),
307
-
),
308
-
const SizedBox(height: 16),
309
-
if (widget.gallery == null)
310
-
Row(
311
-
children: [
312
-
Expanded(
313
-
child: Text(
314
-
'Include image metadata (EXIF)',
315
-
style: theme.textTheme.bodyMedium,
313
+
Padding(
314
+
padding: const EdgeInsets.only(top: 4),
315
+
child: Row(
316
+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
317
+
children: [
318
+
const SizedBox(),
319
+
Text(
320
+
'${_titleController.text.characters.length}/100',
321
+
style: theme.textTheme.bodySmall?.copyWith(
322
+
color: _titleController.text.characters.length > 100
323
+
? theme.colorScheme.error
324
+
: theme.textTheme.bodySmall?.color,
325
+
),
326
+
),
327
+
],
316
328
),
317
329
),
318
-
Switch(
319
-
value: _includeExif,
320
-
onChanged: (val) {
321
-
setState(() {
322
-
_includeExif = val;
323
-
});
324
-
},
330
+
const SizedBox(height: 16),
331
+
PlainTextField(
332
+
label: 'Description',
333
+
controller: _descController,
334
+
maxLines: 6,
335
+
hintText: 'Enter a description',
325
336
),
326
-
],
327
-
),
328
-
const SizedBox(height: 16),
329
-
if (widget.gallery == null)
330
-
Row(
331
-
children: [
332
-
Expanded(
333
-
child: AppButton(
334
-
label: 'Upload photos',
335
-
onPressed: _pickImages,
336
-
icon: AppIcons.photoLibrary,
337
-
variant: AppButtonVariant.primary,
338
-
height: 40,
339
-
fontSize: 15,
340
-
borderRadius: 6,
337
+
Padding(
338
+
padding: const EdgeInsets.only(top: 4),
339
+
child: Row(
340
+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
341
+
children: [
342
+
const SizedBox(),
343
+
Text(
344
+
'${_descController.text.characters.length}/1000',
345
+
style: theme.textTheme.bodySmall?.copyWith(
346
+
color: _descController.text.characters.length > 1000
347
+
? theme.colorScheme.error
348
+
: theme.textTheme.bodySmall?.color,
349
+
),
350
+
),
351
+
],
341
352
),
342
353
),
343
-
],
344
-
),
345
-
if (_images.isNotEmpty) ...[
346
-
const SizedBox(height: 16),
347
-
GridView.builder(
348
-
shrinkWrap: true,
349
-
physics: const NeverScrollableScrollPhysics(),
350
-
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
351
-
crossAxisCount: 3,
352
-
crossAxisSpacing: 8,
353
-
mainAxisSpacing: 8,
354
-
),
355
-
itemCount: _images.length,
356
-
itemBuilder: (context, index) {
357
-
final galleryImage = _images[index];
358
-
return Stack(
359
-
children: [
360
-
Positioned.fill(
361
-
child: Container(
362
-
decoration: BoxDecoration(
363
-
color: theme.colorScheme.surfaceContainerHighest,
364
-
borderRadius: BorderRadius.circular(8),
354
+
const SizedBox(height: 16),
355
+
if (widget.gallery == null)
356
+
Row(
357
+
children: [
358
+
Expanded(
359
+
child: Text(
360
+
'Include image metadata (EXIF)',
361
+
style: theme.textTheme.bodyMedium,
365
362
),
366
-
child: ClipRRect(
367
-
borderRadius: BorderRadius.circular(8),
368
-
child: Image.file(File(galleryImage.file.path), fit: BoxFit.cover),
363
+
),
364
+
Switch(
365
+
value: _includeExif,
366
+
onChanged: (val) {
367
+
setState(() {
368
+
_includeExif = val;
369
+
});
370
+
},
371
+
),
372
+
],
373
+
),
374
+
const SizedBox(height: 16),
375
+
if (widget.gallery == null) ...[
376
+
Padding(
377
+
padding: const EdgeInsets.only(bottom: 8.0),
378
+
child: Text(
379
+
"You can add up to 10 photos when initially creating a gallery, but you can add more later on. Galleries can capture a moment in time or evolve as an ongoing collection.",
380
+
style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor),
381
+
),
382
+
),
383
+
Row(
384
+
children: [
385
+
Expanded(
386
+
child: AppButton(
387
+
label: 'Add photos (${_images.length}/10)',
388
+
onPressed: _pickImages,
389
+
icon: AppIcons.photoLibrary,
390
+
variant: AppButtonVariant.primary,
391
+
height: 40,
392
+
fontSize: 15,
393
+
borderRadius: 6,
369
394
),
370
395
),
396
+
],
397
+
),
398
+
],
399
+
if (_images.isNotEmpty) ...[
400
+
const SizedBox(height: 16),
401
+
GridView.builder(
402
+
shrinkWrap: true,
403
+
physics: const NeverScrollableScrollPhysics(),
404
+
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
405
+
crossAxisCount: 3,
406
+
crossAxisSpacing: 8,
407
+
mainAxisSpacing: 8,
371
408
),
372
-
if (galleryImage.isExisting)
373
-
Positioned(
374
-
left: 2,
375
-
top: 2,
376
-
child: Container(
377
-
padding: const EdgeInsets.all(2),
378
-
decoration: BoxDecoration(
379
-
color: theme.colorScheme.secondary.withOpacity(0.7),
380
-
borderRadius: BorderRadius.circular(4),
409
+
itemCount: _images.length,
410
+
itemBuilder: (context, index) {
411
+
final galleryImage = _images[index];
412
+
return Stack(
413
+
children: [
414
+
Positioned.fill(
415
+
child: Container(
416
+
decoration: BoxDecoration(
417
+
color: theme.colorScheme.surfaceContainerHighest,
418
+
borderRadius: BorderRadius.circular(8),
419
+
),
420
+
child: ClipRRect(
421
+
borderRadius: BorderRadius.circular(8),
422
+
child: Image.file(
423
+
File(galleryImage.file.path),
424
+
fit: BoxFit.cover,
425
+
),
426
+
),
427
+
),
381
428
),
382
-
child: Icon(
383
-
AppIcons.checkCircle,
384
-
color: theme.colorScheme.onSecondary,
385
-
size: 16,
429
+
if (galleryImage.isExisting)
430
+
Positioned(
431
+
left: 2,
432
+
top: 2,
433
+
child: Container(
434
+
padding: const EdgeInsets.all(2),
435
+
decoration: BoxDecoration(
436
+
color: theme.colorScheme.secondary.withOpacity(0.7),
437
+
borderRadius: BorderRadius.circular(4),
438
+
),
439
+
child: Icon(
440
+
AppIcons.checkCircle,
441
+
color: theme.colorScheme.onSecondary,
442
+
size: 16,
443
+
),
444
+
),
445
+
),
446
+
Positioned(
447
+
top: 2,
448
+
right: 2,
449
+
child: GestureDetector(
450
+
onTap: () => _removeImage(index),
451
+
child: Container(
452
+
decoration: BoxDecoration(
453
+
color: Colors.grey.withOpacity(0.7),
454
+
shape: BoxShape.circle,
455
+
),
456
+
padding: const EdgeInsets.all(4),
457
+
child: const Icon(
458
+
AppIcons.close,
459
+
color: Colors.white,
460
+
size: 20,
461
+
),
462
+
),
463
+
),
386
464
),
387
-
),
388
-
),
389
-
Positioned(
390
-
top: 2,
391
-
right: 2,
392
-
child: GestureDetector(
393
-
onTap: () => _removeImage(index),
394
-
child: Container(
395
-
decoration: BoxDecoration(
396
-
color: Colors.grey.withOpacity(0.7),
397
-
shape: BoxShape.circle,
398
-
),
399
-
padding: const EdgeInsets.all(4),
400
-
child: const Icon(AppIcons.close, color: Colors.white, size: 20),
401
-
),
402
-
),
403
-
),
404
-
],
405
-
);
406
-
},
465
+
],
466
+
);
467
+
},
468
+
),
469
+
],
470
+
],
407
471
),
408
-
],
409
-
],
472
+
),
473
+
),
410
474
),
411
475
),
412
-
),
476
+
if (_showUploadOverlay && widget.gallery == null)
477
+
UploadProgressOverlay(
478
+
images: _images.where((img) => !img.isExisting).toList(),
479
+
currentIndex: _currentUploadIndex,
480
+
progress: _currentUploadProgress,
481
+
visible: _showUploadOverlay,
482
+
),
483
+
],
413
484
);
414
485
}
415
486
}
+84
lib/widgets/upload_progress_overlay.dart
+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
+
}