diff --git a/lib/src/model/account/account_preferences.dart b/lib/src/model/account/account_preferences.dart index c0f4506194..d640b9ee45 100644 --- a/lib/src/model/account/account_preferences.dart +++ b/lib/src/model/account/account_preferences.dart @@ -14,7 +14,7 @@ typedef AccountPrefState = // game display Zen zenMode, PieceNotation pieceNotation, - BooleanPref showRatings, + ShowRatings showRatings, // game behavior BooleanPref premove, AutoQueen autoQueen, @@ -32,9 +32,9 @@ typedef AccountPrefState = /// A provider that tells if the user wants to see ratings in the app. @Riverpod(keepAlive: true) -Future showRatingsPref(Ref ref) async { +Future showRatingsPref(Ref ref) async { return ref.watch( - accountPreferencesProvider.selectAsync((state) => state?.showRatings.value ?? true), + accountPreferencesProvider.selectAsync((state) => state?.showRatings ?? ShowRatings.yes), ); } @@ -57,7 +57,7 @@ Future pieceNotation(Ref ref) async { final defaultAccountPreferences = ( zenMode: Zen.no, pieceNotation: PieceNotation.symbol, - showRatings: const BooleanPref(true), + showRatings: ShowRatings.yes, premove: const BooleanPref(true), autoQueen: AutoQueen.premove, autoThreefold: AutoThreefold.always, @@ -94,7 +94,7 @@ class AccountPreferences extends _$AccountPreferences { Future setZen(Zen value) => _setPref('zen', value); Future setPieceNotation(PieceNotation value) => _setPref('pieceNotation', value); - Future setShowRatings(BooleanPref value) => _setPref('ratings', value); + Future setShowRatings(ShowRatings value) => _setPref('ratings', value); Future setPremove(BooleanPref value) => _setPref('premove', value); Future setTakeback(Takeback value) => _setPref('takeback', value); @@ -178,6 +178,44 @@ enum Zen implements AccountPref { } } +enum ShowRatings implements AccountPref { + no(0), + yes(1), + exceptInGame(2); + + const ShowRatings(this.value); + + @override + final int value; + + @override + String get toFormData => value.toString(); + + String label(BuildContext context) { + switch (this) { + case ShowRatings.no: + return context.l10n.no; + case ShowRatings.yes: + return context.l10n.yes; + case ShowRatings.exceptInGame: + return context.l10n.preferencesExceptInGame; + } + } + + static ShowRatings fromInt(int value) { + switch (value) { + case 0: + return ShowRatings.no; + case 1: + return ShowRatings.yes; + case 2: + return ShowRatings.exceptInGame; + default: + throw Exception('Invalid value for ShowRatings'); + } + } +} + enum PieceNotation implements AccountPref { symbol(0), letter(1); diff --git a/lib/src/model/account/account_repository.dart b/lib/src/model/account/account_repository.dart index b41cdb7f5b..499dbf14f6 100644 --- a/lib/src/model/account/account_repository.dart +++ b/lib/src/model/account/account_repository.dart @@ -114,7 +114,7 @@ AccountPrefState _accountPreferencesFromPick(RequiredPick pick) { return ( zenMode: Zen.fromInt(pick('zen').asIntOrThrow()), pieceNotation: PieceNotation.fromInt(pick('pieceNotation').asIntOrThrow()), - showRatings: BooleanPref.fromInt(pick('ratings').asIntOrThrow()), + showRatings: ShowRatings.fromInt(pick('ratings').asIntOrThrow()), premove: BooleanPref(pick('premove').asBoolOrThrow()), autoQueen: AutoQueen.fromInt(pick('autoQueen').asIntOrThrow()), autoThreefold: AutoThreefold.fromInt(pick('autoThreefold').asIntOrThrow()), diff --git a/lib/src/model/correspondence/correspondence_game_storage.dart b/lib/src/model/correspondence/correspondence_game_storage.dart index 7b8aaf90c5..991e17a927 100644 --- a/lib/src/model/correspondence/correspondence_game_storage.dart +++ b/lib/src/model/correspondence/correspondence_game_storage.dart @@ -114,7 +114,7 @@ class CorrespondenceGameStorage { Future save(OfflineCorrespondenceGame game) async { try { await _db.insert(kCorrespondenceStorageTable, { - 'userId': game.me.user?.id.toString() ?? kCorrespondenceStorageAnonId, + 'userId': game.me?.user?.id.toString() ?? kCorrespondenceStorageAnonId, 'gameId': game.id.toString(), 'lastModified': DateTime.now().toIso8601String(), 'data': jsonEncode(game.toJson()), diff --git a/lib/src/model/correspondence/correspondence_service.dart b/lib/src/model/correspondence/correspondence_service.dart index f8fa995758..84bb3c1607 100644 --- a/lib/src/model/correspondence/correspondence_service.dart +++ b/lib/src/model/correspondence/correspondence_service.dart @@ -218,7 +218,7 @@ class CorrespondenceService { perf: game.meta.perf, white: game.white, black: game.black, - youAre: game.youAre!, + youAre: game.youAre, daysPerTurn: game.meta.daysPerTurn, clock: game.correspondenceClock, winner: game.winner, diff --git a/lib/src/model/correspondence/offline_correspondence_game.dart b/lib/src/model/correspondence/offline_correspondence_game.dart index 83a1d6a354..189e286e1b 100644 --- a/lib/src/model/correspondence/offline_correspondence_game.dart +++ b/lib/src/model/correspondence/offline_correspondence_game.dart @@ -14,6 +14,9 @@ part 'offline_correspondence_game.freezed.dart'; part 'offline_correspondence_game.g.dart'; /// An offline correspondence game. +/// +/// This is always a game of the current user, so [youAre], [me] and [opponent] +/// are always guaranteed to be non-null. @Freezed(fromJson: true, toJson: true) class OfflineCorrespondenceGame with _$OfflineCorrespondenceGame, BaseGame, IndexableSteps @@ -35,7 +38,7 @@ class OfflineCorrespondenceGame required Perf perf, required Player white, required Player black, - required Side youAre, + required Side? youAre, int? daysPerTurn, Side? winner, bool? isThreefoldRepetition, @@ -45,7 +48,7 @@ class OfflineCorrespondenceGame factory OfflineCorrespondenceGame.fromJson(Map json) => _$OfflineCorrespondenceGameFromJson(json); - Side get orientation => youAre; + Side get orientation => youAre!; @override IList? get clocks => null; @@ -53,14 +56,11 @@ class OfflineCorrespondenceGame @override IList? get evals => null; - Player get me => youAre == Side.white ? white : black; - Player get opponent => youAre == Side.white ? black : white; - Side get sideToMove => lastPosition.turn; bool get isMyTurn => sideToMove == youAre; - Duration? myTimeLeft(DateTime lastModifiedTime) => estimatedTimeLeft(youAre, lastModifiedTime); + Duration? myTimeLeft(DateTime lastModifiedTime) => estimatedTimeLeft(youAre!, lastModifiedTime); Duration? estimatedTimeLeft(Side side, DateTime lastModifiedTime) { final timeLeft = side == Side.white ? clock?.white : clock?.black; @@ -69,8 +69,4 @@ class OfflineCorrespondenceGame } return null; } - - bool get playable => status.value < GameStatus.aborted.value; - bool get playing => status.value > GameStatus.started.value; - bool get finished => status.value >= GameStatus.mate.value; } diff --git a/lib/src/model/game/archived_game.dart b/lib/src/model/game/archived_game.dart index 609802cce8..1a34f67a59 100644 --- a/lib/src/model/game/archived_game.dart +++ b/lib/src/model/game/archived_game.dart @@ -63,22 +63,6 @@ class ArchivedGame with _$ArchivedGame, BaseGame, IndexableSteps implements Base /// Create an archived game from a local storage JSON. factory ArchivedGame.fromJson(Map json) => _$ArchivedGameFromJson(json); - - /// Player point of view. Null if spectating. - Player? get me => - youAre == null - ? null - : youAre == Side.white - ? white - : black; - - /// Opponent point of view. Null if spectating. - Player? get opponent => - youAre == null - ? null - : youAre == Side.white - ? black - : white; } /// A [LightArchivedGame] associated with a point of view of a player. diff --git a/lib/src/model/game/game.dart b/lib/src/model/game/game.dart index d220db2392..aa43d99dfc 100644 --- a/lib/src/model/game/game.dart +++ b/lib/src/model/game/game.dart @@ -38,6 +38,17 @@ abstract mixin class BaseGame { GameStatus get status; + /// Whether the game is properly finished (not aborted). + bool get finished => status.value >= GameStatus.mate.value; + bool get aborted => status == GameStatus.aborted; + + /// Whether the game is still playable (not finished or aborted and not imported). + bool get playable => status.value < GameStatus.aborted.value; + + /// This field is null if the game is being watched as a spectator, and + /// represents the side that the current player is playing as otherwise. + Side? get youAre; + Side? get winner; bool? get isThreefoldRepetition; @@ -45,6 +56,22 @@ abstract mixin class BaseGame { Player get white; Player get black; + /// Player of the playing point of view. Null if spectating. + Player? get me => + youAre == null + ? null + : youAre == Side.white + ? white + : black; + + /// Opponent from the playing point of view. Null if spectating. + Player? get opponent => + youAre == null + ? null + : youAre == Side.white + ? black + : white; + Position get lastPosition; Side? playerSideOf(UserId id) { diff --git a/lib/src/model/game/over_the_board_game.dart b/lib/src/model/game/over_the_board_game.dart index ba8c87db94..6ac44a2388 100644 --- a/lib/src/model/game/over_the_board_game.dart +++ b/lib/src/model/game/over_the_board_game.dart @@ -3,10 +3,11 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/common/eval.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; - import 'package:lichess_mobile/src/model/game/game.dart'; import 'package:lichess_mobile/src/model/game/game_status.dart'; import 'package:lichess_mobile/src/model/game/player.dart'; +import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/utils/string.dart'; part 'over_the_board_game.freezed.dart'; part 'over_the_board_game.g.dart'; @@ -19,9 +20,19 @@ abstract class OverTheBoardGame with _$OverTheBoardGame, BaseGame, IndexableStep const OverTheBoardGame._(); @override - Player get white => const Player(); + Player get white => Player( + onGame: true, + user: LightUser(id: UserId(Side.white.name), name: Side.white.name.capitalize()), + ); + @override - Player get black => const Player(); + Player get black => Player( + onGame: true, + user: LightUser(id: UserId(Side.black.name), name: Side.black.name.capitalize()), + ); + + @override + Side? get youAre => null; @override IList? get evals => null; @@ -40,6 +51,4 @@ abstract class OverTheBoardGame with _$OverTheBoardGame, BaseGame, IndexableStep Side? winner, bool? isThreefoldRepetition, }) = _OverTheBoardGame; - - bool get finished => status.value >= GameStatus.mate.value; } diff --git a/lib/src/model/game/playable_game.dart b/lib/src/model/game/playable_game.dart index 7a99c72834..a9eabfe159 100644 --- a/lib/src/model/game/playable_game.dart +++ b/lib/src/model/game/playable_game.dart @@ -72,22 +72,6 @@ class PlayableGame with _$PlayableGame, BaseGame, IndexableSteps implements Base return _playableGameFromPick(pick(json).required()); } - /// Player of the playing point of view. Null if spectating. - Player? get me => - youAre == null - ? null - : youAre == Side.white - ? white - : black; - - /// Opponent from the playing point of view. Null if spectating. - Player? get opponent => - youAre == null - ? null - : youAre == Side.white - ? black - : white; - Side get sideToMove => lastPosition.turn; bool get hasAI => white.isAI || black.isAI; @@ -97,12 +81,6 @@ class PlayableGame with _$PlayableGame, BaseGame, IndexableSteps implements Base /// Whether it is the current player's turn. bool get isMyTurn => lastPosition.turn == youAre; - /// Whether the game is properly finished (not aborted). - bool get finished => status.value >= GameStatus.mate.value; - bool get aborted => status == GameStatus.aborted; - - /// Whether the game is still playable (not finished or aborted and not imported). - bool get playable => status.value < GameStatus.aborted.value && !imported; bool get abortable => playable && lastPosition.fullmoves <= 1 && diff --git a/lib/src/view/account/rating_pref_aware.dart b/lib/src/view/account/rating_pref_aware.dart index a71faf9acc..6d9bc4710e 100644 --- a/lib/src/view/account/rating_pref_aware.dart +++ b/lib/src/view/account/rating_pref_aware.dart @@ -5,26 +5,33 @@ import 'package:lichess_mobile/src/model/account/account_preferences.dart'; /// A widget that knows if the user has enabled ratings in their settings. class RatingPrefAware extends ConsumerWidget { - /// Creates a widget that displays its child only if the user has enabled ratings + /// Creates a widget that displays its [child] only if the user has enabled ratings /// in their settings. /// /// Optionally, a different [orElse] widget can be displayed if ratings are disabled. - const RatingPrefAware({required this.child, this.orElse, super.key}); + /// + /// If the user has chosen [ShowRatings.exceptInGame] in their settings, + /// the [child] will be hidden if and only if [isActiveGameOfCurrentUser] is true. + /// Set this to `true` if the widget is being used in a screen that shows an active game. + const RatingPrefAware({ + required this.child, + this.orElse = const SizedBox.shrink(), + this.isActiveGameOfCurrentUser = false, + super.key, + }); final Widget child; - final Widget? orElse; + final Widget orElse; + final bool isActiveGameOfCurrentUser; @override Widget build(BuildContext context, WidgetRef ref) { final showRatingAsync = ref.watch(showRatingsPrefProvider); - return showRatingAsync.maybeWhen( - data: (showRatings) { - if (!showRatings) { - return orElse ?? const SizedBox.shrink(); - } - return child; - }, - orElse: () => orElse ?? const SizedBox.shrink(), - ); + + return switch (showRatingAsync) { + AsyncData(value: ShowRatings.yes) => child, + AsyncData(value: ShowRatings.exceptInGame) => isActiveGameOfCurrentUser ? orElse : child, + _ => orElse, + }; } } diff --git a/lib/src/view/analysis/analysis_share_screen.dart b/lib/src/view/analysis/analysis_share_screen.dart index 24ed1b3170..f3c653ae96 100644 --- a/lib/src/view/analysis/analysis_share_screen.dart +++ b/lib/src/view/analysis/analysis_share_screen.dart @@ -108,80 +108,78 @@ class _EditPgnTagsFormState extends ConsumerState<_EditPgnTagsForm> { } } - return showRatingAsync.maybeWhen( - data: (showRatings) { - return SafeArea( - child: Padding( - padding: Styles.verticalBodyPadding, - child: ListView( - children: [ - Column( - children: - pgnHeaders.entries - .where((e) => showRatings || !_ratingHeaders.contains(e.key)) - .mapIndexed((index, e) { - return _EditablePgnField( - entry: e, - controller: _controllers[e.key]!, - focusNode: _focusNodes[e.key]!, - onTap: () { - _controllers[e.key]!.selection = TextSelection( - baseOffset: 0, - extentOffset: _controllers[e.key]!.text.length, + return switch (showRatingAsync) { + AsyncData(:final value) => SafeArea( + child: Padding( + padding: Styles.verticalBodyPadding, + child: ListView( + children: [ + Column( + children: + pgnHeaders.entries + .where((e) => value != ShowRatings.no || !_ratingHeaders.contains(e.key)) + .mapIndexed((index, e) { + return _EditablePgnField( + entry: e, + controller: _controllers[e.key]!, + focusNode: _focusNodes[e.key]!, + onTap: () { + _controllers[e.key]!.selection = TextSelection( + baseOffset: 0, + extentOffset: _controllers[e.key]!.text.length, + ); + if (e.key == 'Result') { + _showResultChoicePicker( + e, + context: context, + onEntryChanged: () { + focusAndSelectNextField(index, pgnHeaders); + }, ); - if (e.key == 'Result') { - _showResultChoicePicker( - e, - context: context, - onEntryChanged: () { - focusAndSelectNextField(index, pgnHeaders); - }, - ); - } else if (e.key == 'Date') { - _showDatePicker( - e, - context: context, - onEntryChanged: () { - focusAndSelectNextField(index, pgnHeaders); - }, - ); - } - }, - onSubmitted: (value) { - ref.read(ctrlProvider.notifier).updatePgnHeader(e.key, value); - focusAndSelectNextField(index, pgnHeaders); - }, - ); - }) - .toList(), - ), - Padding( - padding: const EdgeInsets.all(24.0), - child: Builder( - builder: (context) { - return FatButton( - semanticsLabel: 'Share PGN', - onPressed: () { - launchShareDialog( - context, - text: - ref - .read(analysisControllerProvider(widget.options).notifier) - .makeExportPgn(), + } else if (e.key == 'Date') { + _showDatePicker( + e, + context: context, + onEntryChanged: () { + focusAndSelectNextField(index, pgnHeaders); + }, + ); + } + }, + onSubmitted: (value) { + ref.read(ctrlProvider.notifier).updatePgnHeader(e.key, value); + focusAndSelectNextField(index, pgnHeaders); + }, ); - }, - child: Text(context.l10n.mobileShareGamePGN), - ); - }, - ), + }) + .toList(), + ), + Padding( + padding: const EdgeInsets.all(24.0), + child: Builder( + builder: (context) { + return FatButton( + semanticsLabel: 'Share PGN', + onPressed: () { + launchShareDialog( + context, + text: + ref + .read(analysisControllerProvider(widget.options).notifier) + .makeExportPgn(), + ); + }, + child: Text(context.l10n.mobileShareGamePGN), + ); + }, ), - ], - ), + ), + ], ), - ); - }, - orElse: () => const SizedBox.shrink(), - ); + ), + ), + _ => const SizedBox.shrink(), + }; } Future _showDatePicker( diff --git a/lib/src/view/correspondence/offline_correspondence_game_screen.dart b/lib/src/view/correspondence/offline_correspondence_game_screen.dart index 472a6ed00b..e975171063 100644 --- a/lib/src/view/correspondence/offline_correspondence_game_screen.dart +++ b/lib/src/view/correspondence/offline_correspondence_game_screen.dart @@ -134,7 +134,8 @@ class _BodyState extends ConsumerState<_Body> { final youAre = game.youAre; final black = GamePlayer( - player: game.black, + game: game, + side: Side.black, materialDiff: materialDifference.visible ? game.materialDiffAt(stepCursor, Side.black) : null, materialDifferenceFormat: materialDifference, shouldLinkToUserProfile: false, @@ -152,7 +153,8 @@ class _BodyState extends ConsumerState<_Body> { : null, ); final white = GamePlayer( - player: game.white, + game: game, + side: Side.white, materialDiff: materialDifference.visible ? game.materialDiffAt(stepCursor, Side.white) : null, materialDifferenceFormat: materialDifference, shouldLinkToUserProfile: false, @@ -179,7 +181,7 @@ class _BodyState extends ConsumerState<_Body> { child: SafeArea( bottom: false, child: BoardTable( - orientation: isBoardTurned ? youAre.opposite : youAre, + orientation: isBoardTurned ? youAre!.opposite : youAre!, fen: position.fen, lastMove: game.moveAt(stepCursor) as NormalMove?, gameData: GameData( @@ -227,7 +229,7 @@ class _BodyState extends ConsumerState<_Body> { builder: (_) => AnalysisScreen( options: AnalysisOptions( - orientation: game.youAre, + orientation: game.youAre!, standalone: ( pgn: game.makePgn(), isComputerAnalysisAllowed: false, diff --git a/lib/src/view/game/archived_game_screen.dart b/lib/src/view/game/archived_game_screen.dart index 090851e001..aaa45253b5 100644 --- a/lib/src/view/game/archived_game_screen.dart +++ b/lib/src/view/game/archived_game_screen.dart @@ -184,15 +184,9 @@ class _BoardBody extends ConsumerWidget { final isBoardTurned = ref.watch(isBoardTurnedProvider); final gameCursor = ref.watch(gameCursorProvider(gameData.id)); - final black = GamePlayer(key: const ValueKey('black-player'), player: gameData.black); - final white = GamePlayer(key: const ValueKey('white-player'), player: gameData.white); - final topPlayer = orientation == Side.white ? black : white; - final bottomPlayer = orientation == Side.white ? white : black; final loadingBoard = BoardTable( orientation: (isBoardTurned ? orientation.opposite : orientation), fen: initialCursor == null ? gameData.lastFen ?? kEmptyBoardFEN : kEmptyBoardFEN, - topTable: topPlayer, - bottomTable: bottomPlayer, showMoveListPlaceholder: true, ); @@ -203,13 +197,15 @@ class _BoardBody extends ConsumerWidget { final blackClock = game.archivedBlackClockAt(cursor); final black = GamePlayer( key: const ValueKey('black-player'), - player: gameData.black, + game: game, + side: Side.black, clock: blackClock != null ? Clock(timeLeft: blackClock) : null, materialDiff: game.materialDiffAt(cursor, Side.black), ); final white = GamePlayer( key: const ValueKey('white-player'), - player: gameData.white, + game: game, + side: Side.white, clock: whiteClock != null ? Clock(timeLeft: whiteClock) : null, materialDiff: game.materialDiffAt(cursor, Side.white), ); diff --git a/lib/src/view/game/game_body.dart b/lib/src/view/game/game_body.dart index 4ffa8ccf24..96c20817b3 100644 --- a/lib/src/view/game/game_body.dart +++ b/lib/src/view/game/game_body.dart @@ -109,7 +109,8 @@ class GameBody extends ConsumerWidget { final archivedWhiteClock = gameState.game.archivedWhiteClockAt(gameState.stepCursor); final black = GamePlayer( - player: gameState.game.black, + game: gameState.game, + side: Side.black, materialDiff: boardPreferences.materialDifferenceFormat.visible ? gameState.game.materialDiffAt(gameState.stepCursor, Side.black) @@ -157,7 +158,8 @@ class GameBody extends ConsumerWidget { : null, ); final white = GamePlayer( - player: gameState.game.white, + game: gameState.game, + side: Side.white, materialDiff: boardPreferences.materialDifferenceFormat.visible ? gameState.game.materialDiffAt(gameState.stepCursor, Side.white) diff --git a/lib/src/view/game/game_player.dart b/lib/src/view/game/game_player.dart index e73dc5b354..8560ac48aa 100644 --- a/lib/src/view/game/game_player.dart +++ b/lib/src/view/game/game_player.dart @@ -8,8 +8,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; +import 'package:lichess_mobile/src/model/game/game.dart'; import 'package:lichess_mobile/src/model/game/material_diff.dart'; -import 'package:lichess_mobile/src/model/game/player.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; @@ -26,7 +26,8 @@ import 'package:lichess_mobile/src/widgets/buttons.dart'; /// A widget to display player information above/below the chess board. class GamePlayer extends StatelessWidget { const GamePlayer({ - required this.player, + required this.game, + required this.side, this.clock, this.materialDiff, this.materialDifferenceFormat, @@ -39,7 +40,9 @@ class GamePlayer extends StatelessWidget { super.key, }); - final Player player; + final BaseGame game; + final Side side; + final Widget? clock; final MaterialDiffSide? materialDiff; final MaterialDifferenceFormat? materialDifferenceFormat; @@ -60,6 +63,8 @@ class GamePlayer extends StatelessWidget { final remaingHeight = estimateRemainingHeightLeftBoard(context); final playerFontSize = remaingHeight <= kSmallRemainingHeightLeftBoardThreshold ? 14.0 : 16.0; + final player = game.playerOf(side); + final playerWidget = Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, @@ -119,6 +124,7 @@ class GamePlayer extends StatelessWidget { ], if (player.rating != null) RatingPrefAware( + isActiveGameOfCurrentUser: game.me != null && !game.finished && !game.aborted, child: Text.rich( TextSpan( text: ' ${player.rating}${player.provisional == true ? '?' : ''}', diff --git a/lib/src/view/game/offline_correspondence_games_screen.dart b/lib/src/view/game/offline_correspondence_games_screen.dart index c3aad1e893..90140a96ac 100644 --- a/lib/src/view/game/offline_correspondence_games_screen.dart +++ b/lib/src/view/game/offline_correspondence_games_screen.dart @@ -67,7 +67,7 @@ class OfflineCorrespondenceGamePreview extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - UserFullNameWidget(user: game.opponent.user, style: Styles.boardPreviewTitle), + UserFullNameWidget(user: game.opponent!.user, style: Styles.boardPreviewTitle), if (game.myTimeLeft(lastModified) != null) Text(relativeDate(context.l10n, DateTime.now().add(game.myTimeLeft(lastModified)!))), Icon(game.perf.icon, size: 40, color: DefaultTextStyle.of(context).style.color), diff --git a/lib/src/view/home/home_tab_screen.dart b/lib/src/view/home/home_tab_screen.dart index e357b686b3..00b730d345 100644 --- a/lib/src/view/home/home_tab_screen.dart +++ b/lib/src/view/home/home_tab_screen.dart @@ -537,10 +537,10 @@ class _OfflineCorrespondenceCarousel extends ConsumerWidget { perf: el.$2.perf, speed: el.$2.speed, variant: el.$2.variant, - opponent: el.$2.opponent.user, + opponent: el.$2.opponent!.user, isMyTurn: el.$2.isMyTurn, - opponentRating: el.$2.opponent.rating, - opponentAiLevel: el.$2.opponent.aiLevel, + opponentRating: el.$2.opponent!.rating, + opponentAiLevel: el.$2.opponent!.aiLevel, lastMove: el.$2.lastMove, secondsLeft: el.$2.myTimeLeft(el.$1)?.inSeconds, ), diff --git a/lib/src/view/over_the_board/over_the_board_screen.dart b/lib/src/view/over_the_board/over_the_board_screen.dart index c4e5f82985..4fab242c0a 100644 --- a/lib/src/view/over_the_board/over_the_board_screen.dart +++ b/lib/src/view/over_the_board/over_the_board_screen.dart @@ -7,17 +7,13 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/game/player.dart'; import 'package:lichess_mobile/src/model/over_the_board/over_the_board_clock.dart'; import 'package:lichess_mobile/src/model/over_the_board/over_the_board_game_controller.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/settings/brightness.dart'; import 'package:lichess_mobile/src/model/settings/over_the_board_preferences.dart'; -import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/utils/immersive_mode.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/utils/string.dart'; import 'package:lichess_mobile/src/view/game/game_player.dart'; import 'package:lichess_mobile/src/view/game/game_result_dialog.dart'; import 'package:lichess_mobile/src/view/over_the_board/configure_over_the_board_game.dart'; @@ -276,10 +272,8 @@ class _Player extends ConsumerWidget { return RotatedBox( quarterTurns: upsideDown ? 2 : 0, child: GamePlayer( - player: Player( - onGame: true, - user: LightUser(id: UserId(side.name), name: side.name.capitalize()), - ), + game: gameState.game, + side: side, materialDiff: boardPreferences.materialDifferenceFormat.visible ? gameState.currentMaterialDiff(side) diff --git a/lib/src/view/settings/account_preferences_screen.dart b/lib/src/view/settings/account_preferences_screen.dart index 10031c8be6..ebea0ae9c2 100644 --- a/lib/src/view/settings/account_preferences_screen.dart +++ b/lib/src/view/settings/account_preferences_screen.dart @@ -111,20 +111,37 @@ class _AccountPreferencesScreenState extends ConsumerState ref - .read(accountPreferencesProvider.notifier) - .setShowRatings(BooleanPref(value)), - ); - }, + SettingsListTile( + settingsLabel: Text(context.l10n.preferencesShowPlayerRatings), + settingsValue: data.showRatings.label(context), + showCupertinoTrailingValue: false, + onTap: () { + if (Theme.of(context).platform == TargetPlatform.android) { + showChoicePicker( + context, + choices: ShowRatings.values, + selectedItem: data.showRatings, + labelBuilder: (t) => Text(t.label(context)), + onSelectedItemChanged: + isLoading + ? null + : (ShowRatings? value) { + _setPref( + () => ref + .read(accountPreferencesProvider.notifier) + .setShowRatings(value ?? data.showRatings), + ); + }, + ); + } else { + pushPlatformRoute( + context, + title: context.l10n.preferencesShowPlayerRatings, + builder: (context) => const ShowRatingsSettingsScreen(), + ); + } + }, + explanation: context.l10n.preferencesExplainShowPlayerRatings, ), ], ), @@ -520,6 +537,65 @@ class _PieceNotationSettingsScreenState extends ConsumerState createState() => _ShowRatingsSettingsScreenState(); +} + +class _ShowRatingsSettingsScreenState extends ConsumerState { + Future? _pendingSetShowRatings; + + @override + Widget build(BuildContext context) { + final accountPrefs = ref.watch(accountPreferencesProvider); + return accountPrefs.when( + data: (data) { + if (data == null) { + return Center(child: Text(context.l10n.mobileMustBeLoggedIn)); + } + + return FutureBuilder( + future: _pendingSetShowRatings, + builder: (context, snapshot) { + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + trailing: + snapshot.connectionState == ConnectionState.waiting + ? const CircularProgressIndicator.adaptive() + : null, + ), + child: ListView( + children: [ + ChoicePicker( + choices: ShowRatings.values, + selectedItem: data.showRatings, + titleBuilder: (t) => Text(t.label(context)), + onSelectedItemChanged: + snapshot.connectionState == ConnectionState.waiting + ? null + : (ShowRatings? v) { + final future = ref + .read(accountPreferencesProvider.notifier) + .setShowRatings(v ?? data.showRatings); + setState(() { + _pendingSetShowRatings = future; + }); + }, + ), + ], + ), + ); + }, + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (err, stack) => Center(child: Text(err.toString())), + ); + } +} + class TakebackSettingsScreen extends ConsumerStatefulWidget { const TakebackSettingsScreen({super.key}); diff --git a/lib/src/view/watch/tv_screen.dart b/lib/src/view/watch/tv_screen.dart index 64e2a639c0..cc7e29030e 100644 --- a/lib/src/view/watch/tv_screen.dart +++ b/lib/src/view/watch/tv_screen.dart @@ -86,7 +86,8 @@ class _Body extends ConsumerWidget { final position = gameState.game.positionAt(gameState.stepCursor); final blackPlayerWidget = GamePlayer( - player: game.black.setOnGame(true), + game: game.copyWith(black: game.black.setOnGame(true)), + side: Side.black, clock: gameState.game.clock != null ? CountdownClockBuilder( @@ -106,7 +107,8 @@ class _Body extends ConsumerWidget { materialDiff: game.lastMaterialDiffAt(Side.black), ); final whitePlayerWidget = GamePlayer( - player: game.white.setOnGame(true), + game: game.copyWith(white: game.white.setOnGame(true)), + side: Side.white, clock: gameState.game.clock != null ? CountdownClockBuilder( diff --git a/lib/src/widgets/user_full_name.dart b/lib/src/widgets/user_full_name.dart index f869aeedf0..db71984ae0 100644 --- a/lib/src/widgets/user_full_name.dart +++ b/lib/src/widgets/user_full_name.dart @@ -54,10 +54,10 @@ class UserFullNameWidget extends ConsumerWidget { final provisionalStr = provisional == true ? '?' : ''; final ratingStr = rating != null ? '($rating$provisionalStr)' : null; final showRatingAsync = ref.watch(showRatingsPrefProvider); - final shouldShowRating = showRatingAsync.maybeWhen( - data: (showRating) => showRating, - orElse: () => false, - ); + final shouldShowRating = switch (showRatingAsync) { + AsyncData(:final value) => value != ShowRatings.no, + _ => true, + }; final displayName = user?.name ?? diff --git a/test/test_provider_scope.dart b/test/test_provider_scope.dart index c025467349..b042ee2f9c 100644 --- a/test/test_provider_scope.dart +++ b/test/test_provider_scope.dart @@ -194,7 +194,7 @@ Future makeTestProviderScope( // ignore: scoped_providers_should_specify_dependencies connectivityPluginProvider.overrideWith((_) => FakeConnectivity()), // ignore: scoped_providers_should_specify_dependencies - showRatingsPrefProvider.overrideWith((ref) => true), + showRatingsPrefProvider.overrideWith((ref) => ShowRatings.yes), // ignore: scoped_providers_should_specify_dependencies crashlyticsProvider.overrideWithValue(FakeCrashlytics()), // ignore: scoped_providers_should_specify_dependencies diff --git a/test/view/game/archived_game_screen_test.dart b/test/view/game/archived_game_screen_test.dart index 8350079209..60663ff9fb 100644 --- a/test/view/game/archived_game_screen_test.dart +++ b/test/view/game/archived_game_screen_test.dart @@ -42,7 +42,7 @@ void main() { expect(find.byType(CircularProgressIndicator), findsOneWidget); // wait for game data loading - await tester.pump(const Duration(milliseconds: 100)); + await tester.pumpAndSettle(const Duration(milliseconds: 100)); expect(find.byType(PieceWidget), findsNWidgets(25)); expect(find.widgetWithText(GamePlayer, 'veloce'), findsOneWidget); @@ -61,8 +61,6 @@ void main() { // data shown immediately expect(find.byType(Chessboard), findsOneWidget); expect(find.byType(PieceWidget), findsNWidgets(25)); - expect(find.widgetWithText(GamePlayer, 'veloce'), findsOneWidget); - expect(find.widgetWithText(GamePlayer, 'Stockfish level 1'), findsOneWidget); // cannot interact with board expect(tester.widget(find.byType(Chessboard)).game, null); @@ -82,19 +80,19 @@ void main() { // same info still displayed expect(find.byType(Chessboard), findsOneWidget); expect(find.byType(PieceWidget), findsNWidgets(25)); - expect(find.widgetWithText(GamePlayer, 'veloce'), findsOneWidget); - expect(find.widgetWithText(GamePlayer, 'Stockfish level 1'), findsOneWidget); // now with the clocks expect(find.text('1:46', findRichText: true), findsNWidgets(1)); expect(find.text('0:46', findRichText: true), findsNWidgets(1)); - // moves are loaded + // moves and players are loaded expect(find.byType(MoveList), findsOneWidget); expect( tester.widget(find.byKey(const ValueKey('cursor-back'))).onTap, isNotNull, ); + expect(find.widgetWithText(GamePlayer, 'veloce'), findsOneWidget); + expect(find.widgetWithText(GamePlayer, 'Stockfish level 1'), findsOneWidget); }, variant: kPlatformVariant); testWidgets('navigate game positions', (tester) async { diff --git a/test/view/puzzle/puzzle_screen_test.dart b/test/view/puzzle/puzzle_screen_test.dart index 5789fcb318..6635308c0d 100644 --- a/test/view/puzzle/puzzle_screen_test.dart +++ b/test/view/puzzle/puzzle_screen_test.dart @@ -229,7 +229,7 @@ void main() { await tester.pump(const Duration(seconds: 4)); }); - for (final showRatings in [true, false]) { + for (final showRatings in ShowRatings.values) { testWidgets('fails a puzzle, (showRatings: $showRatings)', variant: kPlatformVariant, ( tester, ) async { @@ -319,9 +319,9 @@ void main() { 'Played ${puzzle2.puzzle.plays.toString().localizeNumbers()} times.'; expect( find.text( - showRatings - ? 'Rating: ${puzzle2.puzzle.rating}. $expectedPlayedXTimes' - : expectedPlayedXTimes, + showRatings == ShowRatings.no + ? expectedPlayedXTimes + : 'Rating: ${puzzle2.puzzle.rating}. $expectedPlayedXTimes', ), findsOneWidget, );