From a0c907b39ce98d7073c943a47def7359cc53feb2 Mon Sep 17 00:00:00 2001 From: FaizHashmi Date: Sun, 1 Nov 2026 12:14:51 +0300 Subject: [PATCH] Added Arabic banner support and onTap support --- .../occasion_theme/occasion_config_model.dart | 40 +++++++++++++++---- .../occasion_theme_view_model.dart | 12 +++++- .../home/data/landing_page_data.dart | 4 +- lib/presentation/home/landing_page.dart | 5 ++- .../home/widgets/occasion_banner_widget.dart | 23 +++++++---- .../profile_settings/profile_settings.dart | 2 +- 6 files changed, 66 insertions(+), 20 deletions(-) diff --git a/lib/features/occasion_theme/occasion_config_model.dart b/lib/features/occasion_theme/occasion_config_model.dart index 26ddf733..53f8701d 100644 --- a/lib/features/occasion_theme/occasion_config_model.dart +++ b/lib/features/occasion_theme/occasion_config_model.dart @@ -39,8 +39,21 @@ class OccasionConfigModel { /// Remote (or bundled asset path) image rendered as the occasion's main /// header/banner (e.g. the National Day greeting banner), drawn at full - /// opacity on top of [backgroundOpacityImage]. - final String? headerImage; + /// opacity on top of [backgroundOpacityImage]. Language-specific — pick + /// [headerImageEn] or [headerImageAr] based on the app's current locale + /// (the model itself doesn't know about locale). + /// + /// Note: the remote JSON also still publishes a legacy `header_image` key + /// (mirrored to the English image) purely so old app versions that only + /// know that single key keep showing *a* banner until they update — this + /// model intentionally does not read that legacy key at all. + final String? headerImageEn; + final String? headerImageAr; + + /// Redirection link opened when the corresponding header banner is + /// tapped. Null/empty means "do nothing" — the tap has no effect. + final String? headerImageLinkEn; + final String? headerImageLinkAr; const OccasionConfigModel({ required this.key, @@ -50,7 +63,10 @@ class OccasionConfigModel { this.primaryColorHex, this.backgroundOpacityImage, this.backgroundImageOpacity = 1.0, - this.headerImage, + this.headerImageEn, + this.headerImageAr, + this.headerImageLinkEn, + this.headerImageLinkAr, }); /// Fully inert config — identical to the app's current/default look. @@ -62,20 +78,25 @@ class OccasionConfigModel { primaryColorHex: null, backgroundOpacityImage: null, backgroundImageOpacity: 1.0, - headerImage: null, + headerImageEn: null, + headerImageAr: null, + headerImageLinkEn: null, + headerImageLinkAr: null, ); factory OccasionConfigModel.fromJson(Map json) { return OccasionConfigModel( key: (json['key'] as String?) ?? 'none', enabled: (json['enabled'] as bool?) ?? false, - // enabled: true, startDate: _parseDate(json['start_date']), endDate: _parseDate(json['end_date']), primaryColorHex: json['primary_color'] as String?, backgroundOpacityImage: json['background_opacity_image'] as String?, backgroundImageOpacity: (json['background_image_opacity'] as num?)?.toDouble() ?? 1.0, - headerImage: json['header_image'] as String?, + headerImageEn: json['header_image_en'] as String?, + headerImageAr: json['header_image_ar'] as String?, + headerImageLinkEn: json['header_image_link_en'] as String?, + headerImageLinkAr: json['header_image_link_ar'] as String?, ); } @@ -142,6 +163,11 @@ class OccasionConfigModel { 'primary_color': primaryColorHex, 'background_opacity_image': backgroundOpacityImage, 'background_image_opacity': backgroundImageOpacity, - 'header_image': headerImage, + // Legacy key for old app versions — see [headerImageEn] doc comment. + 'header_image': headerImageEn, + 'header_image_en': headerImageEn, + 'header_image_ar': headerImageAr, + 'header_image_link_en': headerImageLinkEn, + 'header_image_link_ar': headerImageLinkAr, }; } diff --git a/lib/features/occasion_theme/occasion_theme_view_model.dart b/lib/features/occasion_theme/occasion_theme_view_model.dart index 73f94ec7..210a6882 100644 --- a/lib/features/occasion_theme/occasion_theme_view_model.dart +++ b/lib/features/occasion_theme/occasion_theme_view_model.dart @@ -53,7 +53,15 @@ class OccasionThemeViewModel extends ChangeNotifier { /// The occasion's main header/banner image (e.g. National Day greeting /// banner), rendered at full opacity. Null unless an occasion is active. - String? get headerImage => isOccasionActive ? _config.headerImage : null; + /// Language-specific — the widget picks [headerImageEn] or + /// [headerImageAr] based on the app's current locale. + String? get headerImageEn => isOccasionActive ? _config.headerImageEn : null; + String? get headerImageAr => isOccasionActive ? _config.headerImageAr : null; + + /// Redirection link for the corresponding header banner — null/empty + /// means tapping the banner does nothing. + String? get headerImageLinkEn => isOccasionActive ? _config.headerImageLinkEn : null; + String? get headerImageLinkAr => isOccasionActive ? _config.headerImageLinkAr : null; /// Lighter/translucent variant of the active occasion's primary color /// (11% opacity) — e.g. for chip/badge backgrounds. Null unless an @@ -94,7 +102,7 @@ class OccasionThemeViewModel extends ChangeNotifier { notifyListeners(); } catch (ex) { _loggerService.logError('OccasionThemeViewModel.refresh error: $ex'); - // Keep whatever config was already applied (cached/disabled) — never + // Keep whatever coFnfig was already applied (cached/disabled) — never // let a remote-config failure change the app's appearance unexpectedly. } } diff --git a/lib/presentation/home/data/landing_page_data.dart b/lib/presentation/home/data/landing_page_data.dart index 026c1725..b9069bd6 100644 --- a/lib/presentation/home/data/landing_page_data.dart +++ b/lib/presentation/home/data/landing_page_data.dart @@ -14,7 +14,7 @@ class LandingPageData { icon: AppAssets.emergency_services_icon, title: getIt.get().isArabic() ? LocaleKeys.services3 : LocaleKeys.emergency2, subtitle: getIt.get().isArabic() ? LocaleKeys.emergency2 : LocaleKeys.services3, - backgroundColor: AppColors.primaryRedColorBase, + backgroundColor: AppColors.primaryRedColor, iconColor: Colors.white, textColor: Colors.white, isBold: true, @@ -77,7 +77,7 @@ class LandingPageData { icon: AppAssets.emergency_services_icon, title: getIt.get().isArabic() ? LocaleKeys.services3 : LocaleKeys.emergency2, subtitle: getIt.get().isArabic() ? LocaleKeys.emergency2 : LocaleKeys.services3, - backgroundColor: AppColors.primaryRedColorBase, + backgroundColor: AppColors.primaryRedColor, iconColor: Colors.white, textColor: Colors.white, isBold: true, diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index b8cb01a5..d2e2737e 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -234,9 +234,12 @@ class _LandingPageState extends State { // (no occasion, or occasion active but no banner configured) the // page behaves exactly as it always has — banner (if any) scrolls // inline with the rest of the content. - final hasStickyBanner = context.watch().headerImage?.isNotEmpty ?? false; + final occasionThemeVmForBanner = context.watch(); + final hasStickyBanner = + (occasionThemeVmForBanner.headerImageEn?.isNotEmpty ?? false) || (occasionThemeVmForBanner.headerImageAr?.isNotEmpty ?? false); log("hasStickyBanner: $hasStickyBanner"); + log("occasionThemeVmForBanner.headerImageAr?.isNotEmpty: ${occasionThemeVmForBanner.headerImageAr?.isNotEmpty}"); return Stack( children: [ // Renders nothing unless a remote-config occasion theme (e.g. diff --git a/lib/presentation/home/widgets/occasion_banner_widget.dart b/lib/presentation/home/widgets/occasion_banner_widget.dart index 1f19b826..89429d35 100644 --- a/lib/presentation/home/widgets/occasion_banner_widget.dart +++ b/lib/presentation/home/widgets/occasion_banner_widget.dart @@ -1,16 +1,20 @@ -import 'dart:developer'; - import 'package:cached_network_image/cached_network_image.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/occasion_theme/occasion_theme_view_model.dart'; import 'package:provider/provider.dart'; -/// Renders the occasion's main header/banner (`header_image`, e.g. the -/// National Day greeting banner) inline at the top of the landing page's -/// scrollable content, drawn at full opacity. +/// Renders the occasion's main header/banner (`header_image_en`/ +/// `header_image_ar`, e.g. the National Day greeting banner) inline at the +/// top of the landing page's scrollable content, drawn at full opacity. +/// Picks the English or Arabic image based on the app's current locale. +/// Tapping it opens `header_image_link_en`/`header_image_link_ar` (the +/// matching language's link) via [Utils.openWebView] — if that link is +/// null/empty, the tap does nothing. /// /// Fully data-driven: when no occasion is active (the default), this widget /// renders nothing and takes zero space — the existing layout/UI is @@ -25,14 +29,19 @@ class OccasionBannerWidget extends StatelessWidget { Widget build(BuildContext context) { return Consumer( builder: (context, occasionThemeVm, _) { - final headerImageUrl = occasionThemeVm.headerImage; + final isArabic = context.locale.languageCode == 'ar'; + final headerImageUrl = isArabic ? occasionThemeVm.headerImageAr : occasionThemeVm.headerImageEn; + final headerLinkUrl = isArabic ? occasionThemeVm.headerImageLinkAr : occasionThemeVm.headerImageLinkEn; final hasHeaderImage = occasionThemeVm.isOccasionActive && (headerImageUrl?.isNotEmpty ?? false); if (!hasHeaderImage) { return const SizedBox(height: 0); } - return buildOccasionImage(headerImageUrl!).paddingOnly(bottom: 16.h); + return GestureDetector( + onTap: (headerLinkUrl?.isNotEmpty ?? false) ? () => Utils.openWebView(url: headerLinkUrl!) : null, + child: buildOccasionImage(headerImageUrl!).paddingOnly(bottom: 16.h), + ); }, ); } diff --git a/lib/presentation/profile_settings/profile_settings.dart b/lib/presentation/profile_settings/profile_settings.dart index ce300c9b..9e5d12b5 100644 --- a/lib/presentation/profile_settings/profile_settings.dart +++ b/lib/presentation/profile_settings/profile_settings.dart @@ -758,7 +758,7 @@ class _FamilyCardWidgetState extends State { height: isFoldable ? 50.h : 45.h, fontSize: 14.f, ).paddingOnly(top: 12.h, right: 16.w, left: 16.w, bottom: 16.h); - } + } Widget _buildNonParentUserButton(int? superUserId) { final canSwitchBack = superUserId != null && superUserId == widget.profile.responseId; -- 2.30.2