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

View File

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