From 46a21288db24c9bf37c28f570d6bc7f40844b53e Mon Sep 17 00:00:00 2001 From: Albert Ford Date: Wed, 18 Jan 2023 02:09:40 -0800 Subject: [PATCH] Add non visual game board to game screen --- .../game/ui/board/playable_game_screen.dart | 168 ++++++++++-------- lib/src/widgets/non_visual_board.dart | 68 ++++--- lib/src/widgets/non_visual_game_board.dart | 31 +++- 3 files changed, 160 insertions(+), 107 deletions(-) diff --git a/lib/src/features/game/ui/board/playable_game_screen.dart b/lib/src/features/game/ui/board/playable_game_screen.dart index c218b5fb35..64dbb6e5f7 100644 --- a/lib/src/features/game/ui/board/playable_game_screen.dart +++ b/lib/src/features/game/ui/board/playable_game_screen.dart @@ -9,6 +9,7 @@ import 'package:lichess_mobile/src/common/lichess_icons.dart'; import 'package:lichess_mobile/src/utils/chessground_compat.dart'; import 'package:lichess_mobile/src/utils/async_value.dart'; import 'package:lichess_mobile/src/widgets/game_board_layout.dart'; +import 'package:lichess_mobile/src/widgets/non_visual_game_board.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/player.dart'; @@ -169,88 +170,105 @@ class _BoardBody extends ConsumerWidget { final isBoardTurned = ref.watch(isBoardTurnedProvider); final isReplaying = gameState != null && positionCursor < gameState.positionIndex; + final mediaQuery = MediaQuery.of(context); return gameClockStream.when( data: (clock) { - final black = Player( - key: const ValueKey('black-player'), - name: game.black.name, - rating: game.black.rating, - title: game.black.title, - active: gameState != null && - gameState.status == GameStatus.started && - gameState.position.fullmoves > 1 && - gameState.position.turn == Side.black, - clock: clock.blackTime, - ); - final white = Player( - key: const ValueKey('white-player'), - name: game.white.name, - rating: game.white.rating, - title: game.white.title, - active: gameState != null && - gameState.status == GameStatus.started && - gameState.position.fullmoves > 1 && - gameState.position.turn == Side.white, - clock: clock.whiteTime, - ); - final topPlayer = game.orientation == Side.white ? black : white; - final bottomPlayer = game.orientation == Side.white ? white : black; + if (mediaQuery.accessibleNavigation) { + return NonVisualGameBoard( + gameState: gameState, + onMove: (Move move) => + ref.read(gameStateProvider.notifier).onUserMove(game.id, move), + ); + } else { + final black = Player( + key: const ValueKey('black-player'), + name: game.black.name, + rating: game.black.rating, + title: game.black.title, + active: gameState != null && + gameState.status == GameStatus.started && + gameState.position.fullmoves > 1 && + gameState.position.turn == Side.black, + clock: clock.blackTime, + ); + final white = Player( + key: const ValueKey('white-player'), + name: game.white.name, + rating: game.white.rating, + title: game.white.title, + active: gameState != null && + gameState.status == GameStatus.started && + gameState.position.fullmoves > 1 && + gameState.position.turn == Side.white, + clock: clock.whiteTime, + ); + final topPlayer = game.orientation == Side.white ? black : white; + final bottomPlayer = game.orientation == Side.white ? white : black; - return GameBoardLayout( - boardData: cg.BoardData( - interactableSide: - gameState == null || !gameState.playing || isReplaying - ? cg.InteractableSide.none - : game.orientation == Side.white - ? cg.InteractableSide.white - : cg.InteractableSide.black, - orientation: - (isBoardTurned ? game.orientation.opposite : game.orientation) - .cg, - fen: gameState?.positions[positionCursor].fen ?? game.initialFen, - validMoves: gameState?.validMoves, - lastMove: gameState != null && gameState.gameOver - ? positionCursor > 0 - ? gameState.moveAtPly(positionCursor - 1)?.cg - : null - : gameState?.lastMove?.cg, - sideToMove: gameState?.position.turn.cg ?? game.orientation.cg, - onMove: (cg.Move move, {bool? isPremove}) => ref - .read(gameStateProvider.notifier) - .onUserMove(game.id, Move.fromUci(move.uci)!), - ), - topPlayer: topPlayer, - bottomPlayer: bottomPlayer, - moves: gameState?.sanMoves, - currentMoveIndex: positionCursor, - ); + return GameBoardLayout( + boardData: cg.BoardData( + interactableSide: + gameState == null || !gameState.playing || isReplaying + ? cg.InteractableSide.none + : game.orientation == Side.white + ? cg.InteractableSide.white + : cg.InteractableSide.black, + orientation: + (isBoardTurned ? game.orientation.opposite : game.orientation) + .cg, + fen: gameState?.positions[positionCursor].fen ?? game.initialFen, + validMoves: gameState?.validMoves, + lastMove: gameState != null && gameState.gameOver + ? positionCursor > 0 + ? gameState.moveAtPly(positionCursor - 1)?.cg + : null + : gameState?.lastMove?.cg, + sideToMove: gameState?.position.turn.cg ?? game.orientation.cg, + onMove: (cg.Move move, {bool? isPremove}) => ref + .read(gameStateProvider.notifier) + .onUserMove(game.id, Move.fromUci(move.uci)!), + ), + topPlayer: topPlayer, + bottomPlayer: bottomPlayer, + moves: gameState?.sanMoves, + currentMoveIndex: positionCursor, + ); + } }, loading: () { - final player = Player( - name: game.player.name, - rating: game.player.rating, - title: game.player.title, - active: false, - clock: Duration.zero, - ); - final opponent = Player( - name: game.opponent.name, - rating: game.opponent.rating, - title: game.opponent.title, - active: false, - clock: Duration.zero, - ); + if (mediaQuery.accessibleNavigation) { + return NonVisualGameBoard( + gameState: null, + onMove: (move) {}, + isLoading: true, + ); + } else { + final player = Player( + name: game.player.name, + rating: game.player.rating, + title: game.player.title, + active: false, + clock: Duration.zero, + ); + final opponent = Player( + name: game.opponent.name, + rating: game.opponent.rating, + title: game.opponent.title, + active: false, + clock: Duration.zero, + ); - return GameBoardLayout( - topPlayer: opponent, - bottomPlayer: player, - boardData: cg.BoardData( - interactableSide: cg.InteractableSide.none, - orientation: game.orientation.cg, - fen: game.initialFen, - ), - ); + return GameBoardLayout( + topPlayer: opponent, + bottomPlayer: player, + boardData: cg.BoardData( + interactableSide: cg.InteractableSide.none, + orientation: game.orientation.cg, + fen: game.initialFen, + ), + ); + } }, error: (err, stackTrace) { debugPrint( diff --git a/lib/src/widgets/non_visual_board.dart b/lib/src/widgets/non_visual_board.dart index eca41cd0b2..345e2c177e 100644 --- a/lib/src/widgets/non_visual_board.dart +++ b/lib/src/widgets/non_visual_board.dart @@ -7,12 +7,18 @@ class NonVisualBoard extends StatefulWidget { required this.position, required this.lastSanMove, required this.handleCommand, + this.isLoading = false, + this.loadCompleteMsg = 'Loading complete', super.key, }); final Position position; - final String lastSanMove; + final String? lastSanMove; + + final bool isLoading; + + final String loadCompleteMsg; /// Callback called when a command is submitted. /// @@ -42,29 +48,45 @@ class _NonVisualBoardState extends State { ScaffoldMessenger.of(context).showSnackBar(snackBar); } - return ListView( - children: [ - const Text('Current position'), - Semantics( - liveRegion: true, - // TODO: decide on a set of parameters - child: Text(renderCurrentPos(widget.lastSanMove, widget.position)), - ), - TextField( - decoration: const InputDecoration( - labelText: 'move input', + if (widget.isLoading) { + return ListView( + children: [ + Semantics( + key: const ValueKey('loading-status'), + liveRegion: true, + child: const Text('Loading'), + ), + ], + ); + } else { + return ListView( + children: [ + Semantics( + key: const ValueKey('loading-status'), + liveRegion: true, + child: Text(widget.loadCompleteMsg), ), - controller: textController, - onSubmitted: (input) { - final message = widget.handleCommand(input); - if (message != null) { - announce(message); - } - textController.clear(); - }, - ), - ], - ); + const Text('Current position'), + Semantics( + liveRegion: true, + child: Text(renderCurrentPos(widget.lastSanMove, widget.position)), + ), + TextField( + decoration: const InputDecoration( + labelText: 'move input', + ), + controller: textController, + onSubmitted: (input) { + final message = widget.handleCommand(input); + if (message != null) { + announce(message); + } + textController.clear(); + }, + ), + ], + ); + } } } diff --git a/lib/src/widgets/non_visual_game_board.dart b/lib/src/widgets/non_visual_game_board.dart index 1a83680be5..94f040f7bf 100644 --- a/lib/src/widgets/non_visual_game_board.dart +++ b/lib/src/widgets/non_visual_game_board.dart @@ -10,38 +10,51 @@ class NonVisualGameBoard extends StatelessWidget { const NonVisualGameBoard({ required this.gameState, required this.onMove, + this.isLoading = false, super.key, }); - final GameState gameState; + final GameState? gameState; final void Function(Move) onMove; + final bool isLoading; + @override Widget build(BuildContext context) { - final board = gameState.position.board; - final lastSanMove = - gameState.sanMoves.isEmpty ? null : gameState.sanMoves.last; + final position = gameState?.position ?? Chess.initial; + final lastSanMove = gameState?.sanMoves.isNotEmpty == true + ? gameState!.sanMoves.last + : null; return NonVisualBoard( - position: gameState.position, - lastSanMove: gameState.sanMoves.last, + position: position, + lastSanMove: lastSanMove, + isLoading: isLoading, + loadCompleteMsg: 'Game loaded', handleCommand: (command) { final lowered = command.toLowerCase(); if (lowered == 'c' || lowered == 'clock') { return 'todo: read clocks'; } else if (lowered == 'l' || lowered == 'last') { - return renderCurrentPos(lastSanMove, gameState.position); + return renderCurrentPos(lastSanMove, position); } else if (lowered == 'o' || lowered == 'opponent') { return 'todo: read player'; } - final pieceResult = handlePieceCommand(command, board); + final pieceResult = handlePieceCommand(command, position.board); if (pieceResult != null) { return pieceResult; } - final scanResult = handleScanCommand(lowered, board); + final scanResult = handleScanCommand(lowered, position.board); if (scanResult != null) { return scanResult; } + // TODO: uncomment when parseSan is available + final move = + Move.fromUci(command) /*?? gameState.position.parseSan(command)*/; + if (move != null) { + onMove(move); + return null; + } return 'Invalid command: $command'; }, );