Merge pull request 'Accessibility' (#330) from dev_aamir into master

Reviewed-on: https://34.17.182.140/Haroon6138/HMG_Patient_App_New/pulls/330
master
Haroon6138 2 weeks ago
commit 266911b365

@ -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/services/navigation_service.dart';
import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'package:screen_brightness/screen_brightness.dart';
class ProfileSettingsViewModel extends ChangeNotifier { class ProfileSettingsViewModel extends ChangeNotifier {
static const String _darkModeKey = 'is_dark_mode'; static const String _darkModeKey = 'is_dark_mode';
@ -178,6 +179,10 @@ class ProfileSettingsViewModel extends ChangeNotifier {
void loadAccessibilitySettings() { void loadAccessibilitySettings() {
_fontSizeStep = _cacheService.getInt(key: _fontSizeStepKey) ?? 2; _fontSizeStep = _cacheService.getInt(key: _fontSizeStepKey) ?? 2;
_visualMode = _cacheService.getString(key: _visualModeKey) ?? 'off'; _visualMode = _cacheService.getString(key: _visualModeKey) ?? 'off';
// Apply brightness based on saved visual mode
_applyBrightnessForMode(_visualMode);
// No notifyListeners() here we are called before build. // No notifyListeners() here we are called before build.
} }
@ -195,6 +200,23 @@ class ProfileSettingsViewModel extends ChangeNotifier {
notifyListeners(); 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 /// Get font scale multiplier based on current font size step
double getFontScaleMultiplier() { double getFontScaleMultiplier() {
switch (_fontSizeStep) { switch (_fontSizeStep) {

@ -1,16 +1,20 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.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_assets.dart';
import 'package:hmg_patient_app_new/core/app_export.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/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.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/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.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/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/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.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:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:screen_brightness/screen_brightness.dart';
enum VisualMode { off, invert, dim, highContrast } enum VisualMode { off, invert, dim, highContrast }
@ -26,12 +30,16 @@ class _AccessibilityPageState extends State<AccessibilityPage> {
int _currentFontSizeStep = 2; // 0: Small, 1: Small-Medium, 2: Medium (default), 3: Medium-Large, 4: Large int _currentFontSizeStep = 2; // 0: Small, 1: Small-Medium, 2: Medium (default), 3: Medium-Large, 4: Large
VisualMode _selectedMode = VisualMode.off; VisualMode _selectedMode = VisualMode.off;
// Screen brightness control
final ScreenBrightness _screenBrightness = ScreenBrightness();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) async {
if (mounted) { if (mounted) {
profileSettingsViewModel = Provider.of<ProfileSettingsViewModel>(context, listen: false); profileSettingsViewModel = Provider.of<ProfileSettingsViewModel>(context, listen: false);
// Load saved settings from ViewModel // Load saved settings from ViewModel
_loadSavedSettings(); _loadSavedSettings();
} }
@ -75,9 +83,32 @@ class _AccessibilityPageState extends State<AccessibilityPage> {
@override @override
void dispose() { 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(); super.dispose();
} }
/// Apply brightness based on visual mode
Future<void> _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) { double _getFontSizeForStep(int step) {
const double mediumSize = 40.0; // Medium is baseline const double mediumSize = 40.0; // Medium is baseline
switch (step) { switch (step) {
@ -120,24 +151,21 @@ class _AccessibilityPageState extends State<AccessibilityPage> {
textColor: AppColors.whiteColor, textColor: AppColors.whiteColor,
fontSize: 16.f, fontSize: 16.f,
isBold: true, isBold: true,
onPressed: () { onPressed: () async {
// Save accessibility settings to ViewModel // Save accessibility settings to ViewModel
profileSettingsViewModel.setFontSizeStep(_currentFontSizeStep); profileSettingsViewModel.setFontSizeStep(_currentFontSizeStep);
profileSettingsViewModel.setVisualMode(_visualModeToString(_selectedMode)); profileSettingsViewModel.setVisualMode(_visualModeToString(_selectedMode));
// Show success message // Apply brightness changes based on selected mode
ScaffoldMessenger.of(context).showSnackBar( await _applyBrightness(_selectedMode);
SnackBar(
content: LocaleKeys.settingsSavedSuccessfully.tr(context: context).toText14(
color: AppColors.whiteColor,
),
backgroundColor: AppColors.successColor,
duration: const Duration(seconds: 2),
),
);
// Pop the page // Show success bottom sheet
Navigator.pop(context); getIt<DialogService>().showSuccessBottomSheetWithoutH(
message: LocaleKeys.settingsSavedSuccessfully.tr(context: context),
onOkPressed: () {
Navigator.pop(context);
},
);
}, },
), ),
), ),
@ -243,9 +271,9 @@ class _AccessibilityPageState extends State<AccessibilityPage> {
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
LocaleKeys.fontSizeSmall.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.greyTextColor), LocaleKeys.fontSizeMedium.tr(context: context).toText12(color: AppColors.textColor, fontWeight: FontWeight.w600),
LocaleKeys.fontSizeLarge.tr(context: context).toText12(color: AppColors.greyTextColor), LocaleKeys.fontSizeLarge.tr(context: context).toText12(color: AppColors.textColor, fontWeight: FontWeight.w600),
], ],
), ),
], ],
@ -338,10 +366,7 @@ class _AccessibilityPageState extends State<AccessibilityPage> {
: null, : null,
), ),
SizedBox(width: 8.w), SizedBox(width: 8.w),
label.toText14( label.toText14(isBold: isSelected, color: isSelected ? AppColors.textColor : AppColors.textColor, weight: FontWeight.w600),
isBold: isSelected,
color: isSelected ? AppColors.blackBgColor : AppColors.greyTextColor,
),
], ],
), ),
); );
@ -349,6 +374,7 @@ class _AccessibilityPageState extends State<AccessibilityPage> {
Widget _buildPreviewCard() { Widget _buildPreviewCard() {
// Always build preview content with base "Off" mode colors // Always build preview content with base "Off" mode colors
// Use raw widgets WITHOUT preservation wrappers to isolate from saved visual mode
Widget previewContent = Container( Widget previewContent = Container(
padding: EdgeInsets.all(16.w), padding: EdgeInsets.all(16.w),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration( decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
@ -377,9 +403,11 @@ class _AccessibilityPageState extends State<AccessibilityPage> {
), ),
Transform.scale( Transform.scale(
scaleX: Utils.appState.isArabic() ? -1 : 1, scaleX: Utils.appState.isArabic() ? -1 : 1,
child: Utils.buildSvgWithAssets( child: SvgPicture.asset(
icon: AppAssets.arrow_forward, AppAssets.arrow_forward,
iconColor: AppColors.blackBgColor, // Always use base color colorFilter: ColorFilter.mode(AppColors.blackBgColor, BlendMode.srcIn),
width: 24.w,
height: 24.h,
), ),
), ),
], ],
@ -394,41 +422,95 @@ class _AccessibilityPageState extends State<AccessibilityPage> {
color: AppColors.blackBgColor, // Always use base color color: AppColors.blackBgColor, // Always use base color
), ),
SizedBox(width: 4.w), SizedBox(width: 4.w),
LocaleKeys.riyal.tr(context: context).toText14( SvgPicture.asset(
color: AppColors.blackBgColor, // Always use base color AppAssets.saudi_riyal_icon,
), colorFilter: ColorFilter.mode(AppColors.blackBgColor, BlendMode.srcIn),
SizedBox(width: 4.w),
Utils.buildSvgWithAssets(
icon: AppAssets.saudi_riyal_icon,
iconColor: AppColors.blackBgColor, // Always use base color
width: 16.w, width: 16.w,
height: 16.w, height: 16.h,
), ),
], ],
), ),
SizedBox(height: 16.h), SizedBox(height: 16.h),
// Button - always show with base "Off" colors // Button - simple container without preservation wrappers
CustomButton( Container(
height: 44.h, height: 44.h,
icon: AppAssets.add_icon, padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h),
iconColor: AppColors.whiteColor, decoration: BoxDecoration(
// Always use base color color: AppColors.primaryRedColor,
text: LocaleKeys.buttonText.tr(context: context), borderRadius: BorderRadius.circular(12.r),
backgroundColor: AppColors.primaryRedColor, ),
// Always use base color child: Row(
borderColor: AppColors.primaryRedColor, mainAxisAlignment: MainAxisAlignment.center,
// Always use base color children: [
textColor: AppColors.whiteColor, SvgPicture.asset(
// Always use base color AppAssets.add_icon,
fontSize: 14.f, colorFilter: ColorFilter.mode(AppColors.whiteColor, BlendMode.srcIn),
onPressed: () {}, 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(<double>[
-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(<double>[
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) { switch (_selectedMode) {
case VisualMode.invert: case VisualMode.invert:
return ColorFiltered( return ColorFiltered(
@ -438,25 +520,13 @@ class _AccessibilityPageState extends State<AccessibilityPage> {
0, 0, -1, 0, 255, // Blue channel inverted 0, 0, -1, 0, 255, // Blue channel inverted
0, 0, 0, 1, 0, // Alpha channel unchanged 0, 0, 0, 1, 0, // Alpha channel unchanged
]), ]),
child: previewContent, child: child,
); );
case VisualMode.dim: case VisualMode.dim:
return Stack( // For dim mode, we control actual screen brightness
children: [ // No filter is applied - images remain unchanged
previewContent, return child;
Positioned.fill(
child: IgnorePointer(
child: Container(
decoration: BoxDecoration(
color: AppColors.blackBgColor.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(20.r),
),
),
),
),
],
);
case VisualMode.highContrast: case VisualMode.highContrast:
return ColorFiltered( return ColorFiltered(
@ -466,11 +536,11 @@ class _AccessibilityPageState extends State<AccessibilityPage> {
0.2126, 0.7152, 0.0722, 0, 0, // Blue channel (grayscale) 0.2126, 0.7152, 0.0722, 0, 0, // Blue channel (grayscale)
0, 0, 0, 1, 0, // Alpha channel unchanged 0, 0, 0, 1, 0, // Alpha channel unchanged
]), ]),
child: previewContent, child: child,
); );
case VisualMode.off: case VisualMode.off:
return previewContent; return child;
} }
} }
} }

