Improve accessibility (WIP)

* Add Accessibility settings
* Support screen reader (WIP)
* Only support German/English/Chinese
This commit is contained in:
Calcitem 2021-08-28 20:50:20 +08:00
parent 28fd9fb0a2
commit ff7d860359
No known key found for this signature in database
GPG Key ID: F2F7C29E054CFB80
9 changed files with 178 additions and 35 deletions

View File

@ -31,6 +31,7 @@ class Config {
// Preferences // Preferences
static bool toneEnabled = true; static bool toneEnabled = true;
static bool keepMuteWhenTakingBack = true; static bool keepMuteWhenTakingBack = true;
static bool screenReaderSupport = false;
static bool aiMovesFirst = false; static bool aiMovesFirst = false;
static bool aiIsLazy = false; static bool aiIsLazy = false;
static int skillLevel = 1; static int skillLevel = 1;
@ -105,6 +106,7 @@ class Config {
// Preferences // Preferences
Config.toneEnabled = settings['ToneEnabled'] ?? true; Config.toneEnabled = settings['ToneEnabled'] ?? true;
Config.keepMuteWhenTakingBack = settings['KeepMuteWhenTakingBack'] ?? true; Config.keepMuteWhenTakingBack = settings['KeepMuteWhenTakingBack'] ?? true;
Config.screenReaderSupport = settings['ScreenReaderSupport'] ?? false;
Config.aiMovesFirst = settings['AiMovesFirst'] ?? false; Config.aiMovesFirst = settings['AiMovesFirst'] ?? false;
Config.aiIsLazy = settings['AiIsLazy'] ?? false; Config.aiIsLazy = settings['AiIsLazy'] ?? false;
Config.skillLevel = settings['SkillLevel'] ?? 1; Config.skillLevel = settings['SkillLevel'] ?? 1;
@ -209,6 +211,7 @@ class Config {
// Preferences // Preferences
settings['ToneEnabled'] = Config.toneEnabled; settings['ToneEnabled'] = Config.toneEnabled;
settings['KeepMuteWhenTakingBack'] = Config.keepMuteWhenTakingBack; settings['KeepMuteWhenTakingBack'] = Config.keepMuteWhenTakingBack;
settings['ScreenReaderSupport'] = Config.screenReaderSupport;
settings['AiMovesFirst'] = Config.aiMovesFirst; settings['AiMovesFirst'] = Config.aiMovesFirst;
settings['AiIsLazy'] = Config.aiIsLazy; settings['AiIsLazy'] = Config.aiIsLazy;
settings['SkillLevel'] = Config.skillLevel; settings['SkillLevel'] = Config.skillLevel;

View File

@ -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": "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": { "@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." "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"
} }
} }

View File

@ -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": "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": { "@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." "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"
} }
} }

View File

@ -1128,5 +1128,21 @@
"drawIfNoRemovalWithinTenMovesWhenThreeLeft_Detail": "当一方只剩下三颗棋子时,而双方都不能在接下来十步之内吃掉对方的棋子,则判为和棋。", "drawIfNoRemovalWithinTenMovesWhenThreeLeft_Detail": "当一方只剩下三颗棋子时,而双方都不能在接下来十步之内吃掉对方的棋子,则判为和棋。",
"@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." "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"
} }
} }

View File

