Enhance avatar upload functionality with progress tracking in AccountSettingsDialog
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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,8 +527,11 @@ 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(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
radius: 50,
|
radius: 50,
|
||||||
backgroundColor: AppTheme.secondaryText.withValues(
|
backgroundColor: AppTheme.secondaryText.withValues(
|
||||||
alpha: 0.2,
|
alpha: 0.2,
|
||||||
@@ -523,7 +543,8 @@ class _AccountSettingsDialogState extends State<AccountSettingsDialog> {
|
|||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 100,
|
height: 100,
|
||||||
errorBuilder: (context, error, stackTrace) {
|
errorBuilder:
|
||||||
|
(context, error, stackTrace) {
|
||||||
return Icon(
|
return Icon(
|
||||||
Icons.person,
|
Icons.person,
|
||||||
size: 50,
|
size: 50,
|
||||||
@@ -538,10 +559,30 @@ class _AccountSettingsDialogState extends State<AccountSettingsDialog> {
|
|||||||
color: AppTheme.secondaryText,
|
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
|
||||||
|
.withValues(alpha: 0.2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
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,
|
||||||
|
|||||||
Reference in New Issue
Block a user