|
|
|
|
@ -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<AccessibilityPage> {
|
|
|
|
|
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<ProfileSettingsViewModel>(context, listen: false);
|
|
|
|
|
|
|
|
|
|
// Load saved settings from ViewModel
|
|
|
|
|
_loadSavedSettings();
|
|
|
|
|
}
|
|
|
|
|
@ -75,9 +83,32 @@ class _AccessibilityPageState extends State<AccessibilityPage> {
|
|
|
|
|
|
|
|
|
|
@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<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) {
|
|
|
|
|
const double mediumSize = 40.0; // Medium is baseline
|
|
|
|
|
switch (step) {
|
|
|
|
|
@ -120,24 +151,21 @@ class _AccessibilityPageState extends State<AccessibilityPage> {
|
|
|
|
|
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<DialogService>().showSuccessBottomSheetWithoutH(
|
|
|
|
|
message: LocaleKeys.settingsSavedSuccessfully.tr(context: context),
|
|
|
|
|
onOkPressed: () {
|
|
|
|
|
Navigator.pop(context);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
@ -243,9 +271,9 @@ class _AccessibilityPageState extends State<AccessibilityPage> {
|
|
|
|
|
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<AccessibilityPage> {
|
|
|
|
|
: 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<AccessibilityPage> {
|
|
|
|
|
|
|
|
|
|
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<AccessibilityPage> {
|
|
|
|
|
),
|
|
|
|
|
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<AccessibilityPage> {
|
|
|
|
|
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(<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) {
|
|
|
|
|
case VisualMode.invert:
|
|
|
|
|
return ColorFiltered(
|
|
|
|
|
@ -438,25 +520,13 @@ class _AccessibilityPageState extends State<AccessibilityPage> {
|
|
|
|
|
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<AccessibilityPage> {
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|