diff --git a/b0esche_cloud/lib/services/api_client.dart b/b0esche_cloud/lib/services/api_client.dart index b255bcc..026f086 100644 --- a/b0esche_cloud/lib/services/api_client.dart +++ b/b0esche_cloud/lib/services/api_client.dart @@ -208,8 +208,9 @@ class ApiClient { // Avatar upload Future> uploadAvatar( List 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); diff --git a/b0esche_cloud/lib/widgets/account_settings_dialog.dart b/b0esche_cloud/lib/widgets/account_settings_dialog.dart index 3365616..3ca05a2 100644 --- a/b0esche_cloud/lib/widgets/account_settings_dialog.dart +++ b/b0esche_cloud/lib/widgets/account_settings_dialog.dart @@ -21,6 +21,8 @@ class AccountSettingsDialog extends StatefulWidget { class _AccountSettingsDialogState extends State { 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 { final filename = file.name; setState(() { - _isLoading = true; + _isUploadingAvatar = true; + _avatarUploadProgress = 0.0; }); try { final apiClient = GetIt.I(); - 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 { ); } } 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 { 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( + 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,