diff --git a/lib/src/model/game/create_game_service.dart b/lib/src/model/game/create_game_service.dart index 49cbec3971..637559f2cc 100644 --- a/lib/src/model/game/create_game_service.dart +++ b/lib/src/model/game/create_game_service.dart @@ -9,8 +9,6 @@ import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/lobby/lobby_repository.dart'; import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; import 'package:lichess_mobile/src/model/auth/auth_socket.dart'; -import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/settings/play_preferences.dart'; part 'create_game_service.g.dart'; @@ -27,15 +25,12 @@ class CreateGameService { StreamSubscription? _socketSubscription; - /// Create a new online game seek based on saved preferences. - Future newLobbyGame() async { + Future newLobbyGame(GameSeek seek) async { if (_socketSubscription != null) { throw StateError('Already creating a game.'); } - final session = ref.read(authSessionProvider); final socket = ref.read(authSocketProvider); - final playPref = ref.read(playPreferencesProvider); final lobbyRepo = ref.read(lobbyRepositoryProvider); final (stream, socketReady) = socket.connect(Uri(path: '/lobby/socket/v5')); final completer = Completer(); @@ -55,11 +50,7 @@ class CreateGameService { await Result.release( lobbyRepo.createSeek( - GameSeek( - time: Duration(seconds: playPref.timeIncrement.time), - increment: Duration(seconds: playPref.timeIncrement.increment), - rated: session != null, - ), + seek, sri: socket.sri, ), ); diff --git a/lib/src/model/lobby/game_seek.dart b/lib/src/model/lobby/game_seek.dart index 7969fd5772..66ed59ddcc 100644 --- a/lib/src/model/lobby/game_seek.dart +++ b/lib/src/model/lobby/game_seek.dart @@ -1,13 +1,18 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:dartchess/dartchess.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/model/common/time_increment.dart'; +import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/settings/play_preferences.dart'; part 'game_seek.freezed.dart'; @freezed class GameSeek with _$GameSeek { const GameSeek._(); + const factory GameSeek({ required Duration time, required Duration increment, @@ -16,6 +21,42 @@ class GameSeek with _$GameSeek { Side? side, }) = _GameSeek; + factory GameSeek.fastPairingSeekFromPrefs( + PlayPrefs playPref, + AuthSessionState? session, + ) { + return GameSeek( + time: Duration(seconds: playPref.timeIncrement.time), + increment: Duration(seconds: playPref.timeIncrement.increment), + rated: session != null, + ); + } + + factory GameSeek.customSeekFromPrefs( + PlayPrefs playPref, + AuthSessionState? session, + ) { + return GameSeek( + time: Duration(seconds: playPref.customTimeSeconds), + increment: Duration(seconds: playPref.customIncrementSeconds), + rated: session != null && playPref.customRated, + variant: playPref.customVariant, + side: playPref.customRated == true || + playPref.customSide == PlayableSide.random + ? null + : playPref.customSide == PlayableSide.white + ? Side.white + : Side.black, + ); + } + + TimeIncrement get timeIncrement => TimeIncrement( + time.inSeconds, + increment.inSeconds, + ); + + Speed get speed => Speed.fromTimeIncrement(timeIncrement); + Map get requestBody => { 'time': (time.inSeconds / 60).toString(), 'increment': increment.inSeconds.toString(), diff --git a/lib/src/model/game/lobby_game.dart b/lib/src/model/lobby/lobby_game.dart similarity index 88% rename from lib/src/model/game/lobby_game.dart rename to lib/src/model/lobby/lobby_game.dart index 8c62f4d935..c45ae620f8 100644 --- a/lib/src/model/game/lobby_game.dart +++ b/lib/src/model/lobby/lobby_game.dart @@ -3,6 +3,8 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/create_game_service.dart'; +import 'game_seek.dart'; + part 'lobby_game.g.dart'; /// The [LobbyGame] provider is used to create a new online game from the lobby @@ -16,20 +18,20 @@ class LobbyGame extends _$LobbyGame { late Object? _key; @override - Future build() { + Future build(GameSeek seek) { _key = Object(); ref.onDispose(() { _service.cancel(); _key = null; }); - return _service.newLobbyGame(); + return _service.newLobbyGame(seek); } Future newOpponent() async { final key = _key; state = const AsyncValue.loading(); final newState = await AsyncValue.guard(() { - return _service.newLobbyGame(); + return _service.newLobbyGame(seek); }); // mounted property check logic from: // https://github.com/rrousselGit/riverpod/issues/1879#issuecomment-1303189191 diff --git a/lib/src/model/settings/play_preferences.dart b/lib/src/model/settings/play_preferences.dart index 9ce56c0ce7..7cb169bbd7 100644 --- a/lib/src/model/settings/play_preferences.dart +++ b/lib/src/model/settings/play_preferences.dart @@ -1,8 +1,6 @@ import 'dart:convert'; -import 'package:flutter/widgets.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:dartchess/dartchess.dart'; import 'package:lichess_mobile/src/db/shared_preferences.dart'; import 'package:lichess_mobile/src/model/common/time_increment.dart'; @@ -13,6 +11,8 @@ part 'play_preferences.g.dart'; const _prefKey = 'preferences.play'; +enum PlayableSide { random, white, black } + @Riverpod(keepAlive: true) class PlayPreferences extends _$PlayPreferences { @override @@ -46,7 +46,7 @@ class PlayPreferences extends _$PlayPreferences { return _save(state.copyWith(customRated: rated)); } - Future setCustomSide(Side? side) { + Future setCustomSide(PlayableSide side) { return _save(state.copyWith(customSide: side)); } @@ -73,7 +73,7 @@ class PlayPrefs with _$PlayPrefs { required int customIncrementSeconds, required Variant customVariant, required bool customRated, - Side? customSide, + required PlayableSide customSide, }) = _PlayPrefs; static const defaults = PlayPrefs( @@ -82,7 +82,7 @@ class PlayPrefs with _$PlayPrefs { customIncrementSeconds: 0, customVariant: Variant.standard, customRated: false, - customSide: null, + customSide: PlayableSide.random, ); factory PlayPrefs.fromJson(Map json) { @@ -92,8 +92,6 @@ class PlayPrefs with _$PlayPrefs { return defaults; } } - - IconData get speedIcon => timeIncrement.speed.icon; } const kAvailableTimesInSeconds = [ diff --git a/lib/src/view/game/game_screen.dart b/lib/src/view/game/game_screen.dart index deac7b5463..80cd759e0e 100644 --- a/lib/src/view/game/game_screen.dart +++ b/lib/src/view/game/game_screen.dart @@ -6,11 +6,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:dartchess/dartchess.dart'; import 'package:chessground/chessground.dart' as cg; -import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/auth/auth_socket.dart'; import 'package:lichess_mobile/src/model/game/game_ctrl.dart'; -import 'package:lichess_mobile/src/model/game/lobby_game.dart'; import 'package:lichess_mobile/src/model/game/game_status.dart'; +import 'package:lichess_mobile/src/model/lobby/lobby_game.dart'; +import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; import 'package:lichess_mobile/src/model/settings/play_preferences.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; @@ -39,9 +39,12 @@ final RouteObserver> gameRouteObserver = class GameScreen extends ConsumerStatefulWidget { const GameScreen({ + required this.seek, super.key, }); + final GameSeek seek; + @override ConsumerState createState() => _GameScreenState(); } @@ -70,7 +73,8 @@ class _GameScreenState extends ConsumerState @override Widget build(BuildContext context) { - final gameId = ref.watch(lobbyGameProvider); + final gameProvider = lobbyGameProvider(widget.seek); + final gameId = ref.watch(gameProvider); final playPrefs = ref.watch(playPreferencesProvider); return gameId.when( @@ -79,7 +83,11 @@ class _GameScreenState extends ConsumerState final gameState = ref.watch(ctrlProvider); return gameState.when( data: (state) { - final body = _Body(gameState: state, ctrlProvider: ctrlProvider); + final body = _Body( + gameState: state, + ctrlProvider: ctrlProvider, + gameProvider: gameProvider, + ); return PlatformWidget( androidBuilder: (context) => _androidBuilder( context: context, @@ -158,7 +166,7 @@ class _GameScreenState extends ConsumerState child: PingRating(size: 24.0), ) : null, - title: _GameTitle(playPrefs: playPrefs), + title: _GameTitle(seek: widget.seek), actions: [ SettingsButton( onPressed: () => showAdaptiveBottomSheet( @@ -188,7 +196,7 @@ class _GameScreenState extends ConsumerState child: PingRating(size: 24.0), ) : null, - middle: _GameTitle(playPrefs: playPrefs), + middle: _GameTitle(seek: widget.seek), trailing: SettingsButton( onPressed: () => showAdaptiveBottomSheet( context: context, @@ -204,24 +212,24 @@ class _GameScreenState extends ConsumerState class _GameTitle extends ConsumerWidget { const _GameTitle({ - required this.playPrefs, + required this.seek, }); - final PlayPrefs playPrefs; + final GameSeek seek; @override Widget build(BuildContext context, WidgetRef ref) { - final session = ref.watch(authSessionProvider); - final mode = session == null ? '' : ' • ${context.l10n.rated}'; + final mode = + seek.rated ? ' • ${context.l10n.rated}' : ' • ${context.l10n.casual}'; return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( - playPrefs.speedIcon, + seek.speed.icon, color: DefaultTextStyle.of(context).style.color, ), const SizedBox(width: 4.0), - Text('${playPrefs.timeIncrement.display}$mode'), + Text('${seek.timeIncrement.display}$mode'), ], ); } @@ -231,10 +239,12 @@ class _Body extends ConsumerWidget { const _Body({ required this.gameState, required this.ctrlProvider, + required this.gameProvider, }); final GameCtrlState gameState; final GameCtrlProvider ctrlProvider; + final LobbyGameProvider gameProvider; @override Widget build(BuildContext context, WidgetRef ref) { @@ -247,6 +257,7 @@ class _Body extends ConsumerWidget { context: context, builder: (context) => _GameEndDialog( ctrlProvider: ctrlProvider, + gameProvider: gameProvider, ), barrierDismissible: true, ); @@ -268,7 +279,7 @@ class _Body extends ConsumerWidget { // Be sure to pop any dialogs that might be on top of the game screen. Navigator.of(context).popUntil((route) => route is! RawDialogRoute); ref - .read(lobbyGameProvider.notifier) + .read(gameProvider.notifier) .rematch(state.requireValue.redirectGameId!); } } @@ -365,7 +376,11 @@ class _Body extends ConsumerWidget { ), ), ), - _GameBottomBar(gameState: gameState, ctrlProvider: ctrlProvider), + _GameBottomBar( + gameState: gameState, + ctrlProvider: ctrlProvider, + gameProvider: gameProvider, + ), ], ); @@ -440,10 +455,12 @@ class _GameBottomBar extends ConsumerWidget { const _GameBottomBar({ required this.gameState, required this.ctrlProvider, + required this.gameProvider, }); final GameCtrlState gameState; final GameCtrlProvider ctrlProvider; + final LobbyGameProvider gameProvider; @override Widget build(BuildContext context, WidgetRef ref) { @@ -694,7 +711,7 @@ class _GameBottomBar extends ConsumerWidget { BottomSheetAction( label: Text(context.l10n.newOpponent), onPressed: (_) { - ref.read(lobbyGameProvider.notifier).newOpponent(); + ref.read(gameProvider.notifier).newOpponent(); }, ), ], @@ -705,9 +722,11 @@ class _GameBottomBar extends ConsumerWidget { class _GameEndDialog extends ConsumerStatefulWidget { const _GameEndDialog({ required this.ctrlProvider, + required this.gameProvider, }); final GameCtrlProvider ctrlProvider; + final LobbyGameProvider gameProvider; @override ConsumerState<_GameEndDialog> createState() => _GameEndDialogState(); @@ -796,7 +815,7 @@ class _GameEndDialogState extends ConsumerState<_GameEndDialog> { semanticsLabel: context.l10n.newOpponent, onPressed: _activateButtons ? () { - ref.read(lobbyGameProvider.notifier).newOpponent(); + ref.read(widget.gameProvider.notifier).newOpponent(); // Other alert dialogs may be shown before this one, so be sure to pop them all Navigator.of(context) .popUntil((route) => route is! RawDialogRoute); diff --git a/lib/src/view/home/home_screen.dart b/lib/src/view/home/home_screen.dart index 4284318b84..1d98e13767 100644 --- a/lib/src/view/home/home_screen.dart +++ b/lib/src/view/home/home_screen.dart @@ -16,6 +16,7 @@ import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/bottom_navigation.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_service.dart'; @@ -340,8 +341,8 @@ class _CreateAGame extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final timeControlPref = ref - .watch(playPreferencesProvider.select((prefs) => prefs.timeIncrement)); + final playPrefs = ref.watch(playPreferencesProvider); + final session = ref.watch(authSessionProvider); return SmallBoardPreview( orientation: Side.white.cg, fen: kInitialFEN, @@ -356,12 +357,12 @@ class _CreateAGame extends ConsumerWidget { Row( children: [ Icon( - timeControlPref.speed.icon, + playPrefs.timeIncrement.speed.icon, size: 20, color: DefaultTextStyle.of(context).style.color, ), const SizedBox(width: 5), - Text(timeControlPref.display, style: Styles.timeControl), + Text(playPrefs.timeIncrement.display, style: Styles.timeControl), ], ), ], @@ -371,7 +372,9 @@ class _CreateAGame extends ConsumerWidget { context, rootNavigator: true, builder: (BuildContext context) { - return const GameScreen(); + return GameScreen( + seek: GameSeek.fastPairingSeekFromPrefs(playPrefs, session), + ); }, ); }, diff --git a/lib/src/view/play/custom_play_screen.dart b/lib/src/view/play/custom_play_screen.dart index f54b74b26e..16632aae00 100644 --- a/lib/src/view/play/custom_play_screen.dart +++ b/lib/src/view/play/custom_play_screen.dart @@ -10,6 +10,7 @@ import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; import 'package:lichess_mobile/src/model/settings/play_preferences.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/view/game/game_screen.dart'; @@ -141,6 +142,27 @@ class _Body extends ConsumerWidget { }, ), ), + if (preferences.customRated == false) + PlatformListTile( + title: Text(context.l10n.side), + trailing: AdaptiveTextButton( + onPressed: () { + showChoicePicker( + context, + choices: PlayableSide.values, + selectedItem: preferences.customSide, + labelBuilder: (PlayableSide side) => + Text(_customSideLabel(context, side)), + onSelectedItemChanged: (PlayableSide side) { + ref + .read(playPreferencesProvider.notifier) + .setCustomSide(side); + }, + ); + }, + child: Text(_customSideLabel(context, preferences.customSide)), + ), + ), const SizedBox(height: 20), Padding( padding: const EdgeInsets.symmetric(horizontal: 20.0), @@ -152,7 +174,12 @@ class _Body extends ConsumerWidget { context, rootNavigator: true, builder: (BuildContext context) { - return const GameScreen(); + return GameScreen( + seek: GameSeek.customSeekFromPrefs( + preferences, + session, + ), + ); }, ); } @@ -180,3 +207,14 @@ String _clockTimeLabel(num seconds) { return '${(seconds / 60).floor()}'; } } + +String _customSideLabel(BuildContext context, PlayableSide side) { + switch (side) { + case PlayableSide.white: + return context.l10n.white; + case PlayableSide.black: + return context.l10n.black; + case PlayableSide.random: + return context.l10n.randomColor; + } +} diff --git a/lib/src/view/play/play_screen.dart b/lib/src/view/play/play_screen.dart index 5bfa6a8718..7b8601677b 100644 --- a/lib/src/view/play/play_screen.dart +++ b/lib/src/view/play/play_screen.dart @@ -9,6 +9,8 @@ import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; import 'package:lichess_mobile/src/model/settings/play_preferences.dart'; import 'package:lichess_mobile/src/view/game/game_screen.dart'; @@ -45,6 +47,8 @@ class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final playPrefs = ref.watch(playPreferencesProvider); + final session = ref.watch(authSessionProvider); return Center( child: Column( mainAxisSize: MainAxisSize.max, @@ -65,7 +69,10 @@ class _Body extends ConsumerWidget { context, rootNavigator: true, builder: (BuildContext context) { - return const GameScreen(); + return GameScreen( + seek: + GameSeek.fastPairingSeekFromPrefs(playPrefs, session), + ); }, ); },