diff --git a/lib/config/ui_config.dart b/lib/config/ui_config.dart index 27caee8b..11bc66b4 100644 --- a/lib/config/ui_config.dart +++ b/lib/config/ui_config.dart @@ -171,6 +171,12 @@ abstract class FilterConfig { static const paddingMedium = 8.0; static const spacingBetweenWidgets = 12.0; static final radius = BorderRadius.circular(8); + static const buttonPadding = + EdgeInsets.symmetric(vertical: 10, horizontal: 20); +} + +class DialogsConfig { + static final padding = const EdgeInsets.all(20).copyWith(top: 6); } abstract class LottieAnimationConfig { diff --git a/lib/features/digital_guide_view/presentation/widgets/accessibility_button.dart b/lib/features/digital_guide_view/presentation/widgets/accessibility_button.dart index 1a9f1c11..d117c237 100644 --- a/lib/features/digital_guide_view/presentation/widgets/accessibility_button.dart +++ b/lib/features/digital_guide_view/presentation/widgets/accessibility_button.dart @@ -1,7 +1,10 @@ +import "dart:async"; + import "package:flutter/material.dart"; import "../../../../config/ui_config.dart"; import "../../../../theme/app_theme.dart"; +import "../../tabs/accessibility_dialog/presentation/accessibility_dialog.dart"; class AccessibilityButton extends StatelessWidget { @override @@ -9,7 +12,14 @@ class AccessibilityButton extends StatelessWidget { return Padding( padding: const EdgeInsets.only(right: 8), child: OutlinedButton( - onPressed: () {}, + onPressed: () { + unawaited( + showDialog( + context: context, + builder: (_) => const AccessibilityDialog(), + ), + ); + }, style: OutlinedButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: diff --git a/lib/features/digital_guide_view/tabs/accessibility_dialog/business/accessibility_mode_service.dart b/lib/features/digital_guide_view/tabs/accessibility_dialog/business/accessibility_mode_service.dart new file mode 100644 index 00000000..6305bb78 --- /dev/null +++ b/lib/features/digital_guide_view/tabs/accessibility_dialog/business/accessibility_mode_service.dart @@ -0,0 +1,67 @@ +import "dart:async"; + +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; + +import "../data/accessibility_mode_repository.dart"; +import "../data/modes.dart"; + +part "accessibility_mode_service.g.dart"; + +// if the mode has children, it will be calculated based on them +// if the mode has a key, it will be calculated based on the repository +@riverpod +class AccessibilityModeService extends _$AccessibilityModeService { + @override + Future build(AccessibilityMode mode) async { + return switch (mode) { + ModeWithChildren() => _calculateModeWithChildrenState(mode), + ModeWithKey() => ref.watch( + accessibilityModeRepositoryProvider(mode).future, + ), + }; + } + + Future setMode({required bool newValue}) async { + final modeStronglyTyped = mode; // needed for typing system + await switch (modeStronglyTyped) { + ModeWithChildren() => + _setModeWithChildrenState(modeStronglyTyped, newValue), + ModeWithKey() => _setSingularModeState(modeStronglyTyped, newValue), + }; + } + + // true if any of its children are true + Future _calculateModeWithChildrenState(ModeWithChildren mode) async { + final submodesValues = await Future.wait( + mode.children.map( + (child) => ref.watch(accessibilityModeServiceProvider(child).future), + ), + ); + return submodesValues.anyIs(true); + } + + // sets all childrens' of the mode to newValue + Future _setModeWithChildrenState( + ModeWithChildren mode, + bool newValue, + ) async { + for (final child in mode.children) { + await ref + .read(accessibilityModeServiceProvider(child).notifier) + .setMode(newValue: newValue); + } + } + + // calls directly the repository + Future _setSingularModeState( + ModeWithKey modeStronglyTyped, + bool newValue, + ) { + return ref + .read( + accessibilityModeRepositoryProvider(modeStronglyTyped).notifier, + ) + .setMode(newValue: newValue); + } +} diff --git a/lib/features/digital_guide_view/tabs/accessibility_dialog/business/top_level_modes.dart b/lib/features/digital_guide_view/tabs/accessibility_dialog/business/top_level_modes.dart new file mode 100644 index 00000000..604d59a3 --- /dev/null +++ b/lib/features/digital_guide_view/tabs/accessibility_dialog/business/top_level_modes.dart @@ -0,0 +1,8 @@ +import "../data/modes.dart"; + +final topLevelModes = [ + const MotorImpairment(), + const VisualImpairment(), + const SensorySensitivity(), + const CognitiveImpairment(), +]; diff --git a/lib/features/digital_guide_view/tabs/accessibility_dialog/data/accessibility_mode_repository.dart b/lib/features/digital_guide_view/tabs/accessibility_dialog/data/accessibility_mode_repository.dart new file mode 100644 index 00000000..c89aeee2 --- /dev/null +++ b/lib/features/digital_guide_view/tabs/accessibility_dialog/data/accessibility_mode_repository.dart @@ -0,0 +1,23 @@ +import "dart:async"; + +import "package:riverpod_annotation/riverpod_annotation.dart"; + +import "../../../../../config/shared_prefs.dart"; +import "modes.dart"; + +part "accessibility_mode_repository.g.dart"; + +@riverpod +class AccessibilityModeRepository extends _$AccessibilityModeRepository { + @override + Future build(ModeWithKey mode) async { + final prefs = await ref.watch(sharedPreferencesSingletonProvider.future); + return prefs.getBool(mode.sharedPrefsKey) ?? false; + } + + Future setMode({required bool newValue}) async { + state = AsyncValue.data(newValue); + final prefs = await ref.watch(sharedPreferencesSingletonProvider.future); + await prefs.setBool(mode.sharedPrefsKey, newValue); + } +} diff --git a/lib/features/digital_guide_view/tabs/accessibility_dialog/data/modes.dart b/lib/features/digital_guide_view/tabs/accessibility_dialog/data/modes.dart new file mode 100644 index 00000000..6b17f20c --- /dev/null +++ b/lib/features/digital_guide_view/tabs/accessibility_dialog/data/modes.dart @@ -0,0 +1,48 @@ +// https://dart.dev/language/class-modifiers#sealed +sealed class AccessibilityMode { + const AccessibilityMode(); +} + +// this mode's state depends on the state of its children +sealed class ModeWithChildren extends AccessibilityMode { + const ModeWithChildren(this.children); + final List children; +} + +// this mode's state is stored locally in shared preferences +sealed class ModeWithKey extends AccessibilityMode { + const ModeWithKey(this.sharedPrefsKey); + final String sharedPrefsKey; +} + +class MotorImpairment extends ModeWithKey { + const MotorImpairment() : super("_prefs_accessibility_motor_impairment"); +} + +class Blind extends ModeWithKey { + const Blind() : super("_prefs_accessibility_blind"); +} + +class LowVision extends ModeWithKey { + const LowVision() : super("_prefs_accessibility_low_vision"); +} + +class SensorySensitivity extends ModeWithKey { + const SensorySensitivity() + : super("_prefs_accessibility_sensory_sensitivity"); +} + +class CognitiveImpairment extends ModeWithKey { + const CognitiveImpairment() + : super("_prefs_accessibility_cognitive_impairment"); +} + +class VisualImpairment extends ModeWithChildren { + const VisualImpairment() + : super( + const [ + LowVision(), + Blind(), + ], + ); +} diff --git a/lib/features/digital_guide_view/tabs/accessibility_dialog/presentation/accessibility_dialog.dart b/lib/features/digital_guide_view/tabs/accessibility_dialog/presentation/accessibility_dialog.dart new file mode 100644 index 00000000..afa7cfaf --- /dev/null +++ b/lib/features/digital_guide_view/tabs/accessibility_dialog/presentation/accessibility_dialog.dart @@ -0,0 +1,18 @@ +import "package:flutter/material.dart"; + +import "../../../../../utils/context_extensions.dart"; +import "checkboxes_list.dart"; +import "red_dialog.dart"; + +class AccessibilityDialog extends StatelessWidget { + const AccessibilityDialog({super.key}); + + @override + Widget build(BuildContext context) { + return RedDialog( + title: context.localize.accessibility_profiles, + subtitle: context.localize.you_can_adjust, + child: const CheckboxesList(), + ); + } +} diff --git a/lib/features/digital_guide_view/tabs/accessibility_dialog/presentation/checkboxes_list.dart b/lib/features/digital_guide_view/tabs/accessibility_dialog/presentation/checkboxes_list.dart new file mode 100644 index 00000000..abd26349 --- /dev/null +++ b/lib/features/digital_guide_view/tabs/accessibility_dialog/presentation/checkboxes_list.dart @@ -0,0 +1,35 @@ +import "package:flutter/material.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; + +import "../business/top_level_modes.dart"; +import "../data/modes.dart"; +import "mode_checkbox.dart"; + +class _SubModePadding extends Padding { + const _SubModePadding({super.child}) + : super( + padding: const EdgeInsets.only(left: 25), + ); +} + +class CheckboxesList extends HookWidget { + const CheckboxesList({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + for (final mode in topLevelModes) + switch (mode) { + ModeWithChildren() => Column( + children: [ + for (final subMode in mode.children) + _SubModePadding(child: ModeCheckbox(subMode)), + ], + ), + ModeWithKey() => ModeCheckbox(mode), + }, + ], + ); + } +} diff --git a/lib/features/digital_guide_view/tabs/accessibility_dialog/presentation/labels.dart b/lib/features/digital_guide_view/tabs/accessibility_dialog/presentation/labels.dart new file mode 100644 index 00000000..4c0a8869 --- /dev/null +++ b/lib/features/digital_guide_view/tabs/accessibility_dialog/presentation/labels.dart @@ -0,0 +1,17 @@ +import "package:flutter/material.dart"; + +import "../../../../../utils/context_extensions.dart"; +import "../data/modes.dart"; + +extension AccessibilityModeLocalizationX on AccessibilityMode { + String localizedLabel(BuildContext context) { + return switch (this) { + MotorImpairment() => context.localize.motorImpairment, + VisualImpairment() => context.localize.visualImpairment, + Blind() => context.localize.blind, + LowVision() => context.localize.lowVision, + SensorySensitivity() => context.localize.sensorySensitivity, + CognitiveImpairment() => context.localize.cognitiveImpairment, + }; + } +} diff --git a/lib/features/digital_guide_view/tabs/accessibility_dialog/presentation/mode_checkbox.dart b/lib/features/digital_guide_view/tabs/accessibility_dialog/presentation/mode_checkbox.dart new file mode 100644 index 00000000..61d8b5b8 --- /dev/null +++ b/lib/features/digital_guide_view/tabs/accessibility_dialog/presentation/mode_checkbox.dart @@ -0,0 +1,40 @@ +import "dart:async"; + +import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; + +import "../../../../../theme/app_theme.dart"; +import "../business/accessibility_mode_service.dart"; +import "../data/modes.dart"; +import "labels.dart"; + +class ModeCheckbox extends ConsumerWidget { + const ModeCheckbox(this.mode, {super.key}); + final AccessibilityMode mode; + @override + Widget build(BuildContext context, WidgetRef ref) { + final value = ref.watch(accessibilityModeServiceProvider(mode)); + + // ignore: avoid_positional_boolean_parameters + void onChanged(bool? value) { + if (value != null) { + unawaited( + ref + .read(accessibilityModeServiceProvider(mode).notifier) + .setMode(newValue: value), + ); + } + } + + return CheckboxListTile( + dense: true, + controlAffinity: ListTileControlAffinity.leading, + title: Text( + mode.localizedLabel(context), + style: context.aboutUsTheme.body, + ), + value: value.value ?? false, + onChanged: onChanged, + ); + } +} diff --git a/lib/features/digital_guide_view/tabs/accessibility_dialog/presentation/red_dialog.dart b/lib/features/digital_guide_view/tabs/accessibility_dialog/presentation/red_dialog.dart new file mode 100644 index 00000000..2b9fa489 --- /dev/null +++ b/lib/features/digital_guide_view/tabs/accessibility_dialog/presentation/red_dialog.dart @@ -0,0 +1,151 @@ +import "package:flutter/material.dart"; + +import "../../../../../config/ui_config.dart"; +import "../../../../../theme/app_theme.dart"; +import "../../../../../utils/context_extensions.dart"; + +// pure UI, no logic, just a nice dialog with a title, subtitle and a child +class RedDialog extends StatelessWidget { + final String title; + final String subtitle; + final Widget child; + + const RedDialog({ + super.key, + required this.title, + required this.child, + required this.subtitle, + }); + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + child: DecoratedBox( + decoration: BoxDecoration( + color: context.colorTheme.whiteSoap, + borderRadius: BorderRadius.circular(8), + border: Border( + top: BorderSide( + color: context.colorTheme.orangePomegranade, + ), + bottom: BorderSide( + color: context.colorTheme.orangePomegranade, + width: 5, + ), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(64), + offset: const Offset(0, 4), + blurRadius: 4, + ), + ], + ), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 332), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _DialogHeader(title: title), + Flexible( + child: SingleChildScrollView( + child: _DialogContent(subtitle: subtitle, child: child), + ), + ), + ], + ), + ), + ), + ); + } +} + +class _DialogContent extends StatelessWidget { + const _DialogContent({ + required this.subtitle, + required this.child, + }); + + final String subtitle; + final Widget child; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text( + subtitle, + style: context.aboutUsTheme.body.copyWith( + height: 1.4, + color: context.colorTheme.greyPigeon, + ), + ), + ), + const SizedBox(height: 6), + child, + const _DialogFooter(), + ], + ); + } +} + +class _DialogFooter extends StatelessWidget { + const _DialogFooter(); + + @override + Widget build(BuildContext context) { + return Padding( + padding: DialogsConfig.padding, + child: ElevatedButton( + onPressed: () { + // we're saving the changes in real time anyway + Navigator.of(context).pop(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: context.colorTheme.orangePomegranadeLighter, + elevation: 2, + padding: FilterConfig.buttonPadding, + shape: RoundedRectangleBorder(borderRadius: FilterConfig.radius), + ), + child: Center( + child: Text( + context.localize.apply, + style: context.textTheme.titleWhite, + ), + ), + ), + ); + } +} + +class _DialogHeader extends StatelessWidget { + const _DialogHeader({required this.title}); + + final String title; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(20).copyWith(bottom: 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + title, + style: context.textTheme.headline.copyWith(height: 1.4), + ), + ), + IconButton( + icon: const Icon(Icons.close), + color: context.colorTheme.greyPigeon, + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + ); + } +} diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 63809452..c918f448 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -287,5 +287,13 @@ "platforms" : "Podesty", "stairs" : "Schody", "key_information" : "Najważniejsze informacje", - "working_hours" : "Godziny otwarcia" + "working_hours" : "Godziny otwarcia", + "motorImpairment": "Dysfunkcja ruchu", + "visualImpairment": "Dysfunkcja wzroku", + "blind": "Niewidomy", + "lowVision": "Słabo widzący", + "sensorySensitivity": "Wrażliwość sensoryczna", + "cognitiveImpairment": "Trudności poznawcze", + "accessibility_profiles":"Profile dostępności", + "you_can_adjust": "Możesz dostosować informacje pod swoje specjalne potrzeby" }