@ -191,7 +191,9 @@ class Audios {
static playTone(var soundId) async { static playTone(var soundId) async {
Chain.capture(() async { Chain.capture(() async {
if (!Config.toneEnabled || isTemporaryMute) { if (!Config.toneEnabled ||
isTemporaryMute ||
Config.screenReaderSupport) {
return; return;
} }

View File

@ -40,10 +40,35 @@ class Board extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
var padding = AppTheme.boardPadding; 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( var container = Container(
margin: EdgeInsets.symmetric( margin: EdgeInsets.symmetric(
vertical: padding, vertical: 0,
horizontal: (width - padding * 2) / 6 / 2 + padding, 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( return GestureDetector(
/*
child: Semantics(
label: S.of(context).board,
child: boardContainer,
),
*/
child: boardContainer, child: boardContainer,
onTapUp: (d) { onTapUp: (d) {
final gridWidth = (width - padding * 2); final gridWidth = (width - padding * 2);

View File

@ -18,6 +18,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:sanmill/common/config.dart'; import 'package:sanmill/common/config.dart';
import 'package:sanmill/generated/l10n.dart';
import 'package:sanmill/l10n/resources.dart'; import 'package:sanmill/l10n/resources.dart';
import 'package:sanmill/style/app_theme.dart'; import 'package:sanmill/style/app_theme.dart';
import 'package:sanmill/widgets/home_drawer.dart'; import 'package:sanmill/widgets/home_drawer.dart';
@ -127,10 +128,13 @@ class _DrawerUserControllerState extends State<DrawerUserController>
// if you use your own menu view UI you add form initialization // if you use your own menu view UI you add form initialization
child: widget.menuView != null child: widget.menuView != null
? widget.menuView ? widget.menuView
: AnimatedIcon( : Semantics(
icon: widget.animatedIconData, label: S.of(context).mainMenu,
color: AppTheme.drawerAnimationIconColor, child: AnimatedIcon(
progress: iconAnimationController), icon: widget.animatedIconData,
color: AppTheme.drawerAnimationIconColor,
progress: iconAnimationController),
),
), ),
onTap: () { onTap: () {
FocusScope.of(context).requestFocus(FocusNode()); FocusScope.of(context).requestFocus(FocusNode());

View File

@ -144,8 +144,12 @@ class _GamePageState extends State<GamePage>
showTip(String? tip) { showTip(String? tip) {
if (!mounted) return; if (!mounted) return;
if (tip != null) print("[tip] $tip"); if (tip != null) {
print("[tip] $tip");
if (Config.screenReaderSupport) {
//showSnackBar(context, tip);
}
}
setState(() => _tip = tip); setState(() => _tip = tip);
} }
@ -429,6 +433,19 @@ class _GamePageState extends State<GamePage>
position.recorder.prune(); position.recorder.prune();
position.recorder.moveIn(m, position); 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(() {}); setState(() {});
if (position.winner == PieceColor.nobody) { if (position.winner == PieceColor.nobody) {
@ -518,6 +535,9 @@ class _GamePageState extends State<GamePage>
Game.instance.doMove(move.move); Game.instance.doMove(move.move);
showTips(); showTips();
if (Config.screenReaderSupport && move.notation != null) {
showSnackBar(context, S.of(context).ai + ": " + move.notation!);
}
break; break;
case 'timeout': case 'timeout':
if (mounted) { if (mounted) {
@ -1511,11 +1531,14 @@ class _GamePageState extends State<GamePage>
child: Column( child: Column(
// Replace with a Row for horizontal icon + text // Replace with a Row for horizontal icon + text
children: <Widget>[ children: <Widget>[
Icon( Semantics(
ltr label: S.of(context).takeBackAll,
? FluentIcons.arrow_previous_24_regular child: Icon(
: FluentIcons.arrow_next_24_regular, ltr
color: Color(Config.navigationToolbarIconColor), ? FluentIcons.arrow_previous_24_regular
: FluentIcons.arrow_next_24_regular,
color: Color(Config.navigationToolbarIconColor),
),
), ),
], ],
), ),
@ -1526,11 +1549,14 @@ class _GamePageState extends State<GamePage>
child: Column( child: Column(
// Replace with a Row for horizontal icon + text // Replace with a Row for horizontal icon + text
children: <Widget>[ children: <Widget>[
Icon( Semantics(
ltr label: S.of(context).takeBack,
? FluentIcons.chevron_left_24_regular child: Icon(
: FluentIcons.chevron_right_24_regular, ltr
color: Color(Config.navigationToolbarIconColor), ? FluentIcons.chevron_left_24_regular
: FluentIcons.chevron_right_24_regular,
color: Color(Config.navigationToolbarIconColor),
),
), ),
], ],
), ),
@ -1541,11 +1567,14 @@ class _GamePageState extends State<GamePage>
child: Column( child: Column(
// Replace with a Row for horizontal icon + text // Replace with a Row for horizontal icon + text
children: <Widget>[ children: <Widget>[
Icon( Semantics(
ltr label: S.of(context).stepForward,
? FluentIcons.chevron_right_24_regular child: Icon(
: FluentIcons.chevron_left_24_regular, ltr
color: Color(Config.navigationToolbarIconColor), ? FluentIcons.chevron_right_24_regular
: FluentIcons.chevron_left_24_regular,
color: Color(Config.navigationToolbarIconColor),
),
), ),
], ],
), ),
@ -1556,11 +1585,14 @@ class _GamePageState extends State<GamePage>
child: Column( child: Column(
// Replace with a Row for horizontal icon + text // Replace with a Row for horizontal icon + text
children: <Widget>[ children: <Widget>[
Icon( Semantics(
ltr label: S.of(context).stepForwardAll,
? FluentIcons.arrow_next_24_regular child: Icon(
: FluentIcons.arrow_previous_24_regular, ltr
color: Color(Config.navigationToolbarIconColor), ? FluentIcons.arrow_next_24_regular
: FluentIcons.arrow_previous_24_regular,
color: Color(Config.navigationToolbarIconColor),
),
), ),
], ],
), ),
@ -1612,7 +1644,7 @@ class _GamePageState extends State<GamePage>
return Scaffold( return Scaffold(
backgroundColor: Color(Config.darkBackgroundColor), backgroundColor: Color(Config.darkBackgroundColor),
body: Column(children: <Widget>[ body: Column(children: <Widget>[
header, BlockSemantics(child: header),
board, board,
Config.isHistoryNavigationToolbarShown Config.isHistoryNavigationToolbarShown
? historyNavToolbar ? historyNavToolbar

View File

@ -183,6 +183,20 @@ class _GameSettingsPageState extends State<GameSettingsPage> {
List<Widget> children(BuildContext context) { List<Widget> children(BuildContext context) {
return <Widget>[ return <Widget>[
Text(S.of(context).whoMovesFirst, style: AppTheme.settingsHeaderStyle),
SettingsCard(
context: context,
children: <Widget>[
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), Text(S.of(context).difficulty, style: AppTheme.settingsHeaderStyle),
SettingsCard( SettingsCard(
context: context, context: context,
@ -261,16 +275,15 @@ class _GameSettingsPageState extends State<GameSettingsPage> {
) )
: Container(height: 0.0, width: 0.0), : Container(height: 0.0, width: 0.0),
SizedBox(height: AppTheme.sizedBoxHeight), SizedBox(height: AppTheme.sizedBoxHeight),
Text(S.of(context).whoMovesFirst, style: AppTheme.settingsHeaderStyle), Text(S.of(context).accessibility, style: AppTheme.settingsHeaderStyle),
SettingsCard( SettingsCard(
context: context, context: context,
children: <Widget>[ children: <Widget>[
SettingsSwitchListTile( SettingsSwitchListTile(
context: context, context: context,
value: !Config.aiMovesFirst, value: Config.screenReaderSupport,
onChanged: setWhoMovesFirst, onChanged: setScreenReaderSupport,
titleString: titleString: S.of(context).screenReaderSupport,
Config.aiMovesFirst ? S.of(context).ai : S.of(context).human,
), ),
], ],
), ),
@ -483,6 +496,16 @@ class _GameSettingsPageState extends State<GameSettingsPage> {
Config.save(); Config.save();
} }
setScreenReaderSupport(bool value) async {
setState(() {
Config.screenReaderSupport = value;
});
print("[config] screenReaderSupport: $value");
Config.save();
}
setDeveloperMode(bool value) async { setDeveloperMode(bool value) async {
setState(() { setState(() {
Config.developerMode = value; Config.developerMode = value;