From 008878676af1ac1df763a5c7d1d30d274adc9521 Mon Sep 17 00:00:00 2001 From: "Aamir.Muhammad" Date: Wed, 10 Jun 2026 16:00:12 +0300 Subject: [PATCH] Accessibility & Image Widgets Update --- .../profile_settings_view_model.dart | 22 ++ .../profile_settings/accessibility_page.dart | 200 ++++++++++++------ .../profile_settings/profile_settings.dart | 2 +- .../accessibility/preserve_image_colors.dart | 14 +- .../accessibility/preserve_svg_colors.dart | 14 +- 5 files changed, 164 insertions(+), 88 deletions(-) diff --git a/lib/features/profile_settings/profile_settings_view_model.dart b/lib/features/profile_settings/profile_settings_view_model.dart index aebdb849..c61665c3 100644 --- a/lib/features/profile_settings/profile_settings_view_model.dart +++ b/lib/features/profile_settings/profile_settings_view_model.dart @@ -13,6 +13,7 @@ import 'package:hmg_patient_app_new/services/error_handler_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:screen_brightness/screen_brightness.dart'; class ProfileSettingsViewModel extends ChangeNotifier { static const String _darkModeKey = 'is_dark_mode'; @@ -178,6 +179,10 @@ class ProfileSettingsViewModel extends ChangeNotifier { void loadAccessibilitySettings() { _fontSizeStep = _cacheService.getInt(key: _fontSizeStepKey) ?? 2; _visualMode = _cacheService.getString(key: _visualModeKey) ?? 'off'; + + // Apply brightness based on saved visual mode + _applyBrightnessForMode(_visualMode); + // No notifyListeners() here — we are called before build. } @@ -195,6 +200,23 @@ class ProfileSettingsViewModel extends ChangeNotifier { notifyListeners(); } + /// Apply screen brightness based on visual mode + void _applyBrightnessForMode(String mode) async { + try { + final screenBrightness = ScreenBrightness(); + + if (mode == 'dim') { + // Reduce brightness to 40% for dim mode + await screenBrightness.setScreenBrightness(0.4); + } else { + // For other modes, reset to system brightness + await screenBrightness.resetScreenBrightness(); + } + } catch (e) { + print('Error applying brightness for mode: $e'); + } + } + /// Get font scale multiplier based on current font size step double getFontScaleMultiplier() { switch (_fontSizeStep) { diff --git a/lib/presentation/profile_settings/accessibility_page.dart b/lib/presentation/profile_settings/accessibility_page.dart index b879c025..7fe9b61b 100644 --- a/lib/presentation/profile_settings/accessibility_page.dart +++ b/lib/presentation/profile_settings/accessibility_page.dart @@ -1,16 +1,20 @@ 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/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:provider/provider.dart'; +import 'package:screen_brightness/screen_brightness.dart'; enum VisualMode { off, invert, dim, highContrast } @@ -26,12 +30,16 @@ class _AccessibilityPageState extends State { int _currentFontSizeStep = 2; // 0: Small, 1: Small-Medium, 2: Medium (default), 3: Medium-Large, 4: Large VisualMode _selectedMode = VisualMode.off; + // Screen brightness control + final ScreenBrightness _screenBrightness = ScreenBrightness(); + @override void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { + WidgetsBinding.instance.addPostFrameCallback((_) async { if (mounted) { profileSettingsViewModel = Provider.of(context, listen: false); + // Load saved settings from ViewModel _loadSavedSettings(); } @@ -75,9 +83,32 @@ class _AccessibilityPageState extends State { @override void dispose() { + // Reset to system brightness when leaving accessibility page + // This ensures we don't keep the preview brightness when navigating away + _screenBrightness.resetScreenBrightness(); super.dispose(); } + /// Apply brightness based on visual mode + Future _applyBrightness(VisualMode mode) async { + try { + switch (mode) { + case VisualMode.dim: + // Reduce brightness to 40% for dim mode + await _screenBrightness.setScreenBrightness(0.4); + break; + case VisualMode.off: + case VisualMode.invert: + case VisualMode.highContrast: + // Reset to system brightness for other modes + await _screenBrightness.resetScreenBrightness(); + break; + } + } catch (e) { + print('Error setting brightness: $e'); + } + } + double _getFontSizeForStep(int step) { const double mediumSize = 40.0; // Medium is baseline switch (step) { @@ -120,24 +151,21 @@ class _AccessibilityPageState extends State { textColor: AppColors.whiteColor, fontSize: 16.f, isBold: true, - onPressed: () { + onPressed: () async { // Save accessibility settings to ViewModel profileSettingsViewModel.setFontSizeStep(_currentFontSizeStep); profileSettingsViewModel.setVisualMode(_visualModeToString(_selectedMode)); - // Show success message - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: LocaleKeys.settingsSavedSuccessfully.tr(context: context).toText14( - color: AppColors.whiteColor, - ), - backgroundColor: AppColors.successColor, - duration: const Duration(seconds: 2), - ), - ); + // Apply brightness changes based on selected mode + await _applyBrightness(_selectedMode); - // Pop the page - Navigator.pop(context); + // Show success bottom sheet + getIt().showSuccessBottomSheetWithoutH( + message: LocaleKeys.settingsSavedSuccessfully.tr(context: context), + onOkPressed: () { + Navigator.pop(context); + }, + ); }, ), ), @@ -243,9 +271,9 @@ class _AccessibilityPageState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - LocaleKeys.fontSizeSmall.tr(context: context).toText12(color: AppColors.greyTextColor), - LocaleKeys.fontSizeMedium.tr(context: context).toText12(color: AppColors.greyTextColor), - LocaleKeys.fontSizeLarge.tr(context: context).toText12(color: AppColors.greyTextColor), + LocaleKeys.fontSizeSmall.tr(context: context).toText12(color: AppColors.textColor, fontWeight: FontWeight.w600), + LocaleKeys.fontSizeMedium.tr(context: context).toText12(color: AppColors.textColor, fontWeight: FontWeight.w600), + LocaleKeys.fontSizeLarge.tr(context: context).toText12(color: AppColors.textColor, fontWeight: FontWeight.w600), ], ), ], @@ -338,10 +366,7 @@ class _AccessibilityPageState extends State { : null, ), SizedBox(width: 8.w), - label.toText14( - isBold: isSelected, - color: isSelected ? AppColors.blackBgColor : AppColors.greyTextColor, - ), + label.toText14(isBold: isSelected, color: isSelected ? AppColors.textColor : AppColors.textColor, weight: FontWeight.w600), ], ), ); @@ -349,6 +374,7 @@ class _AccessibilityPageState extends State { Widget _buildPreviewCard() { // Always build preview content with base "Off" mode colors + // Use raw widgets WITHOUT preservation wrappers to isolate from saved visual mode Widget previewContent = Container( padding: EdgeInsets.all(16.w), decoration: RoundedRectangleBorder().toSmoothCornerDecoration( @@ -377,9 +403,11 @@ class _AccessibilityPageState extends State { ), Transform.scale( scaleX: Utils.appState.isArabic() ? -1 : 1, - child: Utils.buildSvgWithAssets( - icon: AppAssets.arrow_forward, - iconColor: AppColors.blackBgColor, // Always use base color + child: SvgPicture.asset( + AppAssets.arrow_forward, + colorFilter: ColorFilter.mode(AppColors.blackBgColor, BlendMode.srcIn), + width: 24.w, + height: 24.h, ), ), ], @@ -394,41 +422,95 @@ class _AccessibilityPageState extends State { color: AppColors.blackBgColor, // Always use base color ), SizedBox(width: 4.w), - LocaleKeys.riyal.tr(context: context).toText14( - color: AppColors.blackBgColor, // Always use base color - ), - SizedBox(width: 4.w), - Utils.buildSvgWithAssets( - icon: AppAssets.saudi_riyal_icon, - iconColor: AppColors.blackBgColor, // Always use base color + SvgPicture.asset( + AppAssets.saudi_riyal_icon, + colorFilter: ColorFilter.mode(AppColors.blackBgColor, BlendMode.srcIn), width: 16.w, - height: 16.w, + height: 16.h, ), ], ), SizedBox(height: 16.h), - // Button - always show with base "Off" colors - CustomButton( + // Button - simple container without preservation wrappers + Container( height: 44.h, - icon: AppAssets.add_icon, - iconColor: AppColors.whiteColor, - // Always use base color - text: LocaleKeys.buttonText.tr(context: context), - backgroundColor: AppColors.primaryRedColor, - // Always use base color - borderColor: AppColors.primaryRedColor, - // Always use base color - textColor: AppColors.whiteColor, - // Always use base color - fontSize: 14.f, - onPressed: () {}, + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), + decoration: BoxDecoration( + color: AppColors.primaryRedColor, + borderRadius: BorderRadius.circular(12.r), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + AppAssets.add_icon, + colorFilter: ColorFilter.mode(AppColors.whiteColor, BlendMode.srcIn), + width: 20.w, + height: 20.h, + ), + SizedBox(width: 8.w), + LocaleKeys.buttonText.tr(context: context).toText14( + isBold: true, + color: AppColors.whiteColor, + ), + ], + ), ), ], ), ); - // Apply visual mode filters to preview + // First, cancel out the global app filter by applying reverse filter + Widget neutralizedContent = _cancelGlobalFilter(previewContent); + + // Then apply the selected mode filter on top of the neutralized content + return _applySelectedModeFilter(neutralizedContent); + } + + /// Cancel out the global app filter to get back to base colors + Widget _cancelGlobalFilter(Widget child) { + final savedMode = profileSettingsViewModel.visualMode; + + switch (savedMode) { + case 'invert': + // Global filter is invert, apply reverse (invert again) to get base colors + return ColorFiltered( + colorFilter: const ColorFilter.matrix([ + -1, 0, 0, 0, 255, + 0, -1, 0, 0, 255, + 0, 0, -1, 0, 255, + 0, 0, 0, 1, 0, + ]), + child: child, + ); + + case 'dim': + // Global filter has overlay, but we can't reverse an overlay + // Just return child as-is (dim affects whole screen anyway) + return child; + + case 'highContrast': + // Global filter is grayscale, apply saturation boost to approximate reverse + return ColorFiltered( + colorFilter: const ColorFilter.matrix([ + 1.5, 0, 0, 0, 0, + 0, 1.5, 0, 0, 0, + 0, 0, 1.5, 0, 0, + 0, 0, 0, 1, 0, + ]), + child: child, + ); + + case 'off': + default: + // No global filter, return as-is + return child; + } + } + + /// Apply the selected mode filter to the preview + Widget _applySelectedModeFilter(Widget child) { switch (_selectedMode) { case VisualMode.invert: return ColorFiltered( @@ -438,25 +520,13 @@ class _AccessibilityPageState extends State { 0, 0, -1, 0, 255, // Blue channel inverted 0, 0, 0, 1, 0, // Alpha channel unchanged ]), - child: previewContent, + child: child, ); case VisualMode.dim: - return Stack( - children: [ - previewContent, - Positioned.fill( - child: IgnorePointer( - child: Container( - decoration: BoxDecoration( - color: AppColors.blackBgColor.withValues(alpha: 0.3), - borderRadius: BorderRadius.circular(20.r), - ), - ), - ), - ), - ], - ); + // For dim mode, we control actual screen brightness + // No filter is applied - images remain unchanged + return child; case VisualMode.highContrast: return ColorFiltered( @@ -466,11 +536,11 @@ class _AccessibilityPageState extends State { 0.2126, 0.7152, 0.0722, 0, 0, // Blue channel (grayscale) 0, 0, 0, 1, 0, // Alpha channel unchanged ]), - child: previewContent, + child: child, ); case VisualMode.off: - return previewContent; + return child; } } } diff --git a/lib/presentation/profile_settings/profile_settings.dart b/lib/presentation/profile_settings/profile_settings.dart index ee48b95e..2e6b60b6 100644 --- a/lib/presentation/profile_settings/profile_settings.dart +++ b/lib/presentation/profile_settings/profile_settings.dart @@ -256,7 +256,7 @@ class ProfileSettingsState extends State { }, ), 1.divider, - actionItem(AppAssets.bell, LocaleKeys.accessibility.tr(context: context), () { + actionItem(AppAssets.accessibility, LocaleKeys.accessibility.tr(context: context), () { Navigator.of(context).push(CustomPageRoute(page: const AccessibilityPage())); }), ], diff --git a/lib/widgets/accessibility/preserve_image_colors.dart b/lib/widgets/accessibility/preserve_image_colors.dart index a1591a7d..0a7fef72 100644 --- a/lib/widgets/accessibility/preserve_image_colors.dart +++ b/lib/widgets/accessibility/preserve_image_colors.dart @@ -45,17 +45,9 @@ class PreserveImageColors extends StatelessWidget { ); case 'dim': - // Apply brightening to compensate for dimming - // Since parent has 30% black overlay, we increase brightness - return ColorFiltered( - colorFilter: const ColorFilter.matrix([ - 1.43, 0, 0, 0, 0, // Brighten red channel - 0, 1.43, 0, 0, 0, // Brighten green channel - 0, 0, 1.43, 0, 0, // Brighten blue channel - 0, 0, 0, 1, 0, // Alpha unchanged - ]), - child: child, - ); + // Dim mode uses screen brightness control only - no filters applied + // So no reverse filter needed + return child; case 'highContrast': // For high contrast, apply color saturation to restore colors diff --git a/lib/widgets/accessibility/preserve_svg_colors.dart b/lib/widgets/accessibility/preserve_svg_colors.dart index e9d720fe..1576504a 100644 --- a/lib/widgets/accessibility/preserve_svg_colors.dart +++ b/lib/widgets/accessibility/preserve_svg_colors.dart @@ -46,17 +46,9 @@ class PreserveSvgColors extends StatelessWidget { ); case 'dim': - // Apply brightening to compensate for dimming - // Since parent has 30% black overlay, we increase brightness - return ColorFiltered( - colorFilter: const ColorFilter.matrix([ - 1.43, 0, 0, 0, 0, // Brighten red channel - 0, 1.43, 0, 0, 0, // Brighten green channel - 0, 0, 1.43, 0, 0, // Brighten blue channel - 0, 0, 0, 1, 0, // Alpha unchanged - ]), - child: child, - ); + // Dim mode uses screen brightness control only - no filters applied + // So no reverse filter needed + return child; case 'highContrast': // For high contrast, apply color saturation to restore colors