Added Arabic banner support and onTap support

national_day_theme
FaizHashmi 15 hours ago
parent 910bd20d42
commit a0c907b39c

@ -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<String, dynamic> 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,
};
}

@ -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.
}
}

@ -14,7 +14,7 @@ class LandingPageData {
icon: AppAssets.emergency_services_icon,
title: getIt.get<AppState>().isArabic() ? LocaleKeys.services3 : LocaleKeys.emergency2,
subtitle: getIt.get<AppState>().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<AppState>().isArabic() ? LocaleKeys.services3 : LocaleKeys.emergency2,
subtitle: getIt.get<AppState>().isArabic() ? LocaleKeys.emergency2 : LocaleKeys.services3,
backgroundColor: AppColors.primaryRedColorBase,
backgroundColor: AppColors.primaryRedColor,
iconColor: Colors.white,
textColor: Colors.white,
isBold: true,

@ -234,9 +234,12 @@ class _LandingPageState extends State<LandingPage> {
// (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<OccasionThemeViewModel>().headerImage?.isNotEmpty ?? false;
final occasionThemeVmForBanner = context.watch<OccasionThemeViewModel>();
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.

@ -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<OccasionThemeViewModel>(
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),
);
},
);
}

@ -758,7 +758,7 @@ class _FamilyCardWidgetState extends State<FamilyCardWidget> {
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;

Loading…
Cancel
Save