From ff7d860359b4517f341ffce3164c606720b56214 Mon Sep 17 00:00:00 2001 From: Calcitem Date: Sat, 28 Aug 2021 20:50:20 +0800 Subject: [PATCH] Improve accessibility (WIP) * Add Accessibility settings * Support screen reader (WIP) * Only support German/English/Chinese --- src/ui/flutter_app/lib/common/config.dart | 3 + src/ui/flutter_app/lib/l10n/intl_de.arb | 16 ++++ src/ui/flutter_app/lib/l10n/intl_en.arb | 16 ++++ src/ui/flutter_app/lib/l10n/intl_zh.arb | 16 ++++ src/ui/flutter_app/lib/services/audios.dart | 4 +- src/ui/flutter_app/lib/widgets/board.dart | 35 ++++++++- .../lib/widgets/drawer_user_controller.dart | 12 ++- src/ui/flutter_app/lib/widgets/game_page.dart | 78 +++++++++++++------ .../lib/widgets/game_settings_page.dart | 33 ++++++-- 9 files changed, 178 insertions(+), 35 deletions(-) diff --git a/src/ui/flutter_app/lib/common/config.dart b/src/ui/flutter_app/lib/common/config.dart index a23416bc..2de307a6 100644 --- a/src/ui/flutter_app/lib/common/config.dart +++ b/src/ui/flutter_app/lib/common/config.dart @@ -31,6 +31,7 @@ class Config { // Preferences static bool toneEnabled = true; static bool keepMuteWhenTakingBack = true; + static bool screenReaderSupport = false; static bool aiMovesFirst = false; static bool aiIsLazy = false; static int skillLevel = 1; @@ -105,6 +106,7 @@ class Config { // Preferences Config.toneEnabled = settings['ToneEnabled'] ?? true; Config.keepMuteWhenTakingBack = settings['KeepMuteWhenTakingBack'] ?? true; + Config.screenReaderSupport = settings['ScreenReaderSupport'] ?? false; Config.aiMovesFirst = settings['AiMovesFirst'] ?? false; Config.aiIsLazy = settings['AiIsLazy'] ?? false; Config.skillLevel = settings['SkillLevel'] ?? 1; @@ -209,6 +211,7 @@ class Config { // Preferences settings['ToneEnabled'] = Config.toneEnabled; settings['KeepMuteWhenTakingBack'] = Config.keepMuteWhenTakingBack; + settings['ScreenReaderSupport'] = Config.screenReaderSupport; settings['AiMovesFirst'] = Config.aiMovesFirst; settings['AiIsLazy'] = Config.aiIsLazy; settings['SkillLevel'] = Config.skillLevel; diff --git a/src/ui/flutter_app/lib/l10n/intl_de.arb b/src/ui/flutter_app/lib/l10n/intl_de.arb index 6b3fb04b..33823d71 100644 --- a/src/ui/flutter_app/lib/l10n/intl_de.arb +++ b/src/ui/flutter_app/lib/l10n/intl_de.arb @@ -1128,5 +1128,21 @@ "drawIfNoRemovalWithinTenMovesWhenThreeLeft_Detail": "Wenn ein Spieler nur noch drei Steine übrig hat und keiner der Spieler einen gegnerischen Stein innerhalb von zehn Zügen entfernen kann, ist das Spiel unentschieden.", "@drawIfNoRemovalWithinTenMovesWhenThreeLeft_Detail": { "description": "When a player is down to three pieces, and neither player can remove an opponent's piece within ten moves, the game is a draw." + }, + "mainMenu": "Hauptmenü", + "@mainMenu": { + "description": "Main menu" + }, + "board": "Planke", + "@mainMenu": { + "description": "Board" + }, + "accessibility": "Barrierefreiheit", + "@accessibility": { + "description": "Accessibility" + }, + "screenReaderSupport": "Unterstützung für Bildschirmlesegeräte", + "@accessibility": { + "description": "Screen reader support" } } \ No newline at end of file diff --git a/src/ui/flutter_app/lib/l10n/intl_en.arb b/src/ui/flutter_app/lib/l10n/intl_en.arb index 0fc5a016..364720ee 100644 --- a/src/ui/flutter_app/lib/l10n/intl_en.arb +++ b/src/ui/flutter_app/lib/l10n/intl_en.arb @@ -1128,5 +1128,21 @@ "drawIfNoRemovalWithinTenMovesWhenThreeLeft_Detail": "When a player is down to three pieces, and neither player can remove an opponent's piece within ten moves, the game is a draw.", "@drawIfNoRemovalWithinTenMovesWhenThreeLeft_Detail": { "description": "When a player is down to three pieces, and neither player can remove an opponent's piece within ten moves, the game is a draw." + }, + "mainMenu": "Main menu", + "@mainMenu": { + "description": "Main menu" + }, + "board": "Board", + "@mainMenu": { + "description": "Board" + }, + "accessibility": "Accessibility", + "@accessibility": { + "description": "Accessibility" + }, + "screenReaderSupport": "Screen reader support", + "@accessibility": { + "description": "Screen reader support" } } \ No newline at end of file diff --git a/src/ui/flutter_app/lib/l10n/intl_zh.arb b/src/ui/flutter_app/lib/l10n/intl_zh.arb index 069143b9..36f39e61 100644 --- a/src/ui/flutter_app/lib/l10n/intl_zh.arb +++ b/src/ui/flutter_app/lib/l10n/intl_zh.arb @@ -1128,5 +1128,21 @@ "drawIfNoRemovalWithinTenMovesWhenThreeLeft_Detail": "当一方只剩下三颗棋子时,而双方都不能在接下来十步之内吃掉对方的棋子,则判为和棋。", "@drawIfNoRemovalWithinTenMovesWhenThreeLeft_Detail": { "description": "When a player is down to three pieces, and neither player can remove an opponent's piece within ten moves, the game is a draw." + }, + "mainMenu": "主菜单", + "@mainMenu": { + "description": "Main menu" + }, + "board": "棋盘", + "@mainMenu": { + "description": "Board" + }, + "accessibility": "无障碍", + "@accessibility": { + "description": "Accessibility" + }, + "screenReaderSupport": "支持屏幕阅读器", + "@accessibility": { + "description": "Screen reader support" } } \ No newline at end of file diff --git a/src/ui/flutter_app/lib/services/audios.dart b/src/ui/flutter_app/lib/services/audios.dart index 2e97aaf0..3103b2eb 100644 --- a/src/ui/flutter_app/lib/services/audios.dart +++ b/src/ui/flutter_app/lib/services/audios.dart @@ -191,7 +191,9 @@ class Audios { static playTone(var soundId) async { Chain.capture(() async { - if (!Config.toneEnabled || isTemporaryMute) { + if (!Config.toneEnabled || + isTemporaryMute || + Config.screenReaderSupport) { return; } diff --git a/src/ui/flutter_app/lib/widgets/board.dart b/src/ui/flutter_app/lib/widgets/board.dart index 0a693784..fee4f96f 100644 --- a/src/ui/flutter_app/lib/widgets/board.dart +++ b/src/ui/flutter_app/lib/widgets/board.dart @@ -40,10 +40,35 @@ class Board extends StatelessWidget { Widget build(BuildContext context) { var padding = AppTheme.boardPadding; + var coordinates = []; + + for (var file in ['a', 'b', 'c', 'd', 'e', 'f', 'g']) { + for (var rank in ['7', '6', '5', '4', '3', '2', '1']) { + coordinates.add("$file$rank"); + } + } + var container = Container( margin: EdgeInsets.symmetric( - vertical: padding, - horizontal: (width - padding * 2) / 6 / 2 + padding, + vertical: 0, + horizontal: 0, + ), + child: GridView( + scrollDirection: Axis.horizontal, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 7, + ), + children: List.generate(7 * 7, (index) { + return Center( + child: Text( + coordinates[index], + style: TextStyle( + fontSize: Config.fontSize, + color: Config.developerMode ? Colors.red : Colors.transparent, + ), + ), + ); + }), ), ); @@ -70,6 +95,12 @@ class Board extends StatelessWidget { ); return GestureDetector( + /* + child: Semantics( + label: S.of(context).board, + child: boardContainer, + ), + */ child: boardContainer, onTapUp: (d) { final gridWidth = (width - padding * 2); diff --git a/src/ui/flutter_app/lib/widgets/drawer_user_controller.dart b/src/ui/flutter_app/lib/widgets/drawer_user_controller.dart index 80ecc8f1..40355f77 100644 --- a/src/ui/flutter_app/lib/widgets/drawer_user_controller.dart +++ b/src/ui/flutter_app/lib/widgets/drawer_user_controller.dart @@ -18,6 +18,7 @@ import 'package:flutter/material.dart'; import 'package:sanmill/common/config.dart'; +import 'package:sanmill/generated/l10n.dart'; import 'package:sanmill/l10n/resources.dart'; import 'package:sanmill/style/app_theme.dart'; import 'package:sanmill/widgets/home_drawer.dart'; @@ -127,10 +128,13 @@ class _DrawerUserControllerState extends State // if you use your own menu view UI you add form initialization child: widget.menuView != null ? widget.menuView - : AnimatedIcon( - icon: widget.animatedIconData, - color: AppTheme.drawerAnimationIconColor, - progress: iconAnimationController), + : Semantics( + label: S.of(context).mainMenu, + child: AnimatedIcon( + icon: widget.animatedIconData, + color: AppTheme.drawerAnimationIconColor, + progress: iconAnimationController), + ), ), onTap: () { FocusScope.of(context).requestFocus(FocusNode()); diff --git a/src/ui/flutter_app/lib/widgets/game_page.dart b/src/ui/flutter_app/lib/widgets/game_page.dart index ce19e633..08670e48 100644 --- a/src/ui/flutter_app/lib/widgets/game_page.dart +++ b/src/ui/flutter_app/lib/widgets/game_page.dart @@ -144,8 +144,12 @@ class _GamePageState extends State showTip(String? tip) { if (!mounted) return; - if (tip != null) print("[tip] $tip"); - + if (tip != null) { + print("[tip] $tip"); + if (Config.screenReaderSupport) { + //showSnackBar(context, tip); + } + } setState(() => _tip = tip); } @@ -429,6 +433,19 @@ class _GamePageState extends State position.recorder.prune(); position.recorder.moveIn(m, position); + if (Config.screenReaderSupport && m.notation != null) { + /* + var playerName = ""; + if (position.sideToMove() == PieceColor.white) { + playerName = S.of(context).player + " 1"; + } else if (position.sideToMove() == PieceColor.black) { + playerName = S.of(context).player + " 2"; + } + */ + + showSnackBar(context, S.of(context).human + ": " + m.notation!); + } + setState(() {}); if (position.winner == PieceColor.nobody) { @@ -518,6 +535,9 @@ class _GamePageState extends State Game.instance.doMove(move.move); showTips(); + if (Config.screenReaderSupport && move.notation != null) { + showSnackBar(context, S.of(context).ai + ": " + move.notation!); + } break; case 'timeout': if (mounted) { @@ -1511,11 +1531,14 @@ class _GamePageState extends State child: Column( // Replace with a Row for horizontal icon + text children: [ - Icon( - ltr - ? FluentIcons.arrow_previous_24_regular - : FluentIcons.arrow_next_24_regular, - color: Color(Config.navigationToolbarIconColor), + Semantics( + label: S.of(context).takeBackAll, + child: Icon( + ltr + ? FluentIcons.arrow_previous_24_regular + : FluentIcons.arrow_next_24_regular, + color: Color(Config.navigationToolbarIconColor), + ), ), ], ), @@ -1526,11 +1549,14 @@ class _GamePageState extends State child: Column( // Replace with a Row for horizontal icon + text children: [ - Icon( - ltr - ? FluentIcons.chevron_left_24_regular - : FluentIcons.chevron_right_24_regular, - color: Color(Config.navigationToolbarIconColor), + Semantics( + label: S.of(context).takeBack, + child: Icon( + ltr + ? FluentIcons.chevron_left_24_regular + : FluentIcons.chevron_right_24_regular, + color: Color(Config.navigationToolbarIconColor), + ), ), ], ), @@ -1541,11 +1567,14 @@ class _GamePageState extends State child: Column( // Replace with a Row for horizontal icon + text children: [ - Icon( - ltr - ? FluentIcons.chevron_right_24_regular - : FluentIcons.chevron_left_24_regular, - color: Color(Config.navigationToolbarIconColor), + Semantics( + label: S.of(context).stepForward, + child: Icon( + ltr + ? FluentIcons.chevron_right_24_regular + : FluentIcons.chevron_left_24_regular, + color: Color(Config.navigationToolbarIconColor), + ), ), ], ), @@ -1556,11 +1585,14 @@ class _GamePageState extends State child: Column( // Replace with a Row for horizontal icon + text children: [ - Icon( - ltr - ? FluentIcons.arrow_next_24_regular - : FluentIcons.arrow_previous_24_regular, - color: Color(Config.navigationToolbarIconColor), + Semantics( + label: S.of(context).stepForwardAll, + child: Icon( + ltr + ? FluentIcons.arrow_next_24_regular + : FluentIcons.arrow_previous_24_regular, + color: Color(Config.navigationToolbarIconColor), + ), ), ], ), @@ -1612,7 +1644,7 @@ class _GamePageState extends State return Scaffold( backgroundColor: Color(Config.darkBackgroundColor), body: Column(children: [ - header, + BlockSemantics(child: header), board, Config.isHistoryNavigationToolbarShown ? historyNavToolbar diff --git a/src/ui/flutter_app/lib/widgets/game_settings_page.dart b/src/ui/flutter_app/lib/widgets/game_settings_page.dart index a6196f77..6359288c 100644 --- a/src/ui/flutter_app/lib/widgets/game_settings_page.dart +++ b/src/ui/flutter_app/lib/widgets/game_settings_page.dart @@ -183,6 +183,20 @@ class _GameSettingsPageState extends State { List children(BuildContext context) { return [ + Text(S.of(context).whoMovesFirst, style: AppTheme.settingsHeaderStyle), + SettingsCard( + context: context, + children: [ + SettingsSwitchListTile( + context: context, + value: !Config.aiMovesFirst, + onChanged: setWhoMovesFirst, + titleString: + Config.aiMovesFirst ? S.of(context).ai : S.of(context).human, + ), + ], + ), + SizedBox(height: AppTheme.sizedBoxHeight), Text(S.of(context).difficulty, style: AppTheme.settingsHeaderStyle), SettingsCard( context: context, @@ -261,16 +275,15 @@ class _GameSettingsPageState extends State { ) : Container(height: 0.0, width: 0.0), SizedBox(height: AppTheme.sizedBoxHeight), - Text(S.of(context).whoMovesFirst, style: AppTheme.settingsHeaderStyle), + Text(S.of(context).accessibility, style: AppTheme.settingsHeaderStyle), SettingsCard( context: context, children: [ SettingsSwitchListTile( context: context, - value: !Config.aiMovesFirst, - onChanged: setWhoMovesFirst, - titleString: - Config.aiMovesFirst ? S.of(context).ai : S.of(context).human, + value: Config.screenReaderSupport, + onChanged: setScreenReaderSupport, + titleString: S.of(context).screenReaderSupport, ), ], ), @@ -483,6 +496,16 @@ class _GameSettingsPageState extends State { Config.save(); } + setScreenReaderSupport(bool value) async { + setState(() { + Config.screenReaderSupport = value; + }); + + print("[config] screenReaderSupport: $value"); + + Config.save(); + } + setDeveloperMode(bool value) async { setState(() { Config.developerMode = value;