@ -256,7 +256,7 @@ class ProfileSettingsState extends State<ProfileSettings> {
}, },
), ),
1.divider, 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())); Navigator.of(context).push(CustomPageRoute(page: const AccessibilityPage()));
}), }),
], ],

@ -45,17 +45,9 @@ class PreserveImageColors extends StatelessWidget {
); );
case 'dim': case 'dim':
// Apply brightening to compensate for dimming // Dim mode uses screen brightness control only - no filters applied
// Since parent has 30% black overlay, we increase brightness // So no reverse filter needed
return ColorFiltered( return child;
colorFilter: const ColorFilter.matrix(<double>[
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,
);
case 'highContrast': case 'highContrast':
// For high contrast, apply color saturation to restore colors // For high contrast, apply color saturation to restore colors

@ -46,17 +46,9 @@ class PreserveSvgColors extends StatelessWidget {
); );
case 'dim': case 'dim':
// Apply brightening to compensate for dimming // Dim mode uses screen brightness control only - no filters applied
// Since parent has 30% black overlay, we increase brightness // So no reverse filter needed
return ColorFiltered( return child;
colorFilter: const ColorFilter.matrix(<double>[
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,
);
case 'highContrast': case 'highContrast':
// For high contrast, apply color saturation to restore colors // For high contrast, apply color saturation to restore colors

Loading…
Cancel
Save