Accessibility & Image Widgets Update
#328
Merged
Haroon6138
merged 1 commits from dev_aamir into master 6 days ago
@ -0,0 +1,476 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.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/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/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';
|
||||
|
||||
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;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
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() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
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: () {
|
||||
// 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),
|
||||
),
|
||||
);
|
||||
|
||||
// Pop the page
|
||||
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.greyTextColor),
|
||||
LocaleKeys.fontSizeMedium.tr(context: context).toText12(color: AppColors.greyTextColor),
|
||||
LocaleKeys.fontSizeLarge.tr(context: context).toText12(color: AppColors.greyTextColor),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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.blackBgColor : AppColors.greyTextColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPreviewCard() {
|
||||
// Always build preview content with base "Off" mode colors
|
||||
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: Utils.buildSvgWithAssets(
|
||||
icon: AppAssets.arrow_forward,
|
||||
iconColor: AppColors.blackBgColor, // Always use base color
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
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),
|
||||
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
|
||||
width: 16.w,
|
||||
height: 16.w,
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
|
||||
// Button - always show with base "Off" colors
|
||||
CustomButton(
|
||||
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: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
// Apply visual mode filters to preview
|
||||
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: previewContent,
|
||||
);
|
||||
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
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: previewContent,
|
||||
);
|
||||
|
||||
case VisualMode.off:
|
||||
return previewContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue