Enhance avatar upload functionality with progress tracking in AccountSettingsDialog

This commit is contained in:
Leon Bösche
2026-01-29 04:03:48 +01:00
parent b55d277406
commit 5a62121591
2 changed files with 79 additions and 33 deletions

View File

@@ -208,8 +208,9 @@ class ApiClient {
// Avatar upload
Future<Map<String, dynamic>> uploadAvatar(
List<int> imageBytes,
String filename,
) async {
String filename, {
ProgressCallback? onSendProgress,
}) async {
final formData = FormData.fromMap({
'avatar': MultipartFile.fromBytes(
imageBytes,
@@ -219,7 +220,11 @@ class ApiClient {
});
try {
final response = await _dio.post('/user/avatar', data: formData);
final response = await _dio.post(
'/user/avatar',
data: formData,
onSendProgress: onSendProgress,
);
return response.data;
} on DioException catch (e) {
throw _handleError(e);

View File

@@ -21,6 +21,8 @@ class AccountSettingsDialog extends StatefulWidget {
class _AccountSettingsDialogState extends State<AccountSettingsDialog> {
int _selectedTabIndex = 0;
bool _isLoading = false;
bool _isUploadingAvatar = false;
double _avatarUploadProgress = 0.0;
String? _error;
// Profile fields
@@ -78,16 +80,28 @@ class _AccountSettingsDialogState extends State<AccountSettingsDialog> {
final filename = file.name;
setState(() {
_isLoading = true;
_isUploadingAvatar = true;
_avatarUploadProgress = 0.0;
});
try {
final apiClient = GetIt.I<ApiClient>();
final response = await apiClient.uploadAvatar(bytes, filename);
final response = await apiClient.uploadAvatar(
bytes,
filename,
onSendProgress: (sent, total) {
if (total != -1 && mounted) {
setState(() {
_avatarUploadProgress = sent / total;
});
}
},
);
setState(() {
_avatarUrl = response['avatarUrl'] as String?;
_isLoading = false;
_isUploadingAvatar = false;
_avatarUploadProgress = 0.0;
});
if (mounted) {
@@ -96,7 +110,10 @@ class _AccountSettingsDialogState extends State<AccountSettingsDialog> {
);
}
} catch (e) {
setState(() => _isLoading = false);
setState(() {
_isUploadingAvatar = false;
_avatarUploadProgress = 0.0;
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to upload avatar: $e')),
@@ -510,38 +527,62 @@ class _AccountSettingsDialogState extends State<AccountSettingsDialog> {
children: [
const SizedBox(height: 48),
GestureDetector(
onTap: _pickAndUploadAvatar,
child: CircleAvatar(
radius: 50,
backgroundColor: AppTheme.secondaryText.withValues(
alpha: 0.2,
),
child: _avatarUrl != null
? ClipOval(
child: Image.network(
_avatarUrl!,
fit: BoxFit.cover,
width: 100,
height: 100,
errorBuilder: (context, error, stackTrace) {
return Icon(
Icons.person,
size: 50,
color: AppTheme.secondaryText,
);
},
onTap: _isUploadingAvatar ? null : _pickAndUploadAvatar,
child: Stack(
alignment: Alignment.center,
children: [
CircleAvatar(
radius: 50,
backgroundColor: AppTheme.secondaryText.withValues(
alpha: 0.2,
),
child: _avatarUrl != null
? ClipOval(
child: Image.network(
_avatarUrl!,
fit: BoxFit.cover,
width: 100,
height: 100,
errorBuilder:
(context, error, stackTrace) {
return Icon(
Icons.person,
size: 50,
color: AppTheme.secondaryText,
);
},
),
)
: Icon(
Icons.person,
size: 50,
color: AppTheme.secondaryText,
),
),
if (_isUploadingAvatar)
SizedBox(
width: 110,
height: 110,
child: CircularProgressIndicator(
value: _avatarUploadProgress > 0
? _avatarUploadProgress
: null,
strokeWidth: 4,
valueColor: AlwaysStoppedAnimation<Color>(
AppTheme.accentColor,
),
)
: Icon(
Icons.person,
size: 50,
color: AppTheme.secondaryText,
backgroundColor: AppTheme.secondaryText
.withValues(alpha: 0.2),
),
),
],
),
),
const SizedBox(height: 8),
Text(
'Tap to change avatar',
_isUploadingAvatar
? 'Uploading avatar...'
: 'Tap to change avatar',
style: TextStyle(
color: AppTheme.secondaryText,
fontSize: 12,