You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
HMG_Patient_App_New/lib/presentation/profile_settings/accessibility_page.dart

547 lines
18 KiB
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/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 }
class AccessibilityPage extends StatefulWidget {
const AccessibilityPage({super.key});
@override
State<AccessibilityPage> createState() => _AccessibilityPageState();
}
class _AccessibilityPageState extends State<AccessibilityPage> {
late ProfileSettingsViewModel profileSettingsViewModel;
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((_) async {
if (mounted) {
profileSettingsViewModel = Provider.of<ProfileSettingsViewModel>(context, listen: false);
// Load saved settings from ViewModel
_loadSavedSettings();
}
});
}
void _loadSavedSettings() {
setState(() {
_currentFontSizeStep = profileSettingsViewModel.fontSizeStep;
_selectedMode = _visualModeFromString(profileSettingsViewModel.visualMode);
});
}
VisualMode _visualModeFromString(String mode) {
switch (mode) {
case 'off':
return VisualMode.off;
case 'invert':
return VisualMode.invert;
case 'dim':
return VisualMode.dim;
case 'highContrast':
return VisualMode.highContrast;
default:
return VisualMode.off;
}
}
String _visualModeToString(VisualMode mode) {
switch (mode) {
case VisualMode.off:
return 'off';
case VisualMode.invert:
return 'invert';
case VisualMode.dim:
return 'dim';
case VisualMode.highContrast:
return 'highContrast';
}
}
@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) {
case 0:
return mediumSize * 0.5; // Small: 20.0
case 1:
return mediumSize * 0.75; // Small-Medium: 30.0
case 2:
return mediumSize; // Medium (default): 40.0
case 3:
return mediumSize * 1.5; // Medium-Large: 60.0
case 4:
return mediumSize * 1.75; // Large: 70.0
default:
return mediumSize;
}
}
@override
Widget build(BuildContext context) {
profileSettingsViewModel = Provider.of<ProfileSettingsViewModel>(context, listen: false);
return CollapsingListView(
title: LocaleKeys.accessibility.tr(context: context),
isClose: true,
bottomChild: Container(
padding: EdgeInsets.all(24.w),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.r,
hasShadow: false,
),
child: CustomButton(
height: 56.h,
icon: AppAssets.checkmark_icon,
iconColor: AppColors.whiteColor,
text: LocaleKeys.saveChanges.tr(context: context),
backgroundColor: AppColors.primaryRedColor,
borderColor: AppColors.primaryRedColor,
textColor: AppColors.whiteColor,
fontSize: 16.f,
isBold: true,
onPressed: () async {
// Save accessibility settings to ViewModel
profileSettingsViewModel.setFontSizeStep(_currentFontSizeStep);
profileSettingsViewModel.setVisualMode(_visualModeToString(_selectedMode));
// Apply brightness changes based on selected mode
await _applyBrightness(_selectedMode);
// Show success bottom sheet
getIt<DialogService>().showSuccessBottomSheetWithoutH(
message: LocaleKeys.settingsSavedSuccessfully.tr(context: context),
onOkPressed: () {
Navigator.pop(context);
},
);
},
),
),
child: SingleChildScrollView(
padding: EdgeInsets.only(top: 16.h, bottom: 24.h, left: 24.w, right: 24.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Font Size Card
_buildFontSizeCard(),
SizedBox(height: 24.h),
// Mode for Partially Blind Card
_buildVisualModeCard(),
],
),
),
);
}
Widget _buildFontSizeCard() {
return Container(
padding: EdgeInsets.all(24.w),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.r,
hasShadow: false,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Card Label
LocaleKeys.fontSize.tr(context: context).toText18(isBold: true),
SizedBox(height: 24.h),
// Center Aa heading that changes size
Container(
height: 70.h,
child: Center(
child: AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 300),
style: TextStyle(
fontSize: _getFontSizeForStep(_currentFontSizeStep),
fontWeight: FontWeight.bold,
color: AppColors.blackBgColor,
),
child: const Text("Aa"),
),
),
),
SizedBox(height: 32.h),
// Stepped slider
Column(
children: [
// Stack to position circles on top of the bar
Stack(
alignment: Alignment.center,
children: [
// Grey bar
Container(
height: 4.h,
decoration: BoxDecoration(
color: AppColors.greyTextColor.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(2.r),
),
),
// Step indicator with dots positioned on top of the bar
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(5, (index) {
bool isSelected = index == _currentFontSizeStep;
return GestureDetector(
onTap: () {
setState(() {
_currentFontSizeStep = index;
});
},
behavior: HitTestBehavior.opaque,
child: Padding(
padding: EdgeInsets.all(8.w), // Increased tap zone
child: Container(
width: isSelected ? 20.w : 12.w,
height: isSelected ? 20.w : 12.w,
decoration: BoxDecoration(
color: isSelected ? AppColors.primaryRedColor : Colors.transparent,
shape: BoxShape.circle,
border: Border.all(
color: isSelected ? AppColors.primaryRedColor : AppColors.greyTextColor.withValues(alpha: 0.3),
width: 2.w,
),
),
),
),
);
}),
),
],
),
SizedBox(height: 12.h),
// Labels
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
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),
],
),
],
),
],
),
);
}
Widget _buildVisualModeCard() {
return Container(
padding: EdgeInsets.all(24.w),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 24.r,
hasShadow: false,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Card Label
LocaleKeys.blindModes.tr(context: context).toText18(isBold: true),
SizedBox(height: 24.h),
// Two columns layout
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Left Column - Radio buttons
Expanded(
flex: 2,
child: Column(
children: [
_buildRadioOption(VisualMode.off, LocaleKeys.offTheme.tr(context: context)),
SizedBox(height: 12.h),
_buildRadioOption(VisualMode.invert, LocaleKeys.invertTheme.tr(context: context)),
SizedBox(height: 12.h),
_buildRadioOption(VisualMode.dim, LocaleKeys.dimTheme.tr(context: context)),
SizedBox(height: 12.h),
_buildRadioOption(VisualMode.highContrast, LocaleKeys.highContrastTheme.tr(context: context)),
],
),
),
SizedBox(width: 16.w),
// Right Column - Preview Card
Expanded(
flex: 3,
child: _buildPreviewCard(),
),
],
),
],
),
);
}
Widget _buildRadioOption(VisualMode mode, String label) {
bool isSelected = _selectedMode == mode;
return GestureDetector(
onTap: () {
setState(() {
_selectedMode = mode;
});
},
child: Row(
children: [
Container(
width: 20.w,
height: 20.w,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: isSelected ? AppColors.primaryRedColor : AppColors.greyTextColor,
width: 2.w,
),
color: Colors.transparent,
),
child: isSelected
? Center(
child: Container(
width: 10.w,
height: 10.w,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColors.primaryRedColor,
),
),
)
: null,
),
SizedBox(width: 8.w),
label.toText14(isBold: isSelected, color: isSelected ? AppColors.textColor : AppColors.textColor, weight: FontWeight.w600),
],
),
);
}
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(
color: AppColors.whiteColor,
borderRadius: 20.r,
isCustomShadow: [
BoxShadow(
color: const Color(0xff000000).withValues(alpha: 0.1),
blurRadius: 16,
spreadRadius: 16 * 0.1, // 10% of blur radius
offset: const Offset(0, 0), // Uniform shadow on all sides
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Top row with heading and arrow
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
LocaleKeys.cardHeading.tr(context: context).toText14(
isBold: true,
color: AppColors.blackBgColor, // Always use base color
),
Transform.scale(
scaleX: Utils.appState.isArabic() ? -1 : 1,
child: SvgPicture.asset(
AppAssets.arrow_forward,
colorFilter: ColorFilter.mode(AppColors.blackBgColor, BlendMode.srcIn),
width: 24.w,
height: 24.h,
),
),
],
),
SizedBox(height: 16.h),
// Price with SAR symbol
Row(
children: [
"1234".toText24(
isBold: true,
color: AppColors.blackBgColor, // Always use base color
),
SizedBox(width: 4.w),
SvgPicture.asset(
AppAssets.saudi_riyal_icon,
colorFilter: ColorFilter.mode(AppColors.blackBgColor, BlendMode.srcIn),
width: 16.w,
height: 16.h,
),
],
),
SizedBox(height: 16.h),
// Button - simple container without preservation wrappers
Container(
height: 44.h,
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,
),
],
),
),
],
),
);
// 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(
colorFilter: const ColorFilter.matrix(<double>[
-1, 0, 0, 0, 255, // Red channel inverted
0, -1, 0, 0, 255, // Green channel inverted
0, 0, -1, 0, 255, // Blue channel inverted
0, 0, 0, 1, 0, // Alpha channel unchanged
]),
child: child,
);
case VisualMode.dim:
// For dim mode, we control actual screen brightness
// No filter is applied - images remain unchanged
return child;
case VisualMode.highContrast:
return ColorFiltered(
colorFilter: const ColorFilter.matrix(<double>[
0.2126, 0.7152, 0.0722, 0, 0, // Red channel (grayscale)
0.2126, 0.7152, 0.0722, 0, 0, // Green channel (grayscale)
0.2126, 0.7152, 0.0722, 0, 0, // Blue channel (grayscale)
0, 0, 0, 1, 0, // Alpha channel unchanged
]),
child: child,
);
case VisualMode.off:
return child;
}
}
}