diff --git a/assets/images/avatar_placeholder.png b/assets/images/avatar_placeholder.png new file mode 100644 index 00000000..5b64b5c4 Binary files /dev/null and b/assets/images/avatar_placeholder.png differ diff --git a/assets/images/image_placeholder.png b/assets/images/image_placeholder.png new file mode 100644 index 00000000..d5141740 Binary files /dev/null and b/assets/images/image_placeholder.png differ diff --git a/lib/core/constants/app_assets.dart b/lib/core/constants/app_assets.dart index 30bd2d30..b7398902 100644 --- a/lib/core/constants/app_assets.dart +++ b/lib/core/constants/app_assets.dart @@ -29,6 +29,8 @@ abstract final class AppAssets { static const imageFigure = 'figure'; static const imageLine = 'line'; static const imageLineVariant = 'line_variant'; + static const imagePlaceholder = 'assets/images/image_placeholder.png'; + static const avatarPlaceholder = 'assets/images/avatar_placeholder.png'; // Legal documents. static const legalPrivacyPolicy = 'assets/legal/privacy_policy.txt'; diff --git a/lib/features/profile/presentation/widgets/edit_profile_dialog.dart b/lib/features/profile/presentation/widgets/edit_profile_dialog.dart index 2140352a..1e9d7f8d 100644 --- a/lib/features/profile/presentation/widgets/edit_profile_dialog.dart +++ b/lib/features/profile/presentation/widgets/edit_profile_dialog.dart @@ -11,7 +11,6 @@ import '../../../../../uikit/buttons/button_state.dart'; import '../../../../../uikit/buttons/main_button.dart'; import '../../../../../uikit/buttons/secondary_button.dart'; import '../../../../../uikit/dialogs/app_feedback_dialog.dart'; -import '../../../../../uikit/images/network_image_widget.dart'; import '../../../../../uikit/themes/colors/app_color_theme.dart'; import '../../../../../uikit/themes/text/app_text_theme.dart'; import '../../../auth/domain/entities/user.dart'; @@ -19,6 +18,7 @@ import '../../../auth/presentation/validators/auth_validators.dart'; import '../../../auth/presentation/widgets/auth_text_field.dart'; import '../../domain/repositories/profile_repository.dart'; import '../cubits/update_profile_cubit.dart'; +import 'profile_avatar_image.dart'; import 'profile_dialog_shell.dart'; /// Opens the edit-profile dialog and returns the refreshed user on success. @@ -251,7 +251,7 @@ final class _AvatarPreview extends StatelessWidget { fit: BoxFit.cover, ); } - return NetworkImageWidget( + return ProfileAvatarImage( imageUrl: imageUrl ?? '', height: 296, ); diff --git a/lib/features/profile/presentation/widgets/profile_avatar_image.dart b/lib/features/profile/presentation/widgets/profile_avatar_image.dart new file mode 100644 index 00000000..db930aa0 --- /dev/null +++ b/lib/features/profile/presentation/widgets/profile_avatar_image.dart @@ -0,0 +1,76 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; + +import '../../../../../core/constants/app_assets.dart'; + +/// Profile-local network image widget with avatar-specific fallback artwork. +class ProfileAvatarImage extends StatelessWidget { + /// Remote image URL. + final String imageUrl; + + /// Height. + final double height; + + /// Optional width. + final double? width; + + /// Fit. + final BoxFit fit; + + /// Creates an instance of [ProfileAvatarImage]. + const ProfileAvatarImage({ + required this.imageUrl, + required this.height, + this.width, + this.fit = BoxFit.cover, + super.key, + }); + + @override + Widget build(BuildContext context) { + return CachedNetworkImage( + imageUrl: imageUrl, + height: height, + width: width, + fit: fit, + placeholder: (_, _) => _ProfileAvatarPlaceholder( + height: height, + width: width, + fit: fit, + ), + errorWidget: (_, _, _) => _ProfileAvatarPlaceholder( + height: height, + width: width, + fit: fit, + ), + ); + } +} + +final class _ProfileAvatarPlaceholder extends StatelessWidget { + final double height; + final double? width; + final BoxFit fit; + + const _ProfileAvatarPlaceholder({ + required this.height, + required this.width, + required this.fit, + }); + + @override + Widget build(BuildContext context) { + return Image.asset( + AppAssets.avatarPlaceholder, + height: height, + width: width, + fit: fit, + errorBuilder: (_, _, _) => const Center( + child: Icon( + Icons.image_not_supported_outlined, + size: 32, + ), + ), + ); + } +} diff --git a/lib/features/profile/presentation/widgets/user_section_widget.dart b/lib/features/profile/presentation/widgets/user_section_widget.dart index cf40f40e..b1ebeab4 100644 --- a/lib/features/profile/presentation/widgets/user_section_widget.dart +++ b/lib/features/profile/presentation/widgets/user_section_widget.dart @@ -4,10 +4,10 @@ import '../../../../../core/constants/app_strings.dart'; import '../../../../../uikit/buttons/main_button.dart'; import '../../../../../uikit/buttons/secondary_button.dart'; import '../../../../../uikit/cards/app_card.dart'; -import '../../../../../uikit/images/network_image_widget.dart'; import '../../../../../uikit/themes/colors/app_color_theme.dart'; import '../../../../../uikit/themes/text/app_text_theme.dart'; import '../../../auth/domain/entities/user.dart'; +import 'profile_avatar_image.dart'; /// The first profile section with the basic authenticated user data. class UserSectionWidget extends StatelessWidget { @@ -42,7 +42,7 @@ class UserSectionWidget extends StatelessWidget { child: ClipOval( child: SizedBox.square( dimension: 140, - child: NetworkImageWidget( + child: ProfileAvatarImage( imageUrl: user.avatar ?? '', height: 140, ), diff --git a/lib/uikit/images/network_image_widget.dart b/lib/uikit/images/network_image_widget.dart index cd923697..0864eda6 100644 --- a/lib/uikit/images/network_image_widget.dart +++ b/lib/uikit/images/network_image_widget.dart @@ -1,7 +1,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; -import '../themes/colors/app_color_theme.dart'; +import '../../core/constants/app_assets.dart'; /// Widget for displaying cached network images. class NetworkImageWidget extends StatelessWidget { @@ -26,26 +26,24 @@ class NetworkImageWidget extends StatelessWidget { imageUrl: imageUrl, fit: BoxFit.cover, placeholder: (_, _) => const _ImagePlaceholder(), - errorWidget: (_, _, _) => const _ImagePlaceholder( - icon: Icons.image_not_supported_outlined, - ), + errorWidget: (_, _, _) => const _ImagePlaceholder(), ), ); } } class _ImagePlaceholder extends StatelessWidget { - final IconData icon; - const _ImagePlaceholder({this.icon = Icons.image_outlined}); + const _ImagePlaceholder(); @override Widget build(BuildContext context) { - return SizedBox( - child: Center( + return Image.asset( + AppAssets.imagePlaceholder, + fit: BoxFit.cover, + errorBuilder: (_, _, _) => const Center( child: Icon( - icon, + Icons.image_not_supported_outlined, size: 64, - color: AppColorTheme.of(context).disabled, ), ), );