mobile bluesky app made with flutter
lazurite.stormlightlabs.org/
mobile
bluesky
flutter
1import 'dart:io';
2
3import 'package:flutter/material.dart';
4
5import '../../domain/draft.dart';
6
7/// Displays upload progress for a single media attachment.
8class UploadProgressTile extends StatelessWidget {
9 const UploadProgressTile({
10 required this.filename,
11 required this.status,
12 this.thumbnailPath,
13 this.progress = 0.0,
14 this.onRetry,
15 this.onCancel,
16 super.key,
17 });
18
19 /// Display filename for the media.
20 final String filename;
21
22 /// Local path for thumbnail preview.
23 final String? thumbnailPath;
24
25 /// Current upload status.
26 final DraftMediaStatus status;
27
28 /// Upload progress from 0.0 to 1.0.
29 final double progress;
30
31 /// Callback for retry action on failed uploads.
32 final VoidCallback? onRetry;
33
34 /// Callback for cancel action during uploads.
35 final VoidCallback? onCancel;
36
37 @override
38 Widget build(BuildContext context) {
39 final theme = Theme.of(context);
40 final colorScheme = theme.colorScheme;
41
42 return Padding(
43 padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
44 child: Row(
45 children: [
46 ClipRRect(
47 borderRadius: BorderRadius.circular(4),
48 child: SizedBox(
49 width: 48,
50 height: 48,
51 child: thumbnailPath != null
52 ? Image.file(
53 File(thumbnailPath!),
54 fit: BoxFit.cover,
55 errorBuilder: (_, _, _) => _PlaceholderThumbnail(colorScheme: colorScheme),
56 )
57 : _PlaceholderThumbnail(colorScheme: colorScheme),
58 ),
59 ),
60 const SizedBox(width: 12),
61 Expanded(
62 child: Column(
63 crossAxisAlignment: CrossAxisAlignment.start,
64 mainAxisSize: MainAxisSize.min,
65 children: [
66 Text(
67 filename,
68 style: theme.textTheme.bodyMedium,
69 maxLines: 1,
70 overflow: TextOverflow.ellipsis,
71 ),
72 const SizedBox(height: 4),
73 if (status == DraftMediaStatus.uploading)
74 LinearProgressIndicator(
75 value: progress,
76 backgroundColor: colorScheme.surfaceContainerHighest,
77 color: colorScheme.primary,
78 )
79 else
80 _StatusLabel(status: status, colorScheme: colorScheme),
81 ],
82 ),
83 ),
84 const SizedBox(width: 8),
85 _StatusAction(
86 status: status,
87 colorScheme: colorScheme,
88 onRetry: onRetry,
89 onCancel: onCancel,
90 ),
91 ],
92 ),
93 );
94 }
95}
96
97class _PlaceholderThumbnail extends StatelessWidget {
98 const _PlaceholderThumbnail({required this.colorScheme});
99
100 final ColorScheme colorScheme;
101
102 @override
103 Widget build(BuildContext context) {
104 return Container(
105 color: colorScheme.surfaceContainerHighest,
106 child: Icon(Icons.image, color: colorScheme.onSurfaceVariant, size: 24),
107 );
108 }
109}
110
111class _StatusLabel extends StatelessWidget {
112 const _StatusLabel({required this.status, required this.colorScheme});
113
114 final DraftMediaStatus status;
115 final ColorScheme colorScheme;
116
117 @override
118 Widget build(BuildContext context) {
119 final (text, color) = switch (status) {
120 DraftMediaStatus.pending => ('Pending', colorScheme.outline),
121 DraftMediaStatus.uploading => ('Uploading', colorScheme.primary),
122 DraftMediaStatus.uploaded => ('Uploaded', Colors.green),
123 DraftMediaStatus.failed => ('Failed', colorScheme.error),
124 };
125
126 return Text(text, style: Theme.of(context).textTheme.bodySmall?.copyWith(color: color));
127 }
128}
129
130class _StatusAction extends StatelessWidget {
131 const _StatusAction({
132 required this.status,
133 required this.colorScheme,
134 this.onRetry,
135 this.onCancel,
136 });
137
138 final DraftMediaStatus status;
139 final ColorScheme colorScheme;
140 final VoidCallback? onRetry;
141 final VoidCallback? onCancel;
142
143 @override
144 Widget build(BuildContext context) {
145 return switch (status) {
146 DraftMediaStatus.pending => Icon(
147 Icons.hourglass_empty,
148 color: colorScheme.outline,
149 size: 20,
150 ),
151 DraftMediaStatus.uploading => IconButton(
152 onPressed: onCancel,
153 icon: Icon(Icons.close, color: colorScheme.outline),
154 iconSize: 20,
155 padding: EdgeInsets.zero,
156 constraints: const BoxConstraints(),
157 tooltip: 'Cancel upload',
158 ),
159 DraftMediaStatus.uploaded => const Icon(Icons.check_circle, color: Colors.green, size: 20),
160 DraftMediaStatus.failed => IconButton(
161 onPressed: onRetry,
162 icon: Icon(Icons.refresh, color: colorScheme.error),
163 iconSize: 20,
164 padding: EdgeInsets.zero,
165 constraints: const BoxConstraints(),
166 tooltip: 'Retry upload',
167 ),
168 };
169 }
170}