From 6e18f7b4804ccdaca206f11e301bc20cd8a82420 Mon Sep 17 00:00:00 2001 From: CowboyGH Date: Sat, 4 Apr 2026 14:13:07 +0700 Subject: [PATCH] feat(ui): use branded placeholders for images and profile avatars --- assets/images/avatar_placeholder.png | Bin 0 -> 5789 bytes assets/images/image_placeholder.png | Bin 0 -> 3694 bytes lib/core/constants/app_assets.dart | 2 + .../widgets/edit_profile_dialog.dart | 4 +- .../widgets/profile_avatar_image.dart | 76 ++++++++++++++++++ .../widgets/user_section_widget.dart | 4 +- lib/uikit/images/network_image_widget.dart | 18 ++--- 7 files changed, 90 insertions(+), 14 deletions(-) create mode 100644 assets/images/avatar_placeholder.png create mode 100644 assets/images/image_placeholder.png create mode 100644 lib/features/profile/presentation/widgets/profile_avatar_image.dart diff --git a/assets/images/avatar_placeholder.png b/assets/images/avatar_placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..5b64b5c45dfbfb8eb6306315715d6d8ac8ccc037 GIT binary patch literal 5789 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 aps3%(T)4ds=D_bD=#c0VcHD@{i literal 0 HcmV?d00001 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, ), ), );