Improve accessibility (WIP)
* Add Accessibility settings * Support screen reader (WIP) * Only support German/English/Chinese
This commit is contained in:
parent
28fd9fb0a2
commit
ff7d860359
|
@ -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;
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue