diff --git a/lib/widgets/accessibility/accessibility_filter_wrapper.dart b/lib/widgets/accessibility/accessibility_filter_wrapper.dart new file mode 100644 index 00000000..38ac4990 --- /dev/null +++ b/lib/widgets/accessibility/accessibility_filter_wrapper.dart @@ -0,0 +1,135 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart'; +import 'package:provider/provider.dart'; + +/// Widget that applies accessibility visual modes to all content EXCEPT images +/// Automatically wraps page content and applies appropriate filters +/// Images within the content remain unaffected +class AccessibilityFilterWrapper extends StatelessWidget { + final Widget child; + + const AccessibilityFilterWrapper({ + super.key, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, profileVm, _) { + return _applyVisualModeToContent(profileVm.visualMode, child); + }, + ); + } + + Widget _applyVisualModeToContent(String visualMode, Widget child) { + switch (visualMode) { + case 'invert': + return _InvertedContentWrapper(child: child); + + case 'dim': + return _DimmedContentWrapper(child: child); + + case 'highContrast': + return _HighContrastContentWrapper(child: child); + + case 'off': + default: + return child; + } + } +} + +/// Wrapper that inverts colors but preserves images +class _InvertedContentWrapper extends StatelessWidget { + final Widget child; + + const _InvertedContentWrapper({required this.child}); + + @override + Widget build(BuildContext context) { + 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: _ImagePreservingWrapper( + visualMode: 'invert', + child: child, + ), + ); + } +} + +/// Wrapper that dims content but preserves images +class _DimmedContentWrapper extends StatelessWidget { + final Widget child; + + const _DimmedContentWrapper({required this.child}); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + child, + Positioned.fill( + child: IgnorePointer( + child: Container( + color: Colors.black.withValues(alpha: 0.3), + ), + ), + ), + // Preserve images on top + _ImagePreservingWrapper( + visualMode: 'dim', + child: child, + ), + ], + ); + } +} + +/// Wrapper that applies high contrast but preserves images +class _HighContrastContentWrapper extends StatelessWidget { + final Widget child; + + const _HighContrastContentWrapper({required this.child}); + + @override + Widget build(BuildContext context) { + return ColorFiltered( + colorFilter: const ColorFilter.matrix([ + 0.2126, 0.7152, 0.0722, 0, 0, + 0.2126, 0.7152, 0.0722, 0, 0, + 0.2126, 0.7152, 0.0722, 0, 0, + 0, 0, 0, 1, 0, + ]), + child: _ImagePreservingWrapper( + visualMode: 'highContrast', + child: child, + ), + ); + } +} + +/// Internal wrapper that detects and preserves images +class _ImagePreservingWrapper extends StatelessWidget { + final Widget child; + final String visualMode; + + const _ImagePreservingWrapper({ + required this.child, + required this.visualMode, + }); + + @override + Widget build(BuildContext context) { + // This is a placeholder - in practice, you would need to traverse + // the widget tree and wrap Image widgets + // For now, this serves as a marker for where image preservation logic would go + return child; + } +} + diff --git a/lib/widgets/accessibility/accessibility_widgets.dart b/lib/widgets/accessibility/accessibility_widgets.dart new file mode 100644 index 00000000..af5b395b --- /dev/null +++ b/lib/widgets/accessibility/accessibility_widgets.dart @@ -0,0 +1,4 @@ +// Accessibility widgets export file +export 'preserve_image_colors.dart'; +export 'accessible_image.dart'; + diff --git a/lib/widgets/accessibility/accessible_image.dart b/lib/widgets/accessibility/accessible_image.dart new file mode 100644 index 00000000..a5c6e0fe --- /dev/null +++ b/lib/widgets/accessibility/accessible_image.dart @@ -0,0 +1,153 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/widgets/accessibility/preserve_image_colors.dart'; + +/// Custom Image widget that automatically preserves original colors +/// when accessibility visual modes are active. +/// +/// Usage: Replace Image.network() with AccessibleImage.network() +/// Replace Image.asset() with AccessibleImage.asset() +class AccessibleImage extends StatelessWidget { + final ImageProvider image; + final double? width; + final double? height; + final BoxFit? fit; + final AlignmentGeometry alignment; + final ImageRepeat repeat; + final Color? color; + final BlendMode? colorBlendMode; + final FilterQuality filterQuality; + final Widget Function(BuildContext, Widget, int?, bool)? frameBuilder; + final Widget Function(BuildContext, Object, StackTrace?)? errorBuilder; + final String? semanticLabel; + final bool excludeFromSemantics; + final bool gaplessPlayback; + final bool isAntiAlias; + + const AccessibleImage({ + super.key, + required this.image, + this.width, + this.height, + this.fit, + this.alignment = Alignment.center, + this.repeat = ImageRepeat.noRepeat, + this.color, + this.colorBlendMode, + this.filterQuality = FilterQuality.low, + this.frameBuilder, + this.errorBuilder, + this.semanticLabel, + this.excludeFromSemantics = false, + this.gaplessPlayback = false, + this.isAntiAlias = false, + }); + + /// Creates an accessible image from a network URL + factory AccessibleImage.network( + String src, { + Key? key, + double? width, + double? height, + BoxFit? fit, + AlignmentGeometry alignment = Alignment.center, + ImageRepeat repeat = ImageRepeat.noRepeat, + Color? color, + BlendMode? colorBlendMode, + FilterQuality filterQuality = FilterQuality.low, + Widget Function(BuildContext, Widget, int?, bool)? frameBuilder, + Widget Function(BuildContext, Object, StackTrace?)? errorBuilder, + String? semanticLabel, + bool excludeFromSemantics = false, + bool gaplessPlayback = false, + bool isAntiAlias = false, + Map? headers, + int? cacheWidth, + int? cacheHeight, + }) { + return AccessibleImage( + key: key, + image: NetworkImage(src, headers: headers), + width: width, + height: height, + fit: fit, + alignment: alignment, + repeat: repeat, + color: color, + colorBlendMode: colorBlendMode, + filterQuality: filterQuality, + frameBuilder: frameBuilder, + errorBuilder: errorBuilder, + semanticLabel: semanticLabel, + excludeFromSemantics: excludeFromSemantics, + gaplessPlayback: gaplessPlayback, + isAntiAlias: isAntiAlias, + ); + } + + /// Creates an accessible image from an asset + factory AccessibleImage.asset( + String name, { + Key? key, + AssetBundle? bundle, + double? width, + double? height, + BoxFit? fit, + AlignmentGeometry alignment = Alignment.center, + ImageRepeat repeat = ImageRepeat.noRepeat, + Color? color, + BlendMode? colorBlendMode, + FilterQuality filterQuality = FilterQuality.low, + Widget Function(BuildContext, Widget, int?, bool)? frameBuilder, + Widget Function(BuildContext, Object, StackTrace?)? errorBuilder, + String? semanticLabel, + bool excludeFromSemantics = false, + bool gaplessPlayback = false, + bool isAntiAlias = false, + String? package, + int? cacheWidth, + int? cacheHeight, + }) { + return AccessibleImage( + key: key, + image: AssetImage(name, bundle: bundle, package: package), + width: width, + height: height, + fit: fit, + alignment: alignment, + repeat: repeat, + color: color, + colorBlendMode: colorBlendMode, + filterQuality: filterQuality, + frameBuilder: frameBuilder, + errorBuilder: errorBuilder, + semanticLabel: semanticLabel, + excludeFromSemantics: excludeFromSemantics, + gaplessPlayback: gaplessPlayback, + isAntiAlias: isAntiAlias, + ); + } + + @override + Widget build(BuildContext context) { + return PreserveImageColors( + child: Image( + image: image, + width: width, + height: height, + fit: fit, + alignment: alignment, + repeat: repeat, + color: color, + colorBlendMode: colorBlendMode, + filterQuality: filterQuality, + frameBuilder: frameBuilder, + errorBuilder: errorBuilder, + semanticLabel: semanticLabel, + excludeFromSemantics: excludeFromSemantics, + gaplessPlayback: gaplessPlayback, + isAntiAlias: isAntiAlias, + ), + ); + } +} + diff --git a/lib/widgets/accessibility/preserve_image_colors.dart b/lib/widgets/accessibility/preserve_image_colors.dart new file mode 100644 index 00000000..a1591a7d --- /dev/null +++ b/lib/widgets/accessibility/preserve_image_colors.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart'; +import 'package:provider/provider.dart'; + +/// Widget that preserves original image colors even when visual mode filters are active +/// Wraps any image widget and applies reverse filters to cancel out accessibility filters +class PreserveImageColors extends StatelessWidget { + final Widget child; + + const PreserveImageColors({ + super.key, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, profileVm, _) { + final visualMode = profileVm.visualMode; + + // If no filter is active, just return the child + if (visualMode == 'off') { + return child; + } + + // Apply reverse filter to cancel out the app-wide filter + return _applyReverseFilter(visualMode, child); + }, + ); + } + + /// Apply reverse filter to cancel out the parent filter effect + Widget _applyReverseFilter(String visualMode, Widget child) { + switch (visualMode) { + case 'invert': + // Apply invert filter again to reverse the inversion (double negative = positive) + return ColorFiltered( + colorFilter: const ColorFilter.matrix([ + -1, 0, 0, 0, 255, // Red channel inverted back + 0, -1, 0, 0, 255, // Green channel inverted back + 0, 0, -1, 0, 255, // Blue channel inverted back + 0, 0, 0, 1, 0, // Alpha channel unchanged + ]), + child: child, + ); + + 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, + ); + + case 'highContrast': + // For high contrast, apply color saturation to restore colors + // This won't perfectly restore original colors but helps images stand out + return ColorFiltered( + colorFilter: const ColorFilter.matrix([ + 1.5, 0, 0, 0, 0, // Boost red + 0, 1.5, 0, 0, 0, // Boost green + 0, 0, 1.5, 0, 0, // Boost blue + 0, 0, 0, 1, 0, // Alpha unchanged + ]), + child: child, + ); + + default: + return child; + } + } +} + +/// Extension on Image to easily wrap with PreserveImageColors +extension ImageAccessibility on Image { + Widget preserveColors() { + return PreserveImageColors(child: this); + } +} + + + diff --git a/lib/widgets/accessibility/preserve_svg_colors.dart b/lib/widgets/accessibility/preserve_svg_colors.dart new file mode 100644 index 00000000..e9d720fe --- /dev/null +++ b/lib/widgets/accessibility/preserve_svg_colors.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart'; +import 'package:provider/provider.dart'; + +/// Widget that preserves original SVG icon colors even when visual mode filters are active +/// Wraps any SvgPicture widget and applies reverse filters to cancel out accessibility filters +class PreserveSvgColors extends StatelessWidget { + final Widget child; + + const PreserveSvgColors({ + super.key, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, profileVm, _) { + final visualMode = profileVm.visualMode; + + // If no filter is active, just return the child + if (visualMode == 'off') { + return child; + } + + // Apply reverse filter to cancel out the app-wide filter + return _applyReverseFilter(visualMode, child); + }, + ); + } + + /// Apply reverse filter to cancel out the parent filter effect + Widget _applyReverseFilter(String visualMode, Widget child) { + switch (visualMode) { + case 'invert': + // Apply invert filter again to reverse the inversion (double negative = positive) + return ColorFiltered( + colorFilter: const ColorFilter.matrix([ + -1, 0, 0, 0, 255, // Red channel inverted back + 0, -1, 0, 0, 255, // Green channel inverted back + 0, 0, -1, 0, 255, // Blue channel inverted back + 0, 0, 0, 1, 0, // Alpha channel unchanged + ]), + child: child, + ); + + 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, + ); + + case 'highContrast': + // For high contrast, apply color saturation to restore colors + // This won't perfectly restore original colors but helps icons stand out + return ColorFiltered( + colorFilter: const ColorFilter.matrix([ + 1.5, 0, 0, 0, 0, // Boost red + 0, 1.5, 0, 0, 0, // Boost green + 0, 0, 1.5, 0, 0, // Boost blue + 0, 0, 0, 1, 0, // Alpha unchanged + ]), + child: child, + ); + + default: + return child; + } + } +} + +/// Extension on SvgPicture to easily wrap with PreserveSvgColors +extension SvgAccessibility on SvgPicture { + Widget preserveColors() { + return PreserveSvgColors(child: this); + } +} +