mirror of
https://github.com/lichess-org/mobile.git
synced 2026-05-26 13:50:52 +00:00
Require trailing comma
This commit is contained in:
@@ -23,7 +23,6 @@ analyzer:
|
||||
|
||||
linter:
|
||||
rules:
|
||||
require_trailing_commas: false
|
||||
directives_ordering: false
|
||||
always_use_package_imports: false
|
||||
avoid_redundant_argument_values: false
|
||||
|
||||
+14
-7
@@ -15,7 +15,8 @@ void main() async {
|
||||
Logger.root.onRecord.listen((record) {
|
||||
final time = DateFormat.Hms().format(record.time);
|
||||
debugPrint(
|
||||
'${record.level.name} at $time [${record.loggerName}] ${record.message}${record.error != null ? '\n${record.error}' : ''}');
|
||||
'${record.level.name} at $time [${record.loggerName}] ${record.message}${record.error != null ? '\n${record.error}' : ''}',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -35,10 +36,12 @@ void main() async {
|
||||
await container.read(authRepositoryProvider).init();
|
||||
await container.read(soundServiceProvider).init();
|
||||
|
||||
runApp(UncontrolledProviderScope(
|
||||
container: container,
|
||||
child: const App(),
|
||||
));
|
||||
runApp(
|
||||
UncontrolledProviderScope(
|
||||
container: container,
|
||||
child: const App(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class ProviderLogger extends ProviderObserver {
|
||||
@@ -51,7 +54,8 @@ class ProviderLogger extends ProviderObserver {
|
||||
ProviderContainer container,
|
||||
) {
|
||||
_logger.info(
|
||||
'${provider.name ?? provider.runtimeType} initialized with: $value.');
|
||||
'${provider.name ?? provider.runtimeType} initialized with: $value.',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -70,6 +74,9 @@ class ProviderLogger extends ProviderObserver {
|
||||
ProviderContainer container,
|
||||
) {
|
||||
_logger.severe(
|
||||
'${provider.name ?? provider.runtimeType} error', error, stackTrace);
|
||||
'${provider.name ?? provider.runtimeType} error',
|
||||
error,
|
||||
stackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+22
-13
@@ -21,8 +21,11 @@ const defaultRetries = [
|
||||
/// Convenient client that captures and returns API errors.
|
||||
class ApiClient {
|
||||
ApiClient(this._log, this._client, {List<Duration> retries = defaultRetries})
|
||||
: _retryClient = RetryClient.withDelays(_client, retries,
|
||||
whenError: (_, __) => true) {
|
||||
: _retryClient = RetryClient.withDelays(
|
||||
_client,
|
||||
retries,
|
||||
whenError: (_, __) => true,
|
||||
) {
|
||||
_log.info('Creating new ApiClient.');
|
||||
}
|
||||
|
||||
@@ -36,8 +39,8 @@ class ApiClient {
|
||||
bool retry = false,
|
||||
}) =>
|
||||
Result.capture(
|
||||
(retry ? _retryClient : _client).get(url, headers: headers))
|
||||
.mapError((error, stackTrace) {
|
||||
(retry ? _retryClient : _client).get(url, headers: headers),
|
||||
).mapError((error, stackTrace) {
|
||||
_log.severe('Request error', error, stackTrace);
|
||||
return GenericIOException();
|
||||
}).flatMap((response) => _validateResponseStatusResult(url, response));
|
||||
@@ -49,9 +52,10 @@ class ApiClient {
|
||||
Encoding? encoding,
|
||||
bool retry = false,
|
||||
}) =>
|
||||
Result.capture((retry ? _retryClient : _client)
|
||||
.post(url, headers: headers, body: body, encoding: encoding))
|
||||
.mapError((error, stackTrace) {
|
||||
Result.capture(
|
||||
(retry ? _retryClient : _client)
|
||||
.post(url, headers: headers, body: body, encoding: encoding),
|
||||
).mapError((error, stackTrace) {
|
||||
_log.severe('Request error', error, stackTrace);
|
||||
return GenericIOException();
|
||||
}).flatMap((response) => _validateResponseStatusResult(url, response));
|
||||
@@ -63,15 +67,18 @@ class ApiClient {
|
||||
Encoding? encoding,
|
||||
bool retry = false,
|
||||
}) =>
|
||||
Result.capture((retry ? _retryClient : _client)
|
||||
.delete(url, headers: headers, body: body, encoding: encoding))
|
||||
.mapError((error, stackTrace) {
|
||||
Result.capture(
|
||||
(retry ? _retryClient : _client)
|
||||
.delete(url, headers: headers, body: body, encoding: encoding),
|
||||
).mapError((error, stackTrace) {
|
||||
_log.severe('Request error', error, stackTrace);
|
||||
return GenericIOException();
|
||||
}).flatMap((response) => _validateResponseStatusResult(url, response));
|
||||
|
||||
Future<StreamedResponse> stream(Uri url,
|
||||
{Map<String, String>? headers}) async {
|
||||
Future<StreamedResponse> stream(
|
||||
Uri url, {
|
||||
Map<String, String>? headers,
|
||||
}) async {
|
||||
final request = Request('GET', url);
|
||||
if (headers != null) {
|
||||
request.headers.addAll(headers);
|
||||
@@ -86,7 +93,9 @@ class ApiClient {
|
||||
}
|
||||
|
||||
Result<T> _validateResponseStatusResult<T extends BaseResponse>(
|
||||
Uri url, T response) {
|
||||
Uri url,
|
||||
T response,
|
||||
) {
|
||||
if (response.statusCode >= 500) {
|
||||
_log.severe('$url responded with status ${response.statusCode}');
|
||||
} else if (response.statusCode >= 400) {
|
||||
|
||||
@@ -14,17 +14,22 @@ NotifierProvider<PrefNotifier<T>, T> createPrefProvider<T>({
|
||||
String Function(T)? mapTo,
|
||||
}) {
|
||||
return NotifierProvider<PrefNotifier<T>, T>(() {
|
||||
return PrefNotifier<T>(prefKey, defaultValue,
|
||||
mapFrom: mapFrom, mapTo: mapTo);
|
||||
return PrefNotifier<T>(
|
||||
prefKey,
|
||||
defaultValue,
|
||||
mapFrom: mapFrom,
|
||||
mapTo: mapTo,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
class PrefNotifier<T> extends Notifier<T> {
|
||||
PrefNotifier(this.prefKey, this.defaultValue, {this.mapFrom, this.mapTo})
|
||||
: assert(
|
||||
(mapFrom == null && mapTo == null) ||
|
||||
(mapFrom != null && mapTo != null),
|
||||
'You must pass both `mapFrom` and `mapTo`, or none.');
|
||||
(mapFrom == null && mapTo == null) ||
|
||||
(mapFrom != null && mapTo != null),
|
||||
'You must pass both `mapFrom` and `mapTo`, or none.',
|
||||
);
|
||||
|
||||
@override
|
||||
T build() {
|
||||
|
||||
@@ -25,7 +25,8 @@ const kLargeScreenWidth = 600;
|
||||
|
||||
// Misc
|
||||
const kPlatformCardBorder = RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10.0)));
|
||||
borderRadius: BorderRadius.all(Radius.circular(10.0)),
|
||||
);
|
||||
|
||||
const kEmptyWidget = SizedBox.shrink();
|
||||
const kEmptyFen = '8/8/8/8/8/8/8/8 w - - 0 1';
|
||||
|
||||
@@ -24,9 +24,12 @@ class AuthError {
|
||||
}
|
||||
|
||||
class AuthRepository {
|
||||
AuthRepository(ApiClient apiClient, FlutterAppAuth appAuth,
|
||||
FlutterSecureStorage storage, Logger log)
|
||||
: _apiClient = apiClient,
|
||||
AuthRepository(
|
||||
ApiClient apiClient,
|
||||
FlutterAppAuth appAuth,
|
||||
FlutterSecureStorage storage,
|
||||
Logger log,
|
||||
) : _apiClient = apiClient,
|
||||
_appAuth = appAuth,
|
||||
_storage = storage,
|
||||
_log = log;
|
||||
@@ -50,20 +53,24 @@ class AuthRepository {
|
||||
|
||||
FutureResult<void> signIn() {
|
||||
final future = (() async {
|
||||
final result =
|
||||
await _appAuth.authorizeAndExchangeCode(AuthorizationTokenRequest(
|
||||
kLichessClientId,
|
||||
redirectUri,
|
||||
allowInsecureConnections: kDebugMode,
|
||||
serviceConfiguration: const AuthorizationServiceConfiguration(
|
||||
final result = await _appAuth.authorizeAndExchangeCode(
|
||||
AuthorizationTokenRequest(
|
||||
kLichessClientId,
|
||||
redirectUri,
|
||||
allowInsecureConnections: kDebugMode,
|
||||
serviceConfiguration: const AuthorizationServiceConfiguration(
|
||||
authorizationEndpoint: '$kLichessHost/oauth',
|
||||
tokenEndpoint: '$kLichessHost/api/token'),
|
||||
scopes: oauthScopes,
|
||||
));
|
||||
tokenEndpoint: '$kLichessHost/api/token',
|
||||
),
|
||||
scopes: oauthScopes,
|
||||
),
|
||||
);
|
||||
if (result != null) {
|
||||
_log.fine('Got accessToken ${result.accessToken}');
|
||||
await _storage.write(
|
||||
key: kOAuthTokenStorageKey, value: result.accessToken);
|
||||
key: kOAuthTokenStorageKey,
|
||||
value: result.accessToken,
|
||||
);
|
||||
} else {
|
||||
throw Exception('FlutterAppAuth.authorizeAndExchangeCode null result');
|
||||
}
|
||||
@@ -81,18 +88,24 @@ class AuthRepository {
|
||||
}
|
||||
|
||||
FutureResult<void> signOut() {
|
||||
return _apiClient
|
||||
.delete(Uri.parse('$kLichessHost/api/token'))
|
||||
.then((result) => result.map((_) async {
|
||||
await _storage.delete(key: kOAuthTokenStorageKey);
|
||||
_authState.value = null;
|
||||
}));
|
||||
return _apiClient.delete(Uri.parse('$kLichessHost/api/token')).then(
|
||||
(result) => result.map((_) async {
|
||||
await _storage.delete(key: kOAuthTokenStorageKey);
|
||||
_authState.value = null;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
FutureResult<User> getAccount() {
|
||||
return _apiClient.get(Uri.parse('$kLichessHost/api/account')).then(
|
||||
(result) => result.flatMap((response) => readJsonObject(response.body,
|
||||
mapper: User.fromJson, logger: _log)));
|
||||
(result) => result.flatMap(
|
||||
(response) => readJsonObject(
|
||||
response.body,
|
||||
mapper: User.fromJson,
|
||||
logger: _log,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
|
||||
@@ -22,21 +22,23 @@ class SignInWidget extends ConsumerWidget {
|
||||
(_, state) => state.showSnackbarOnError(context),
|
||||
);
|
||||
return authState.maybeWhen(
|
||||
data: (account) => account == null
|
||||
? TextButton(
|
||||
onPressed: authActionsAsync.isLoading
|
||||
? null
|
||||
: () => ref.read(authActionsProvider.notifier).signIn(),
|
||||
child: authActionsAsync.isLoading
|
||||
? const ButtonLoadingIndicator()
|
||||
: Text(context.l10n.signIn,
|
||||
style:
|
||||
defaultTargetPlatform == TargetPlatform.android &&
|
||||
brightness == Brightness.light
|
||||
? const TextStyle(color: Colors.white)
|
||||
: null),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
orElse: () => const ButtonLoadingIndicator());
|
||||
data: (account) => account == null
|
||||
? TextButton(
|
||||
onPressed: authActionsAsync.isLoading
|
||||
? null
|
||||
: () => ref.read(authActionsProvider.notifier).signIn(),
|
||||
child: authActionsAsync.isLoading
|
||||
? const ButtonLoadingIndicator()
|
||||
: Text(
|
||||
context.l10n.signIn,
|
||||
style: defaultTargetPlatform == TargetPlatform.android &&
|
||||
brightness == Brightness.light
|
||||
? const TextStyle(color: Colors.white)
|
||||
: null,
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
orElse: () => const ButtonLoadingIndicator(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,24 +44,26 @@ class ApiEvent with _$ApiEvent {
|
||||
switch (type) {
|
||||
case 'gameStart':
|
||||
case 'gameFinish':
|
||||
return pick('game').letOrThrow((gamePick) => ApiEvent.gameStartOrFinish(
|
||||
type: type == 'gameStart'
|
||||
? GameEventLifecycle.start
|
||||
: GameEventLifecycle.finish,
|
||||
gameId: gamePick('gameId').asGameIdOrThrow(),
|
||||
fullId: gamePick('fullId').asGameFullIdOrThrow(),
|
||||
side: gamePick('color').asSideOrThrow(),
|
||||
fen: gamePick('fen').asStringOrThrow(),
|
||||
hasMoved: gamePick('hasMoved').asBoolOrThrow(),
|
||||
isMyTurn: gamePick('isMyTurn').asBoolOrThrow(),
|
||||
lastMove: gamePick('lastMove').asUciMoveOrNull(),
|
||||
opponent: gamePick('opponent').letOrThrow(Opponent.fromPick),
|
||||
rated: gamePick('rated').asBoolOrThrow(),
|
||||
perf: gamePick('perf').asPerfOrThrow(),
|
||||
speed: gamePick('speed').asSpeedOrThrow(),
|
||||
botCompat: gamePick('compat', 'bot').asBoolOrThrow(),
|
||||
boardCompat: gamePick('compat', 'board').asBoolOrThrow(),
|
||||
));
|
||||
return pick('game').letOrThrow(
|
||||
(gamePick) => ApiEvent.gameStartOrFinish(
|
||||
type: type == 'gameStart'
|
||||
? GameEventLifecycle.start
|
||||
: GameEventLifecycle.finish,
|
||||
gameId: gamePick('gameId').asGameIdOrThrow(),
|
||||
fullId: gamePick('fullId').asGameFullIdOrThrow(),
|
||||
side: gamePick('color').asSideOrThrow(),
|
||||
fen: gamePick('fen').asStringOrThrow(),
|
||||
hasMoved: gamePick('hasMoved').asBoolOrThrow(),
|
||||
isMyTurn: gamePick('isMyTurn').asBoolOrThrow(),
|
||||
lastMove: gamePick('lastMove').asUciMoveOrNull(),
|
||||
opponent: gamePick('opponent').letOrThrow(Opponent.fromPick),
|
||||
rated: gamePick('rated').asBoolOrThrow(),
|
||||
perf: gamePick('perf').asPerfOrThrow(),
|
||||
speed: gamePick('speed').asSpeedOrThrow(),
|
||||
botCompat: gamePick('compat', 'bot').asBoolOrThrow(),
|
||||
boardCompat: gamePick('compat', 'board').asBoolOrThrow(),
|
||||
),
|
||||
);
|
||||
default:
|
||||
throw UnsupportedError('Unsupported event type $type');
|
||||
}
|
||||
|
||||
@@ -14,13 +14,17 @@ class ChallengeRepository {
|
||||
final ApiClient apiClient;
|
||||
|
||||
FutureResult<void> challenge(String username, ChallengeRequest req) {
|
||||
return apiClient.post(Uri.parse('$kLichessHost/api/challenge/$username'),
|
||||
body: req.toRequestBody);
|
||||
return apiClient.post(
|
||||
Uri.parse('$kLichessHost/api/challenge/$username'),
|
||||
body: req.toRequestBody,
|
||||
);
|
||||
}
|
||||
|
||||
FutureResult<void> challengeAI(AiChallengeRequest req) {
|
||||
return apiClient.post(Uri.parse('$kLichessHost/api/challenge/ai'),
|
||||
body: req.toRequestBody);
|
||||
return apiClient.post(
|
||||
Uri.parse('$kLichessHost/api/challenge/ai'),
|
||||
body: req.toRequestBody,
|
||||
);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
|
||||
@@ -37,8 +37,11 @@ class GameRepository {
|
||||
Uri.parse('$kLichessHost/game/export/$id'),
|
||||
headers: {'Accept': 'application/json'},
|
||||
).flatMap((response) {
|
||||
return readJsonObject(response.body,
|
||||
mapper: _makeArchivedGameFromJson, logger: _log);
|
||||
return readJsonObject(
|
||||
response.body,
|
||||
mapper: _makeArchivedGameFromJson,
|
||||
logger: _log,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -46,7 +49,8 @@ class GameRepository {
|
||||
FutureResult<List<ArchivedGameData>> getUserGames(UserId userId) {
|
||||
return apiClient.get(
|
||||
Uri.parse(
|
||||
'$kLichessHost/api/games/user/$userId?max=10&moves=false&lastFen=true'),
|
||||
'$kLichessHost/api/games/user/$userId?max=10&moves=false&lastFen=true',
|
||||
),
|
||||
headers: {'Accept': 'application/x-ndjson'},
|
||||
).flatMap(_decodeNdJsonGames);
|
||||
}
|
||||
@@ -55,7 +59,8 @@ class GameRepository {
|
||||
return apiClient
|
||||
.post(
|
||||
Uri.parse(
|
||||
'$kLichessHost/api/games/export/_ids?moves=false&lastFen=true'),
|
||||
'$kLichessHost/api/games/export/_ids?moves=false&lastFen=true',
|
||||
),
|
||||
headers: {'Accept': 'application/x-ndjson'},
|
||||
body: ids.join(','),
|
||||
)
|
||||
@@ -71,8 +76,9 @@ class GameRepository {
|
||||
.toStringStream()
|
||||
.where((event) => event.isNotEmpty && event != '\n')
|
||||
.map((event) => jsonDecode(event) as Map<String, dynamic>)
|
||||
.where((json) =>
|
||||
json['type'] == 'gameStart' || json['type'] == 'gameFinish')
|
||||
.where(
|
||||
(json) => json['type'] == 'gameStart' || json['type'] == 'gameFinish',
|
||||
)
|
||||
.map((json) => ApiEvent.fromJson(json))
|
||||
.handleError((Object error) => _log.warning(error));
|
||||
}
|
||||
@@ -85,28 +91,33 @@ class GameRepository {
|
||||
.toStringStream()
|
||||
.where((event) => event.isNotEmpty && event != '\n')
|
||||
.map((event) => jsonDecode(event) as Map<String, dynamic>)
|
||||
.where((event) =>
|
||||
event['type'] == 'gameFull' || event['type'] == 'gameState')
|
||||
.where(
|
||||
(event) =>
|
||||
event['type'] == 'gameFull' || event['type'] == 'gameState',
|
||||
)
|
||||
.map((json) => GameEvent.fromJson(json))
|
||||
.handleError((Object error) => _log.warning(error));
|
||||
}
|
||||
|
||||
FutureResult<void> playMove(GameId gameId, Move move) {
|
||||
return apiClient.post(
|
||||
Uri.parse('$kLichessHost/api/board/game/$gameId/move/${move.uci}'),
|
||||
retry: true);
|
||||
Uri.parse('$kLichessHost/api/board/game/$gameId/move/${move.uci}'),
|
||||
retry: true,
|
||||
);
|
||||
}
|
||||
|
||||
FutureResult<void> abort(GameId gameId) {
|
||||
return apiClient.post(
|
||||
Uri.parse('$kLichessHost/api/board/game/$gameId/abort'),
|
||||
retry: true);
|
||||
Uri.parse('$kLichessHost/api/board/game/$gameId/abort'),
|
||||
retry: true,
|
||||
);
|
||||
}
|
||||
|
||||
FutureResult<void> resign(GameId gameId) {
|
||||
return apiClient.post(
|
||||
Uri.parse('$kLichessHost/api/board/game/$gameId/resign'),
|
||||
retry: true);
|
||||
Uri.parse('$kLichessHost/api/board/game/$gameId/resign'),
|
||||
retry: true,
|
||||
);
|
||||
}
|
||||
|
||||
Result<List<ArchivedGameData>> _decodeNdJsonGames(http.Response response) {
|
||||
@@ -136,7 +147,8 @@ ArchivedGame _archivedGameFromPick(RequiredPick pick) {
|
||||
final data = _archivedGameDataFromPick(pick);
|
||||
final clockData = pick('clock').letOrNull(_clockDataFromPick);
|
||||
final clocks = pick('clocks').asListOrNull<Duration>(
|
||||
(p0) => Duration(milliseconds: p0.asIntOrThrow() * 10));
|
||||
(p0) => Duration(milliseconds: p0.asIntOrThrow() * 10),
|
||||
);
|
||||
|
||||
return ArchivedGame(
|
||||
data: data,
|
||||
@@ -152,14 +164,16 @@ ArchivedGame _archivedGameFromPick(RequiredPick pick) {
|
||||
final move = position.parseSan(san);
|
||||
// assume lichess only sends correct moves
|
||||
position = position.playUnchecked(move!);
|
||||
steps.add(GameStep(
|
||||
ply: ply,
|
||||
san: san,
|
||||
uci: move.uci,
|
||||
position: position,
|
||||
whiteClock: ply.isOdd ? stepClock : clock,
|
||||
blackClock: ply.isEven ? stepClock : clock,
|
||||
));
|
||||
steps.add(
|
||||
GameStep(
|
||||
ply: ply,
|
||||
san: san,
|
||||
uci: move.uci,
|
||||
position: position,
|
||||
whiteClock: ply.isOdd ? stepClock : clock,
|
||||
blackClock: ply.isEven ? stepClock : clock,
|
||||
),
|
||||
);
|
||||
clock = stepClock;
|
||||
}
|
||||
return steps;
|
||||
|
||||
@@ -6,8 +6,10 @@ import '../model/time_control.dart';
|
||||
final computerOpponentPrefProvider = createPrefProvider(
|
||||
prefKey: 'play.computerOpponent',
|
||||
defaultValue: ComputerOpponent.maia,
|
||||
mapFrom: (v) => ComputerOpponent.values.firstWhere((e) => e.toString() == v,
|
||||
orElse: () => ComputerOpponent.maia),
|
||||
mapFrom: (v) => ComputerOpponent.values.firstWhere(
|
||||
(e) => e.toString() == v,
|
||||
orElse: () => ComputerOpponent.maia,
|
||||
),
|
||||
mapTo: (v) => v.toString(),
|
||||
);
|
||||
|
||||
@@ -28,7 +30,8 @@ final timeControlPrefProvider = createPrefProvider(
|
||||
prefKey: 'play.timeControl',
|
||||
defaultValue: TimeControl.blitz4,
|
||||
mapFrom: (v) => TimeControl.values.firstWhere(
|
||||
(e) => v != null && e.value == TimeInc.fromString(v),
|
||||
orElse: () => TimeControl.blitz4),
|
||||
(e) => v != null && e.value == TimeInc.fromString(v),
|
||||
orElse: () => TimeControl.blitz4,
|
||||
),
|
||||
mapTo: (v) => v.value.toString(),
|
||||
);
|
||||
|
||||
@@ -23,11 +23,12 @@ class GameClock with _$GameClock {
|
||||
class GameState with _$GameState {
|
||||
const GameState._();
|
||||
|
||||
const factory GameState(
|
||||
{required GameStatus status,
|
||||
required List<String> uciMoves,
|
||||
required List<String> sanMoves,
|
||||
required List<Position<Chess>> positions}) = _GameState;
|
||||
const factory GameState({
|
||||
required GameStatus status,
|
||||
required List<String> uciMoves,
|
||||
required List<String> sanMoves,
|
||||
required List<Position<Chess>> positions,
|
||||
}) = _GameState;
|
||||
|
||||
factory GameState.fromEvent(GameStateEvent event) {
|
||||
final uciMoves = event.moves.split(' ').where((m) => m.isNotEmpty).toList();
|
||||
|
||||
@@ -34,46 +34,52 @@ class CreateGameService {
|
||||
);
|
||||
final createChallengeTask = opponent == ComputerOpponent.stockfish
|
||||
? challengeRepo.challengeAI(
|
||||
AiChallengeRequest(level: level, challenge: challengeRequest))
|
||||
AiChallengeRequest(level: level, challenge: challengeRequest),
|
||||
)
|
||||
: challengeRepo.challenge(maiaStrength.name, challengeRequest);
|
||||
|
||||
return createChallengeTask.flatMap((_) => _waitForGameStart(account));
|
||||
}
|
||||
|
||||
FutureResult<PlayableGame> _waitForGameStart(User account) {
|
||||
return Result.capture((() async {
|
||||
final gameRepo = ref.read(gameRepositoryProvider);
|
||||
final stream = gameRepo.events().timeout(const Duration(seconds: 15),
|
||||
onTimeout: (sink) => sink.close());
|
||||
return Result.capture(
|
||||
(() async {
|
||||
final gameRepo = ref.read(gameRepositoryProvider);
|
||||
final stream = gameRepo.events().timeout(
|
||||
const Duration(seconds: 15),
|
||||
onTimeout: (sink) => sink.close(),
|
||||
);
|
||||
|
||||
final startEvent = await stream.firstWhere(
|
||||
final startEvent = await stream.firstWhere(
|
||||
(event) =>
|
||||
event.type == GameEventLifecycle.start && event.boardCompat,
|
||||
orElse: () {
|
||||
throw Exception('Could not create game.');
|
||||
});
|
||||
throw Exception('Could not create game.');
|
||||
},
|
||||
);
|
||||
|
||||
final player = Player(
|
||||
id: account.id,
|
||||
name: account.username,
|
||||
rating: account.perfs[startEvent.perf]!.rating,
|
||||
);
|
||||
final opponent = Player(
|
||||
final player = Player(
|
||||
id: account.id,
|
||||
name: account.username,
|
||||
rating: account.perfs[startEvent.perf]!.rating,
|
||||
);
|
||||
final opponent = Player(
|
||||
id: startEvent.opponent.id,
|
||||
name: startEvent.opponent.username,
|
||||
rating: startEvent.opponent.rating);
|
||||
return PlayableGame(
|
||||
id: startEvent.gameId,
|
||||
initialFen: startEvent.fen,
|
||||
speed: startEvent.speed,
|
||||
orientation: startEvent.side,
|
||||
rated: startEvent.rated,
|
||||
white: startEvent.side == Side.white ? player : opponent,
|
||||
black: startEvent.side == Side.white ? opponent : player,
|
||||
variant: Variant.standard,
|
||||
);
|
||||
})())
|
||||
.mapError(
|
||||
rating: startEvent.opponent.rating,
|
||||
);
|
||||
return PlayableGame(
|
||||
id: startEvent.gameId,
|
||||
initialFen: startEvent.fen,
|
||||
speed: startEvent.speed,
|
||||
orientation: startEvent.side,
|
||||
rated: startEvent.rated,
|
||||
white: startEvent.side == Side.white ? player : opponent,
|
||||
black: startEvent.side == Side.white ? opponent : player,
|
||||
variant: Variant.standard,
|
||||
);
|
||||
})(),
|
||||
).mapError(
|
||||
(error, trace) {
|
||||
_log.severe('Request error', error, trace);
|
||||
return GenericIOException();
|
||||
|
||||
@@ -40,8 +40,11 @@ final archivedGameProvider =
|
||||
});
|
||||
|
||||
class ArchivedGameScreen extends ConsumerWidget {
|
||||
const ArchivedGameScreen(
|
||||
{required this.gameData, required this.orientation, super.key});
|
||||
const ArchivedGameScreen({
|
||||
required this.gameData,
|
||||
required this.orientation,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final ArchivedGameData gameData;
|
||||
final Side orientation;
|
||||
@@ -62,21 +65,26 @@ class ArchivedGameScreen extends ConsumerWidget {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
}),
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: isSoundMuted
|
||||
? const Icon(Icons.volume_off)
|
||||
: const Icon(Icons.volume_up),
|
||||
onPressed: () =>
|
||||
ref.read(isSoundMutedProvider.notifier).toggleSound())
|
||||
icon: isSoundMuted
|
||||
? const Icon(Icons.volume_off)
|
||||
: const Icon(Icons.volume_up),
|
||||
onPressed: () =>
|
||||
ref.read(isSoundMutedProvider.notifier).toggleSound(),
|
||||
)
|
||||
],
|
||||
),
|
||||
body: _BoardBody(
|
||||
gameData: gameData, game: archivedGame, orientation: orientation),
|
||||
gameData: gameData,
|
||||
game: archivedGame,
|
||||
orientation: orientation,
|
||||
),
|
||||
bottomNavigationBar:
|
||||
_BottomBar(gameData: gameData, steps: archivedGame?.steps),
|
||||
);
|
||||
@@ -88,29 +96,33 @@ class ArchivedGameScreen extends ConsumerWidget {
|
||||
ref.watch(archivedGameProvider(gameData.id)).asData?.value;
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
padding: const EdgeInsetsDirectional.only(start: 0, end: 16.0),
|
||||
leading: CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Icon(CupertinoIcons.back),
|
||||
),
|
||||
trailing: CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
child: isSoundMuted
|
||||
? const Icon(CupertinoIcons.volume_off)
|
||||
: const Icon(CupertinoIcons.volume_up),
|
||||
onPressed: () =>
|
||||
ref.read(isSoundMutedProvider.notifier).toggleSound())),
|
||||
padding: const EdgeInsetsDirectional.only(start: 0, end: 16.0),
|
||||
leading: CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Icon(CupertinoIcons.back),
|
||||
),
|
||||
trailing: CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
child: isSoundMuted
|
||||
? const Icon(CupertinoIcons.volume_off)
|
||||
: const Icon(CupertinoIcons.volume_up),
|
||||
onPressed: () =>
|
||||
ref.read(isSoundMutedProvider.notifier).toggleSound(),
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _BoardBody(
|
||||
gameData: gameData,
|
||||
game: archivedGame,
|
||||
orientation: orientation)),
|
||||
child: _BoardBody(
|
||||
gameData: gameData,
|
||||
game: archivedGame,
|
||||
orientation: orientation,
|
||||
),
|
||||
),
|
||||
_BottomBar(gameData: gameData, steps: archivedGame?.steps),
|
||||
],
|
||||
),
|
||||
@@ -120,8 +132,11 @@ class ArchivedGameScreen extends ConsumerWidget {
|
||||
}
|
||||
|
||||
class _BoardBody extends ConsumerWidget {
|
||||
const _BoardBody(
|
||||
{required this.gameData, this.game, required this.orientation});
|
||||
const _BoardBody({
|
||||
required this.gameData,
|
||||
this.game,
|
||||
required this.orientation,
|
||||
});
|
||||
|
||||
final ArchivedGameData gameData;
|
||||
final ArchivedGame? game;
|
||||
@@ -211,71 +226,73 @@ class _BottomBar extends ConsumerWidget {
|
||||
},
|
||||
icon: const Icon(Icons.menu),
|
||||
),
|
||||
Row(children: [
|
||||
IconButton(
|
||||
key: const ValueKey('cursor-first'),
|
||||
// TODO add translation
|
||||
tooltip: 'First position',
|
||||
onPressed: canGoBackward
|
||||
? () {
|
||||
ref.read(_positionCursorProvider.notifier).state = 0;
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(LichessIcons.fast_backward),
|
||||
iconSize: 20,
|
||||
),
|
||||
IconButton(
|
||||
key: const ValueKey('cursor-back'),
|
||||
// TODO add translation
|
||||
tooltip: 'Backward',
|
||||
onPressed: canGoBackward
|
||||
? () {
|
||||
ref
|
||||
.read(_positionCursorProvider.notifier)
|
||||
.update((state) {
|
||||
if (state != null) {
|
||||
state--;
|
||||
}
|
||||
return state;
|
||||
});
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(LichessIcons.step_backward),
|
||||
iconSize: 20,
|
||||
),
|
||||
IconButton(
|
||||
key: const ValueKey('cursor-forward'),
|
||||
// TODO add translation
|
||||
tooltip: 'Forward',
|
||||
onPressed: canGoForward
|
||||
? () {
|
||||
ref
|
||||
.read(_positionCursorProvider.notifier)
|
||||
.update((state) {
|
||||
if (state != null) {
|
||||
state++;
|
||||
}
|
||||
return state;
|
||||
});
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(LichessIcons.step_forward),
|
||||
iconSize: 20,
|
||||
),
|
||||
IconButton(
|
||||
key: const ValueKey('cursor-last'),
|
||||
// TODO add translation
|
||||
tooltip: 'Last position',
|
||||
onPressed: canGoForward
|
||||
? () {
|
||||
ref.read(_positionCursorProvider.notifier).state =
|
||||
steps!.length - 1;
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(LichessIcons.fast_forward),
|
||||
iconSize: 20,
|
||||
),
|
||||
]),
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
key: const ValueKey('cursor-first'),
|
||||
// TODO add translation
|
||||
tooltip: 'First position',
|
||||
onPressed: canGoBackward
|
||||
? () {
|
||||
ref.read(_positionCursorProvider.notifier).state = 0;
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(LichessIcons.fast_backward),
|
||||
iconSize: 20,
|
||||
),
|
||||
IconButton(
|
||||
key: const ValueKey('cursor-back'),
|
||||
// TODO add translation
|
||||
tooltip: 'Backward',
|
||||
onPressed: canGoBackward
|
||||
? () {
|
||||
ref
|
||||
.read(_positionCursorProvider.notifier)
|
||||
.update((state) {
|
||||
if (state != null) {
|
||||
state--;
|
||||
}
|
||||
return state;
|
||||
});
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(LichessIcons.step_backward),
|
||||
iconSize: 20,
|
||||
),
|
||||
IconButton(
|
||||
key: const ValueKey('cursor-forward'),
|
||||
// TODO add translation
|
||||
tooltip: 'Forward',
|
||||
onPressed: canGoForward
|
||||
? () {
|
||||
ref
|
||||
.read(_positionCursorProvider.notifier)
|
||||
.update((state) {
|
||||
if (state != null) {
|
||||
state++;
|
||||
}
|
||||
return state;
|
||||
});
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(LichessIcons.step_forward),
|
||||
iconSize: 20,
|
||||
),
|
||||
IconButton(
|
||||
key: const ValueKey('cursor-last'),
|
||||
// TODO add translation
|
||||
tooltip: 'Last position',
|
||||
onPressed: canGoForward
|
||||
? () {
|
||||
ref.read(_positionCursorProvider.notifier).state =
|
||||
steps!.length - 1;
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(LichessIcons.fast_forward),
|
||||
iconSize: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -22,8 +22,11 @@ import '../play/play_action_notifier.dart';
|
||||
import './playable_game_screen_providers.dart';
|
||||
|
||||
class PlayableGameScreen extends ConsumerWidget {
|
||||
const PlayableGameScreen(
|
||||
{required this.game, required this.account, super.key});
|
||||
const PlayableGameScreen({
|
||||
required this.game,
|
||||
required this.account,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final PlayableGame game;
|
||||
final User account;
|
||||
@@ -44,8 +47,9 @@ class PlayableGameScreen extends ConsumerWidget {
|
||||
ref.invalidate(gameStateProvider);
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) =>
|
||||
PlayableGameScreen(game: state.value!, account: account)),
|
||||
builder: (context) =>
|
||||
PlayableGameScreen(game: state.value!, account: account),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -65,21 +69,23 @@ class PlayableGameScreen extends ConsumerWidget {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
if (gameState?.gameOver == true) {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
} else {
|
||||
_showExitConfirmDialog(context);
|
||||
}
|
||||
}),
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
if (gameState?.gameOver == true) {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
} else {
|
||||
_showExitConfirmDialog(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: isSoundMuted
|
||||
? const Icon(Icons.volume_off)
|
||||
: const Icon(Icons.volume_up),
|
||||
onPressed: () =>
|
||||
ref.read(isSoundMutedProvider.notifier).toggleSound())
|
||||
icon: isSoundMuted
|
||||
? const Icon(Icons.volume_off)
|
||||
: const Icon(Icons.volume_up),
|
||||
onPressed: () =>
|
||||
ref.read(isSoundMutedProvider.notifier).toggleSound(),
|
||||
)
|
||||
],
|
||||
),
|
||||
body: _BoardBody(game: game, account: account),
|
||||
@@ -92,25 +98,27 @@ class PlayableGameScreen extends ConsumerWidget {
|
||||
final gameState = ref.watch(gameStateProvider);
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
padding: const EdgeInsetsDirectional.only(start: 0, end: 16.0),
|
||||
leading: CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () {
|
||||
if (gameState?.gameOver == true) {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
} else {
|
||||
_showExitConfirmDialog(context);
|
||||
}
|
||||
},
|
||||
child: const Icon(CupertinoIcons.back),
|
||||
),
|
||||
trailing: CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
child: isSoundMuted
|
||||
? const Icon(CupertinoIcons.volume_off)
|
||||
: const Icon(CupertinoIcons.volume_up),
|
||||
onPressed: () =>
|
||||
ref.read(isSoundMutedProvider.notifier).toggleSound())),
|
||||
padding: const EdgeInsetsDirectional.only(start: 0, end: 16.0),
|
||||
leading: CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () {
|
||||
if (gameState?.gameOver == true) {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
} else {
|
||||
_showExitConfirmDialog(context);
|
||||
}
|
||||
},
|
||||
child: const Icon(CupertinoIcons.back),
|
||||
),
|
||||
trailing: CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
child: isSoundMuted
|
||||
? const Icon(CupertinoIcons.volume_off)
|
||||
: const Icon(CupertinoIcons.volume_up),
|
||||
onPressed: () =>
|
||||
ref.read(isSoundMutedProvider.notifier).toggleSound(),
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -254,7 +262,8 @@ class _BoardBody extends ConsumerWidget {
|
||||
},
|
||||
error: (err, stackTrace) {
|
||||
debugPrint(
|
||||
'SEVERE: [PlayableGameScreen] could not load game; $err\n$stackTrace');
|
||||
'SEVERE: [PlayableGameScreen] could not load game; $err\n$stackTrace',
|
||||
);
|
||||
return const Text('Could not load game stream.');
|
||||
},
|
||||
);
|
||||
@@ -282,57 +291,59 @@ class _BottomBar extends ConsumerWidget {
|
||||
},
|
||||
icon: const Icon(Icons.menu),
|
||||
),
|
||||
Row(children: [
|
||||
IconButton(
|
||||
key: const ValueKey('cursor-first'),
|
||||
// TODO add translation
|
||||
tooltip: 'First position',
|
||||
onPressed: positionCursor > 0
|
||||
? () {
|
||||
ref.read(positionCursorProvider.notifier).state = 0;
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(LichessIcons.fast_backward),
|
||||
iconSize: 20,
|
||||
),
|
||||
IconButton(
|
||||
key: const ValueKey('cursor-back'),
|
||||
// TODO add translation
|
||||
tooltip: 'Backward',
|
||||
onPressed: positionCursor > 0
|
||||
? () {
|
||||
ref.read(positionCursorProvider.notifier).state--;
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(LichessIcons.step_backward),
|
||||
iconSize: 20,
|
||||
),
|
||||
IconButton(
|
||||
key: const ValueKey('cursor-forward'),
|
||||
// TODO add translation
|
||||
tooltip: 'Forward',
|
||||
onPressed: positionCursor < (gameState?.positionIndex ?? 0)
|
||||
? () {
|
||||
ref.read(positionCursorProvider.notifier).state++;
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(LichessIcons.step_forward),
|
||||
iconSize: 20,
|
||||
),
|
||||
IconButton(
|
||||
key: const ValueKey('cursor-last'),
|
||||
// TODO add translation
|
||||
tooltip: 'Last position',
|
||||
onPressed: positionCursor < (gameState?.positionIndex ?? 0)
|
||||
? () {
|
||||
ref.read(positionCursorProvider.notifier).state =
|
||||
gameState?.positionIndex ?? 0;
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(LichessIcons.fast_forward),
|
||||
iconSize: 20,
|
||||
),
|
||||
]),
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
key: const ValueKey('cursor-first'),
|
||||
// TODO add translation
|
||||
tooltip: 'First position',
|
||||
onPressed: positionCursor > 0
|
||||
? () {
|
||||
ref.read(positionCursorProvider.notifier).state = 0;
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(LichessIcons.fast_backward),
|
||||
iconSize: 20,
|
||||
),
|
||||
IconButton(
|
||||
key: const ValueKey('cursor-back'),
|
||||
// TODO add translation
|
||||
tooltip: 'Backward',
|
||||
onPressed: positionCursor > 0
|
||||
? () {
|
||||
ref.read(positionCursorProvider.notifier).state--;
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(LichessIcons.step_backward),
|
||||
iconSize: 20,
|
||||
),
|
||||
IconButton(
|
||||
key: const ValueKey('cursor-forward'),
|
||||
// TODO add translation
|
||||
tooltip: 'Forward',
|
||||
onPressed: positionCursor < (gameState?.positionIndex ?? 0)
|
||||
? () {
|
||||
ref.read(positionCursorProvider.notifier).state++;
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(LichessIcons.step_forward),
|
||||
iconSize: 20,
|
||||
),
|
||||
IconButton(
|
||||
key: const ValueKey('cursor-last'),
|
||||
// TODO add translation
|
||||
tooltip: 'Last position',
|
||||
onPressed: positionCursor < (gameState?.positionIndex ?? 0)
|
||||
? () {
|
||||
ref.read(positionCursorProvider.notifier).state =
|
||||
gameState?.positionIndex ?? 0;
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(LichessIcons.fast_forward),
|
||||
iconSize: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -376,16 +387,19 @@ class _BottomBar extends ConsumerWidget {
|
||||
),
|
||||
if (gameState?.gameOver == true)
|
||||
BottomSheetAction(
|
||||
leading: const Icon(CupertinoIcons.arrow_2_squarepath),
|
||||
label: playActionAsync.isLoading
|
||||
? const ButtonLoadingIndicator()
|
||||
: Text(context.l10n.rematch),
|
||||
onPressed: (context) {
|
||||
if (!playActionAsync.isLoading) {
|
||||
ref.read(playActionProvider.notifier).createGame(
|
||||
account: account, side: game.orientation.opposite);
|
||||
}
|
||||
}),
|
||||
leading: const Icon(CupertinoIcons.arrow_2_squarepath),
|
||||
label: playActionAsync.isLoading
|
||||
? const ButtonLoadingIndicator()
|
||||
: Text(context.l10n.rematch),
|
||||
onPressed: (context) {
|
||||
if (!playActionAsync.isLoading) {
|
||||
ref.read(playActionProvider.notifier).createGame(
|
||||
account: account,
|
||||
side: game.orientation.opposite,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,11 +14,13 @@ final isBoardTurnedProvider = StateProvider.autoDispose<bool>((ref) => false);
|
||||
|
||||
final gameStateProvider =
|
||||
AutoDisposeNotifierProvider<GameStateNotifier, GameState?>(
|
||||
GameStateNotifier.new);
|
||||
GameStateNotifier.new,
|
||||
);
|
||||
|
||||
final gameActionProvider =
|
||||
AutoDisposeNotifierProvider<GameActionNotifier, AsyncValue<void>>(
|
||||
GameActionNotifier.new);
|
||||
GameActionNotifier.new,
|
||||
);
|
||||
|
||||
final gameStreamProvider =
|
||||
StreamProvider.autoDispose.family<GameClock, GameId>((ref, gameId) {
|
||||
|
||||
@@ -26,4 +26,5 @@ class PlayActionNotifier
|
||||
|
||||
final playActionProvider =
|
||||
AutoDisposeNotifierProvider<PlayActionNotifier, AsyncValue<PlayableGame?>>(
|
||||
PlayActionNotifier.new);
|
||||
PlayActionNotifier.new,
|
||||
);
|
||||
|
||||
@@ -34,12 +34,18 @@ final maiaBotsProvider =
|
||||
userRepo.getUser('maia9'),
|
||||
]).then(Result.flattenAll);
|
||||
final maiaStatuses = userRepo.getUsersStatus(['maia1', 'maia5', 'maia9']);
|
||||
final result = maiaBots.flatMap((bots) => maiaStatuses.map(
|
||||
(statuses) => bots
|
||||
.map((bot) => Tuple2<User, UserStatus>(
|
||||
bot, statuses.firstWhere((s) => s.id == bot.id)))
|
||||
.toList(),
|
||||
));
|
||||
final result = maiaBots.flatMap(
|
||||
(bots) => maiaStatuses.map(
|
||||
(statuses) => bots
|
||||
.map(
|
||||
(bot) => Tuple2<User, UserStatus>(
|
||||
bot,
|
||||
statuses.firstWhere((s) => s.id == bot.id),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
return result.fold(
|
||||
(data) {
|
||||
// retry on error, cache indefinitely on success
|
||||
@@ -111,8 +117,9 @@ class PlayForm extends ConsumerWidget {
|
||||
ref.invalidate(playActionProvider);
|
||||
Navigator.of(context, rootNavigator: true).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) =>
|
||||
PlayableGameScreen(game: state.value!, account: account!)),
|
||||
builder: (context) =>
|
||||
PlayableGameScreen(game: state.value!, account: account!),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -140,93 +147,104 @@ class PlayForm extends ConsumerWidget {
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Builder(builder: (BuildContext context) {
|
||||
if (opponentPref == ComputerOpponent.maia) {
|
||||
return maiaBots.when(
|
||||
data: (bots) {
|
||||
Builder(
|
||||
builder: (BuildContext context) {
|
||||
if (opponentPref == ComputerOpponent.maia) {
|
||||
return maiaBots.when(
|
||||
data: (bots) {
|
||||
return Column(
|
||||
children: [
|
||||
const Text(
|
||||
'Maia is a human-like neural network chess engine. It was trained by learning from over 10 million Lichess games. It is an ongoing research project aiming to make a more human-friendly, useful, and fun chess AI. For more information go to maiachess.com. ',
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
ListTileChoice(
|
||||
choices: MaiaStrength.values,
|
||||
selectedItem: maiaStrength,
|
||||
titleBuilder: (ms) => Text(ms.name),
|
||||
subtitleBuilder: (ms) => Row(
|
||||
children: [Perf.blitz, Perf.rapid, Perf.classical]
|
||||
.map((p) {
|
||||
final bot = bots
|
||||
.firstWhere(
|
||||
(b) => b.item1.id.value == ms.name,
|
||||
)
|
||||
.item1;
|
||||
return Semantics(
|
||||
label: p.title,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(p.icon, size: 18.0),
|
||||
const SizedBox(width: 3.0),
|
||||
Text(bot.perfs[p]!.rating.toString()),
|
||||
const SizedBox(width: 12.0),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
onSelectedItemChanged: (value) {
|
||||
ref.read(maiaStrengthProvider.notifier).set(value);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
error: (err, st) {
|
||||
debugPrint(
|
||||
'SEVERE [PlayScreen] could not load bot info: $err\n$st',
|
||||
);
|
||||
return const Text('Could not load bot ratings.');
|
||||
},
|
||||
loading: () => const CenterLoadingIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
int value = stockfishLevel;
|
||||
return StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setState) {
|
||||
return Column(
|
||||
children: [
|
||||
const Text(
|
||||
'Maia is a human-like neural network chess engine. It was trained by learning from over 10 million Lichess games. It is an ongoing research project aiming to make a more human-friendly, useful, and fun chess AI. For more information go to maiachess.com. '),
|
||||
'Stockfish is a strong open source engine, 13-time winner of the Top Chess Engine Championship.',
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
ListTileChoice(
|
||||
choices: MaiaStrength.values,
|
||||
selectedItem: maiaStrength,
|
||||
titleBuilder: (ms) => Text(ms.name),
|
||||
subtitleBuilder: (ms) => Row(
|
||||
children:
|
||||
[Perf.blitz, Perf.rapid, Perf.classical].map((p) {
|
||||
final bot = bots
|
||||
.firstWhere((b) => b.item1.id.value == ms.name)
|
||||
.item1;
|
||||
return Semantics(
|
||||
label: p.title,
|
||||
child: Row(children: [
|
||||
Icon(p.icon, size: 18.0),
|
||||
const SizedBox(width: 3.0),
|
||||
Text(bot.perfs[p]!.rating.toString()),
|
||||
const SizedBox(width: 12.0),
|
||||
]),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
onSelectedItemChanged: (value) {
|
||||
ref.read(maiaStrengthProvider.notifier).set(value);
|
||||
},
|
||||
Row(
|
||||
children: [
|
||||
Text(context.l10n.strength),
|
||||
Expanded(
|
||||
child: Slider(
|
||||
value: value.toDouble(),
|
||||
min: 1,
|
||||
max: 8,
|
||||
divisions: 7,
|
||||
label: '${context.l10n.level} $value',
|
||||
semanticFormatterCallback: (double newValue) {
|
||||
return '${context.l10n.level} ${newValue.round()}';
|
||||
},
|
||||
onChanged:
|
||||
opponentPref != ComputerOpponent.stockfish
|
||||
? null
|
||||
: (double newVal) {
|
||||
setState(() {
|
||||
value = newVal.round();
|
||||
});
|
||||
},
|
||||
onChangeEnd: (double value) {
|
||||
ref
|
||||
.read(stockfishLevelProvider.notifier)
|
||||
.set(value.round());
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
error: (err, st) {
|
||||
debugPrint(
|
||||
'SEVERE [PlayScreen] could not load bot info: $err\n$st');
|
||||
return const Text('Could not load bot ratings.');
|
||||
},
|
||||
loading: () => const CenterLoadingIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
int value = stockfishLevel;
|
||||
return StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setState) {
|
||||
return Column(
|
||||
children: [
|
||||
const Text(
|
||||
'Stockfish is a strong open source engine, 13-time winner of the Top Chess Engine Championship.'),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
Text(context.l10n.strength),
|
||||
Expanded(
|
||||
child: Slider(
|
||||
value: value.toDouble(),
|
||||
min: 1,
|
||||
max: 8,
|
||||
divisions: 7,
|
||||
label: '${context.l10n.level} $value',
|
||||
semanticFormatterCallback: (double newValue) {
|
||||
return '${context.l10n.level} ${newValue.round()}';
|
||||
},
|
||||
onChanged: opponentPref != ComputerOpponent.stockfish
|
||||
? null
|
||||
: (double newVal) {
|
||||
setState(() {
|
||||
value = newVal.round();
|
||||
});
|
||||
},
|
||||
onChangeEnd: (double value) {
|
||||
ref
|
||||
.read(stockfishLevelProvider.notifier)
|
||||
.set(value.round());
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}),
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
SecondaryButton(
|
||||
semanticsLabel:
|
||||
@@ -276,10 +294,12 @@ class PlayForm extends ConsumerWidget {
|
||||
.createGame(account: account!),
|
||||
child: authActionsAsync.isLoading || playActionAsync.isLoading
|
||||
? const ButtonLoadingIndicator()
|
||||
: Text(account == null
|
||||
// TODO translate
|
||||
? 'Sign in to start playing'
|
||||
: context.l10n.play),
|
||||
: Text(
|
||||
account == null
|
||||
// TODO translate
|
||||
? 'Sign in to start playing'
|
||||
: context.l10n.play,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -66,32 +66,44 @@ class TimeControlModal extends ConsumerWidget {
|
||||
padding: kBodyPadding,
|
||||
child: Column(
|
||||
children: [
|
||||
_SectionChoices(timeControlPref,
|
||||
choices: const [
|
||||
TimeControl.blitz1,
|
||||
TimeControl.blitz2,
|
||||
TimeControl.blitz3,
|
||||
TimeControl.blitz4
|
||||
],
|
||||
title: const _SectionTitle(
|
||||
title: 'Blitz', icon: LichessIcons.blitz),
|
||||
onSelected: onSelected),
|
||||
_SectionChoices(
|
||||
timeControlPref,
|
||||
choices: const [
|
||||
TimeControl.blitz1,
|
||||
TimeControl.blitz2,
|
||||
TimeControl.blitz3,
|
||||
TimeControl.blitz4
|
||||
],
|
||||
title: const _SectionTitle(
|
||||
title: 'Blitz',
|
||||
icon: LichessIcons.blitz,
|
||||
),
|
||||
onSelected: onSelected,
|
||||
),
|
||||
const SizedBox(height: 30.0),
|
||||
_SectionChoices(timeControlPref,
|
||||
choices: const [
|
||||
TimeControl.rapid1,
|
||||
TimeControl.rapid2,
|
||||
TimeControl.rapid3
|
||||
],
|
||||
title: const _SectionTitle(
|
||||
title: 'Rapid', icon: LichessIcons.rapid),
|
||||
onSelected: onSelected),
|
||||
_SectionChoices(
|
||||
timeControlPref,
|
||||
choices: const [
|
||||
TimeControl.rapid1,
|
||||
TimeControl.rapid2,
|
||||
TimeControl.rapid3
|
||||
],
|
||||
title: const _SectionTitle(
|
||||
title: 'Rapid',
|
||||
icon: LichessIcons.rapid,
|
||||
),
|
||||
onSelected: onSelected,
|
||||
),
|
||||
const SizedBox(height: 30.0),
|
||||
_SectionChoices(timeControlPref,
|
||||
choices: const [TimeControl.classical1, TimeControl.classical2],
|
||||
title: const _SectionTitle(
|
||||
title: 'Classical', icon: LichessIcons.classical),
|
||||
onSelected: onSelected),
|
||||
_SectionChoices(
|
||||
timeControlPref,
|
||||
choices: const [TimeControl.classical1, TimeControl.classical2],
|
||||
title: const _SectionTitle(
|
||||
title: 'Classical',
|
||||
icon: LichessIcons.classical,
|
||||
),
|
||||
onSelected: onSelected,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -100,8 +112,12 @@ class TimeControlModal extends ConsumerWidget {
|
||||
}
|
||||
|
||||
class _SectionChoices extends StatelessWidget {
|
||||
const _SectionChoices(this.selected,
|
||||
{required this.title, required this.choices, required this.onSelected});
|
||||
const _SectionChoices(
|
||||
this.selected, {
|
||||
required this.title,
|
||||
required this.choices,
|
||||
required this.onSelected,
|
||||
});
|
||||
|
||||
final TimeControl selected;
|
||||
final List<TimeControl> choices;
|
||||
|
||||
@@ -22,9 +22,12 @@ class HomeScreen extends StatelessWidget {
|
||||
|
||||
Widget _androidBuilder(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('lichess.org'), actions: const [
|
||||
SignInWidget(),
|
||||
]),
|
||||
appBar: AppBar(
|
||||
title: const Text('lichess.org'),
|
||||
actions: const [
|
||||
SignInWidget(),
|
||||
],
|
||||
),
|
||||
body: _buildBody(context),
|
||||
);
|
||||
}
|
||||
@@ -58,8 +61,11 @@ class HomeScreen extends StatelessWidget {
|
||||
semanticsLabel: context.l10n.createAGame,
|
||||
child: Text(context.l10n.createAGame),
|
||||
onPressed: () {
|
||||
Navigator.of(context).push<void>(MaterialPageRoute(
|
||||
builder: (context) => const PlayScreen()));
|
||||
Navigator.of(context).push<void>(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const PlayScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -25,24 +25,28 @@ class PuzzleLocalDB {
|
||||
final SharedPreferences _prefs;
|
||||
|
||||
// TODO enum for angle
|
||||
PuzzleLocalData? fetch(
|
||||
{String? userId, PuzzleTheme angle = PuzzleTheme.mix}) {
|
||||
PuzzleLocalData? fetch({
|
||||
String? userId,
|
||||
PuzzleTheme angle = PuzzleTheme.mix,
|
||||
}) {
|
||||
final raw = _prefs.getString(_makeKey(userId, angle));
|
||||
if (raw != null) {
|
||||
final json = jsonDecode(raw);
|
||||
if (json is! Map<String, dynamic>) {
|
||||
throw const FormatException(
|
||||
'[PuzzleLocalDB] cannot fetch puzzles: expected an object');
|
||||
'[PuzzleLocalDB] cannot fetch puzzles: expected an object',
|
||||
);
|
||||
}
|
||||
return PuzzleLocalData.fromJson(json);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<bool> save(
|
||||
{String? userId,
|
||||
PuzzleTheme angle = PuzzleTheme.mix,
|
||||
required PuzzleLocalData data}) {
|
||||
Future<bool> save({
|
||||
String? userId,
|
||||
PuzzleTheme angle = PuzzleTheme.mix,
|
||||
required PuzzleLocalData data,
|
||||
}) {
|
||||
return _prefs.setString(_makeKey(userId, angle), jsonEncode(data.toJson()));
|
||||
}
|
||||
|
||||
|
||||
@@ -35,8 +35,10 @@ class PuzzleRepository {
|
||||
final ApiClient apiClient;
|
||||
final Logger _log;
|
||||
|
||||
FutureResult<List<Puzzle>> selectBatch(
|
||||
{PuzzleTheme angle = PuzzleTheme.mix, int nb = kPuzzleLocalQueueLength}) {
|
||||
FutureResult<List<Puzzle>> selectBatch({
|
||||
PuzzleTheme angle = PuzzleTheme.mix,
|
||||
int nb = kPuzzleLocalQueueLength,
|
||||
}) {
|
||||
return apiClient
|
||||
.get(Uri.parse('$kLichessHost/api/puzzle/batch/${angle.name}?nb=$nb'))
|
||||
.flatMap(_decodeJson);
|
||||
@@ -59,18 +61,22 @@ class PuzzleRepository {
|
||||
}
|
||||
|
||||
Result<List<Puzzle>> _decodeJson(http.Response response) {
|
||||
return readJsonObject(response.body, mapper: (Map<String, dynamic> json) {
|
||||
final puzzles = json['puzzles'];
|
||||
if (puzzles is! List<dynamic>) {
|
||||
throw const FormatException('puzzles: expected a list');
|
||||
}
|
||||
return puzzles.map((e) {
|
||||
if (e is! Map<String, dynamic>) {
|
||||
throw const FormatException('Expected an object');
|
||||
return readJsonObject(
|
||||
response.body,
|
||||
mapper: (Map<String, dynamic> json) {
|
||||
final puzzles = json['puzzles'];
|
||||
if (puzzles is! List<dynamic>) {
|
||||
throw const FormatException('puzzles: expected a list');
|
||||
}
|
||||
return _puzzleFromJson(e);
|
||||
}).toList();
|
||||
}, logger: _log);
|
||||
return puzzles.map((e) {
|
||||
if (e is! Map<String, dynamic>) {
|
||||
throw const FormatException('Expected an object');
|
||||
}
|
||||
return _puzzleFromJson(e);
|
||||
}).toList();
|
||||
},
|
||||
logger: _log,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,15 +109,20 @@ PuzzleGame _puzzleGameFromPick(RequiredPick pick) {
|
||||
id: pick('id').asGameIdOrThrow(),
|
||||
perf: pick('perf', 'key').asPerfOrThrow(),
|
||||
rated: pick('rated').asBoolOrThrow(),
|
||||
white: pick('players').letOrThrow((it) => it
|
||||
.asListOrThrow(_puzzlePlayerFromPick)
|
||||
.firstWhere((p) => p.side == Side.white)),
|
||||
black: pick('players').letOrThrow((it) => it
|
||||
.asListOrThrow(_puzzlePlayerFromPick)
|
||||
.firstWhere((p) => p.side == Side.black)),
|
||||
white: pick('players').letOrThrow(
|
||||
(it) => it
|
||||
.asListOrThrow(_puzzlePlayerFromPick)
|
||||
.firstWhere((p) => p.side == Side.white),
|
||||
),
|
||||
black: pick('players').letOrThrow(
|
||||
(it) => it
|
||||
.asListOrThrow(_puzzlePlayerFromPick)
|
||||
.firstWhere((p) => p.side == Side.black),
|
||||
),
|
||||
pgn: pick('pgn').asStringOrThrow(),
|
||||
clock: pick('clock').letOrNull(
|
||||
(p) => TimeInc.fromString(p.asStringOrThrow()) ?? const TimeInc(0, 0)),
|
||||
(p) => TimeInc.fromString(p.asStringOrThrow()) ?? const TimeInc(0, 0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -77,241 +77,308 @@ PuzzleThemeL10n puzzleThemeL10n(BuildContext context, PuzzleTheme theme) {
|
||||
switch (theme) {
|
||||
case PuzzleTheme.mix:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.healthyMix,
|
||||
description: context.l10n.healthyMixDescription);
|
||||
name: context.l10n.healthyMix,
|
||||
description: context.l10n.healthyMixDescription,
|
||||
);
|
||||
case PuzzleTheme.advancedPawn:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.advancedPawn,
|
||||
description: context.l10n.advancedPawnDescription);
|
||||
name: context.l10n.advancedPawn,
|
||||
description: context.l10n.advancedPawnDescription,
|
||||
);
|
||||
case PuzzleTheme.advantage:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.advantage,
|
||||
description: context.l10n.advantageDescription);
|
||||
name: context.l10n.advantage,
|
||||
description: context.l10n.advantageDescription,
|
||||
);
|
||||
case PuzzleTheme.anastasiaMate:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.anastasiaMate,
|
||||
description: context.l10n.anastasiaMateDescription);
|
||||
name: context.l10n.anastasiaMate,
|
||||
description: context.l10n.anastasiaMateDescription,
|
||||
);
|
||||
case PuzzleTheme.arabianMate:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.arabianMate,
|
||||
description: context.l10n.arabianMateDescription);
|
||||
name: context.l10n.arabianMate,
|
||||
description: context.l10n.arabianMateDescription,
|
||||
);
|
||||
case PuzzleTheme.attackingF2F7:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.attackingF2F7,
|
||||
description: context.l10n.attackingF2F7Description);
|
||||
name: context.l10n.attackingF2F7,
|
||||
description: context.l10n.attackingF2F7Description,
|
||||
);
|
||||
case PuzzleTheme.attraction:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.attraction,
|
||||
description: context.l10n.attractionDescription);
|
||||
name: context.l10n.attraction,
|
||||
description: context.l10n.attractionDescription,
|
||||
);
|
||||
case PuzzleTheme.backRankMate:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.backRankMate,
|
||||
description: context.l10n.backRankMateDescription);
|
||||
name: context.l10n.backRankMate,
|
||||
description: context.l10n.backRankMateDescription,
|
||||
);
|
||||
case PuzzleTheme.bishopEndgame:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.bishopEndgame,
|
||||
description: context.l10n.bishopEndgameDescription);
|
||||
name: context.l10n.bishopEndgame,
|
||||
description: context.l10n.bishopEndgameDescription,
|
||||
);
|
||||
case PuzzleTheme.bodenMate:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.bodenMate,
|
||||
description: context.l10n.bodenMateDescription);
|
||||
name: context.l10n.bodenMate,
|
||||
description: context.l10n.bodenMateDescription,
|
||||
);
|
||||
case PuzzleTheme.capturingDefender:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.capturingDefender,
|
||||
description: context.l10n.capturingDefenderDescription);
|
||||
name: context.l10n.capturingDefender,
|
||||
description: context.l10n.capturingDefenderDescription,
|
||||
);
|
||||
case PuzzleTheme.castling:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.castling,
|
||||
description: context.l10n.castlingDescription);
|
||||
name: context.l10n.castling,
|
||||
description: context.l10n.castlingDescription,
|
||||
);
|
||||
case PuzzleTheme.clearance:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.clearance,
|
||||
description: context.l10n.clearanceDescription);
|
||||
name: context.l10n.clearance,
|
||||
description: context.l10n.clearanceDescription,
|
||||
);
|
||||
case PuzzleTheme.crushing:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.crushing,
|
||||
description: context.l10n.crushingDescription);
|
||||
name: context.l10n.crushing,
|
||||
description: context.l10n.crushingDescription,
|
||||
);
|
||||
case PuzzleTheme.defensiveMove:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.defensiveMove,
|
||||
description: context.l10n.defensiveMoveDescription);
|
||||
name: context.l10n.defensiveMove,
|
||||
description: context.l10n.defensiveMoveDescription,
|
||||
);
|
||||
case PuzzleTheme.deflection:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.deflection,
|
||||
description: context.l10n.deflectionDescription);
|
||||
name: context.l10n.deflection,
|
||||
description: context.l10n.deflectionDescription,
|
||||
);
|
||||
case PuzzleTheme.discoveredAttack:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.discoveredAttack,
|
||||
description: context.l10n.discoveredAttackDescription);
|
||||
name: context.l10n.discoveredAttack,
|
||||
description: context.l10n.discoveredAttackDescription,
|
||||
);
|
||||
case PuzzleTheme.doubleBishopMate:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.doubleBishopMate,
|
||||
description: context.l10n.doubleBishopMateDescription);
|
||||
name: context.l10n.doubleBishopMate,
|
||||
description: context.l10n.doubleBishopMateDescription,
|
||||
);
|
||||
case PuzzleTheme.doubleCheck:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.doubleCheck,
|
||||
description: context.l10n.doubleCheckDescription);
|
||||
name: context.l10n.doubleCheck,
|
||||
description: context.l10n.doubleCheckDescription,
|
||||
);
|
||||
case PuzzleTheme.dovetailMate:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.dovetailMate,
|
||||
description: context.l10n.dovetailMateDescription);
|
||||
name: context.l10n.dovetailMate,
|
||||
description: context.l10n.dovetailMateDescription,
|
||||
);
|
||||
case PuzzleTheme.equality:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.equality,
|
||||
description: context.l10n.equalityDescription);
|
||||
name: context.l10n.equality,
|
||||
description: context.l10n.equalityDescription,
|
||||
);
|
||||
case PuzzleTheme.endgame:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.endgame,
|
||||
description: context.l10n.endgameDescription);
|
||||
name: context.l10n.endgame,
|
||||
description: context.l10n.endgameDescription,
|
||||
);
|
||||
case PuzzleTheme.enPassant:
|
||||
return PuzzleThemeL10n(
|
||||
name: 'En passant', description: context.l10n.enPassantDescription);
|
||||
name: 'En passant',
|
||||
description: context.l10n.enPassantDescription,
|
||||
);
|
||||
case PuzzleTheme.exposedKing:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.exposedKing,
|
||||
description: context.l10n.exposedKingDescription);
|
||||
name: context.l10n.exposedKing,
|
||||
description: context.l10n.exposedKingDescription,
|
||||
);
|
||||
case PuzzleTheme.fork:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.fork, description: context.l10n.forkDescription);
|
||||
name: context.l10n.fork,
|
||||
description: context.l10n.forkDescription,
|
||||
);
|
||||
case PuzzleTheme.hangingPiece:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.hangingPiece,
|
||||
description: context.l10n.hangingPieceDescription);
|
||||
name: context.l10n.hangingPiece,
|
||||
description: context.l10n.hangingPieceDescription,
|
||||
);
|
||||
case PuzzleTheme.hookMate:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.hookMate,
|
||||
description: context.l10n.hookMateDescription);
|
||||
name: context.l10n.hookMate,
|
||||
description: context.l10n.hookMateDescription,
|
||||
);
|
||||
case PuzzleTheme.interference:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.interference,
|
||||
description: context.l10n.interferenceDescription);
|
||||
name: context.l10n.interference,
|
||||
description: context.l10n.interferenceDescription,
|
||||
);
|
||||
case PuzzleTheme.intermezzo:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.intermezzo,
|
||||
description: context.l10n.intermezzoDescription);
|
||||
name: context.l10n.intermezzo,
|
||||
description: context.l10n.intermezzoDescription,
|
||||
);
|
||||
case PuzzleTheme.kingsideAttack:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.kingsideAttack,
|
||||
description: context.l10n.kingsideAttackDescription);
|
||||
name: context.l10n.kingsideAttack,
|
||||
description: context.l10n.kingsideAttackDescription,
|
||||
);
|
||||
case PuzzleTheme.knightEndgame:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.knightEndgame,
|
||||
description: context.l10n.knightEndgameDescription);
|
||||
name: context.l10n.knightEndgame,
|
||||
description: context.l10n.knightEndgameDescription,
|
||||
);
|
||||
case PuzzleTheme.long:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.long, description: context.l10n.longDescription);
|
||||
name: context.l10n.long,
|
||||
description: context.l10n.longDescription,
|
||||
);
|
||||
case PuzzleTheme.master:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.master,
|
||||
description: context.l10n.masterDescription);
|
||||
name: context.l10n.master,
|
||||
description: context.l10n.masterDescription,
|
||||
);
|
||||
case PuzzleTheme.masterVsMaster:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.masterVsMaster,
|
||||
description: context.l10n.masterVsMasterDescription);
|
||||
name: context.l10n.masterVsMaster,
|
||||
description: context.l10n.masterVsMasterDescription,
|
||||
);
|
||||
case PuzzleTheme.mate:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.mate, description: context.l10n.mateDescription);
|
||||
name: context.l10n.mate,
|
||||
description: context.l10n.mateDescription,
|
||||
);
|
||||
case PuzzleTheme.mateIn1:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.mateIn1,
|
||||
description: context.l10n.mateIn1Description);
|
||||
name: context.l10n.mateIn1,
|
||||
description: context.l10n.mateIn1Description,
|
||||
);
|
||||
case PuzzleTheme.mateIn2:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.mateIn2,
|
||||
description: context.l10n.mateIn2Description);
|
||||
name: context.l10n.mateIn2,
|
||||
description: context.l10n.mateIn2Description,
|
||||
);
|
||||
case PuzzleTheme.mateIn3:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.mateIn3,
|
||||
description: context.l10n.mateIn3Description);
|
||||
name: context.l10n.mateIn3,
|
||||
description: context.l10n.mateIn3Description,
|
||||
);
|
||||
case PuzzleTheme.mateIn4:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.mateIn4,
|
||||
description: context.l10n.mateIn4Description);
|
||||
name: context.l10n.mateIn4,
|
||||
description: context.l10n.mateIn4Description,
|
||||
);
|
||||
case PuzzleTheme.mateIn5:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.mateIn5,
|
||||
description: context.l10n.mateIn5Description);
|
||||
name: context.l10n.mateIn5,
|
||||
description: context.l10n.mateIn5Description,
|
||||
);
|
||||
case PuzzleTheme.smotheredMate:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.smotheredMate,
|
||||
description: context.l10n.smotheredMateDescription);
|
||||
name: context.l10n.smotheredMate,
|
||||
description: context.l10n.smotheredMateDescription,
|
||||
);
|
||||
case PuzzleTheme.middlegame:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.middlegame,
|
||||
description: context.l10n.middlegameDescription);
|
||||
name: context.l10n.middlegame,
|
||||
description: context.l10n.middlegameDescription,
|
||||
);
|
||||
case PuzzleTheme.oneMove:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.oneMove,
|
||||
description: context.l10n.oneMoveDescription);
|
||||
name: context.l10n.oneMove,
|
||||
description: context.l10n.oneMoveDescription,
|
||||
);
|
||||
case PuzzleTheme.opening:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.opening,
|
||||
description: context.l10n.openingDescription);
|
||||
name: context.l10n.opening,
|
||||
description: context.l10n.openingDescription,
|
||||
);
|
||||
case PuzzleTheme.pawnEndgame:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.pawnEndgame,
|
||||
description: context.l10n.pawnEndgameDescription);
|
||||
name: context.l10n.pawnEndgame,
|
||||
description: context.l10n.pawnEndgameDescription,
|
||||
);
|
||||
case PuzzleTheme.pin:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.pin, description: context.l10n.pinDescription);
|
||||
name: context.l10n.pin,
|
||||
description: context.l10n.pinDescription,
|
||||
);
|
||||
case PuzzleTheme.promotion:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.promotion,
|
||||
description: context.l10n.promotionDescription);
|
||||
name: context.l10n.promotion,
|
||||
description: context.l10n.promotionDescription,
|
||||
);
|
||||
case PuzzleTheme.queenEndgame:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.queenEndgame,
|
||||
description: context.l10n.queenEndgameDescription);
|
||||
name: context.l10n.queenEndgame,
|
||||
description: context.l10n.queenEndgameDescription,
|
||||
);
|
||||
case PuzzleTheme.queenRookEndgame:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.queenRookEndgame,
|
||||
description: context.l10n.queenRookEndgameDescription);
|
||||
name: context.l10n.queenRookEndgame,
|
||||
description: context.l10n.queenRookEndgameDescription,
|
||||
);
|
||||
case PuzzleTheme.queensideAttack:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.queensideAttack,
|
||||
description: context.l10n.queensideAttackDescription);
|
||||
name: context.l10n.queensideAttack,
|
||||
description: context.l10n.queensideAttackDescription,
|
||||
);
|
||||
case PuzzleTheme.quietMove:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.quietMove,
|
||||
description: context.l10n.quietMoveDescription);
|
||||
name: context.l10n.quietMove,
|
||||
description: context.l10n.quietMoveDescription,
|
||||
);
|
||||
case PuzzleTheme.rookEndgame:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.rookEndgame,
|
||||
description: context.l10n.rookEndgameDescription);
|
||||
name: context.l10n.rookEndgame,
|
||||
description: context.l10n.rookEndgameDescription,
|
||||
);
|
||||
case PuzzleTheme.sacrifice:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.sacrifice,
|
||||
description: context.l10n.sacrificeDescription);
|
||||
name: context.l10n.sacrifice,
|
||||
description: context.l10n.sacrificeDescription,
|
||||
);
|
||||
case PuzzleTheme.short:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.short, description: context.l10n.shortDescription);
|
||||
name: context.l10n.short,
|
||||
description: context.l10n.shortDescription,
|
||||
);
|
||||
case PuzzleTheme.skewer:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.skewer,
|
||||
description: context.l10n.skewerDescription);
|
||||
name: context.l10n.skewer,
|
||||
description: context.l10n.skewerDescription,
|
||||
);
|
||||
case PuzzleTheme.superGM:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.superGM,
|
||||
description: context.l10n.superGMDescription);
|
||||
name: context.l10n.superGM,
|
||||
description: context.l10n.superGMDescription,
|
||||
);
|
||||
case PuzzleTheme.trappedPiece:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.trappedPiece,
|
||||
description: context.l10n.trappedPieceDescription);
|
||||
name: context.l10n.trappedPiece,
|
||||
description: context.l10n.trappedPieceDescription,
|
||||
);
|
||||
case PuzzleTheme.underPromotion:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.underPromotion,
|
||||
description: context.l10n.underPromotionDescription);
|
||||
name: context.l10n.underPromotion,
|
||||
description: context.l10n.underPromotionDescription,
|
||||
);
|
||||
case PuzzleTheme.veryLong:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.veryLong,
|
||||
description: context.l10n.veryLongDescription);
|
||||
name: context.l10n.veryLong,
|
||||
description: context.l10n.veryLongDescription,
|
||||
);
|
||||
case PuzzleTheme.xRayAttack:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.xRayAttack,
|
||||
description: context.l10n.xRayAttackDescription);
|
||||
name: context.l10n.xRayAttack,
|
||||
description: context.l10n.xRayAttackDescription,
|
||||
);
|
||||
case PuzzleTheme.zugzwang:
|
||||
return PuzzleThemeL10n(
|
||||
name: context.l10n.zugzwang,
|
||||
description: context.l10n.zugzwangDescription);
|
||||
name: context.l10n.zugzwang,
|
||||
description: context.l10n.zugzwangDescription,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,10 +34,13 @@ class PuzzleService {
|
||||
/// Loads the next puzzle from database. Will sync with server if necessary.
|
||||
///
|
||||
/// This future should never fail on network errors.
|
||||
Future<Puzzle?> nextPuzzle(
|
||||
{String? userId, PuzzleTheme angle = PuzzleTheme.mix}) {
|
||||
Future<Puzzle?> nextPuzzle({
|
||||
String? userId,
|
||||
PuzzleTheme angle = PuzzleTheme.mix,
|
||||
}) {
|
||||
return Result.release(
|
||||
_syncAndLoadData(userId, angle).map((data) => data?.unsolved[0]));
|
||||
_syncAndLoadData(userId, angle).map((data) => data?.unsolved[0]),
|
||||
);
|
||||
}
|
||||
|
||||
/// Update puzzle queue with the solved puzzle and sync with server
|
||||
@@ -72,7 +75,9 @@ class PuzzleService {
|
||||
/// This method should never fail, as if the network is down it will fallback
|
||||
/// to the local database.
|
||||
FutureResult<PuzzleLocalData?> _syncAndLoadData(
|
||||
String? userId, PuzzleTheme angle) {
|
||||
String? userId,
|
||||
PuzzleTheme angle,
|
||||
) {
|
||||
final data = db.fetch(userId: userId, angle: angle);
|
||||
|
||||
final unsolved = data?.unsolved ?? IList(const []);
|
||||
@@ -89,11 +94,15 @@ class PuzzleService {
|
||||
|
||||
return batchResult
|
||||
.fold(
|
||||
(value) => Result.value(Tuple2(
|
||||
(value) => Result.value(
|
||||
Tuple2(
|
||||
PuzzleLocalData(
|
||||
solved: IList(const []),
|
||||
unsolved: IList([...unsolved, ...value])),
|
||||
true)),
|
||||
solved: IList(const []),
|
||||
unsolved: IList([...unsolved, ...value]),
|
||||
),
|
||||
true,
|
||||
),
|
||||
),
|
||||
(_, __) => Result.value(Tuple2(data, false)),
|
||||
)
|
||||
.flatMap((tuple) async {
|
||||
|
||||
@@ -19,6 +19,8 @@ class IsSoundMutedNotifier extends StateNotifier<bool> {
|
||||
final isSoundMutedProvider =
|
||||
StateNotifierProvider.autoDispose<IsSoundMutedNotifier, bool>((ref) {
|
||||
final settingsRepository = ref.watch(settingsRepositoryProvider);
|
||||
return IsSoundMutedNotifier(settingsRepository,
|
||||
initialValue: settingsRepository.isSoundMuted());
|
||||
return IsSoundMutedNotifier(
|
||||
settingsRepository,
|
||||
initialValue: settingsRepository.isSoundMuted(),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -69,27 +69,28 @@ class SettingsScreen extends ConsumerWidget {
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
authState.maybeWhen(
|
||||
data: (data) {
|
||||
return data != null
|
||||
? PlatformCard(
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.exit_to_app),
|
||||
title: Text(context.l10n.logOut),
|
||||
onTap: authActionsAsync.isLoading
|
||||
? null
|
||||
: () async {
|
||||
await ref
|
||||
.read(authActionsProvider.notifier)
|
||||
.signOut();
|
||||
ref
|
||||
.read(currentBottomTabProvider.notifier)
|
||||
.state = BottomTab.play;
|
||||
},
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
},
|
||||
orElse: () => const SizedBox.shrink()),
|
||||
data: (data) {
|
||||
return data != null
|
||||
? PlatformCard(
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.exit_to_app),
|
||||
title: Text(context.l10n.logOut),
|
||||
onTap: authActionsAsync.isLoading
|
||||
? null
|
||||
: () async {
|
||||
await ref
|
||||
.read(authActionsProvider.notifier)
|
||||
.signOut();
|
||||
ref
|
||||
.read(currentBottomTabProvider.notifier)
|
||||
.state = BottomTab.play;
|
||||
},
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
},
|
||||
orElse: () => const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -45,10 +45,11 @@ class ThemeModeScreen extends ConsumerWidget {
|
||||
padding: kBodyPadding,
|
||||
children: [
|
||||
ListTileChoice(
|
||||
choices: ThemeMode.values,
|
||||
selectedItem: themeMode,
|
||||
titleBuilder: (t) => Text(themeTitle(context, t)),
|
||||
onSelectedItemChanged: onChanged)
|
||||
choices: ThemeMode.values,
|
||||
selectedItem: themeMode,
|
||||
titleBuilder: (t) => Text(themeTitle(context, t)),
|
||||
onSelectedItemChanged: onChanged,
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -37,25 +37,33 @@ class TvEvent with _$TvEvent {
|
||||
final type = pick('t').asStringOrThrow();
|
||||
switch (type) {
|
||||
case 'featured':
|
||||
return pick('d').letOrThrow((dataPick) => TvEvent.featured(
|
||||
id: dataPick('id').asGameIdOrThrow(),
|
||||
orientation: dataPick('orientation').asSideOrThrow(),
|
||||
fen: dataPick('fen').asStringOrThrow(),
|
||||
white: dataPick('players').letOrThrow((it) => it
|
||||
return pick('d').letOrThrow(
|
||||
(dataPick) => TvEvent.featured(
|
||||
id: dataPick('id').asGameIdOrThrow(),
|
||||
orientation: dataPick('orientation').asSideOrThrow(),
|
||||
fen: dataPick('fen').asStringOrThrow(),
|
||||
white: dataPick('players').letOrThrow(
|
||||
(it) => it
|
||||
.asListOrThrow(_featuredPlayerFromPick)
|
||||
.firstWhere((p) => p.side == Side.white)),
|
||||
black: dataPick('players').letOrThrow((it) => it
|
||||
.firstWhere((p) => p.side == Side.white),
|
||||
),
|
||||
black: dataPick('players').letOrThrow(
|
||||
(it) => it
|
||||
.asListOrThrow(_featuredPlayerFromPick)
|
||||
.firstWhere((p) => p.side == Side.black)),
|
||||
));
|
||||
.firstWhere((p) => p.side == Side.black),
|
||||
),
|
||||
),
|
||||
);
|
||||
case 'fen':
|
||||
return pick('d').letOrThrow((dataPick) => TvEvent.fen(
|
||||
fen: dataPick('fen').asStringOrThrow(),
|
||||
lastMove: dataPick('lm')
|
||||
.letOrThrow((it) => Move.fromUci(it.asStringOrThrow())!),
|
||||
whiteSeconds: dataPick('wc').asIntOrThrow(),
|
||||
blackSeconds: dataPick('bc').asIntOrThrow(),
|
||||
));
|
||||
return pick('d').letOrThrow(
|
||||
(dataPick) => TvEvent.fen(
|
||||
fen: dataPick('fen').asStringOrThrow(),
|
||||
lastMove: dataPick('lm')
|
||||
.letOrThrow((it) => Move.fromUci(it.asStringOrThrow())!),
|
||||
whiteSeconds: dataPick('wc').asIntOrThrow(),
|
||||
blackSeconds: dataPick('bc').asIntOrThrow(),
|
||||
),
|
||||
);
|
||||
default:
|
||||
throw UnsupportedError('Unsupported event type $type');
|
||||
}
|
||||
|
||||
@@ -5,12 +5,13 @@ part 'featured_player.freezed.dart';
|
||||
|
||||
@freezed
|
||||
class FeaturedPlayer with _$FeaturedPlayer {
|
||||
const factory FeaturedPlayer(
|
||||
{required Side side,
|
||||
required String name,
|
||||
String? title,
|
||||
int? rating,
|
||||
int? seconds}) = _FeaturedPlayer;
|
||||
const factory FeaturedPlayer({
|
||||
required Side side,
|
||||
required String name,
|
||||
String? title,
|
||||
int? rating,
|
||||
int? seconds,
|
||||
}) = _FeaturedPlayer;
|
||||
|
||||
const FeaturedPlayer._();
|
||||
|
||||
|
||||
@@ -35,11 +35,12 @@ class TvScreen extends ConsumerWidget {
|
||||
title: const Text('Lichess TV'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: isSoundMuted
|
||||
? const Icon(Icons.volume_off)
|
||||
: const Icon(Icons.volume_up),
|
||||
onPressed: () =>
|
||||
ref.read(isSoundMutedProvider.notifier).toggleSound())
|
||||
icon: isSoundMuted
|
||||
? const Icon(Icons.volume_off)
|
||||
: const Icon(Icons.volume_up),
|
||||
onPressed: () =>
|
||||
ref.read(isSoundMutedProvider.notifier).toggleSound(),
|
||||
)
|
||||
],
|
||||
),
|
||||
body: _buildBody(context, ref),
|
||||
@@ -50,13 +51,15 @@ class TvScreen extends ConsumerWidget {
|
||||
final isSoundMuted = ref.watch(isSoundMutedProvider);
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
trailing: CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
child: isSoundMuted
|
||||
? const Icon(CupertinoIcons.volume_off)
|
||||
: const Icon(CupertinoIcons.volume_up),
|
||||
onPressed: () =>
|
||||
ref.read(isSoundMutedProvider.notifier).toggleSound())),
|
||||
trailing: CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
child: isSoundMuted
|
||||
? const Icon(CupertinoIcons.volume_off)
|
||||
: const Icon(CupertinoIcons.volume_up),
|
||||
onPressed: () =>
|
||||
ref.read(isSoundMutedProvider.notifier).toggleSound(),
|
||||
),
|
||||
),
|
||||
child: _buildBody(context, ref),
|
||||
);
|
||||
}
|
||||
@@ -96,7 +99,8 @@ class TvScreen extends ConsumerWidget {
|
||||
rating: topPlayer.rating,
|
||||
clock: Duration(seconds: topPlayer.seconds ?? 0),
|
||||
active:
|
||||
!position.isGameOver && position.turn == topPlayer.side)
|
||||
!position.isGameOver && position.turn == topPlayer.side,
|
||||
)
|
||||
: kEmptyWidget;
|
||||
final bottomPlayerWidget = bottomPlayer != null
|
||||
? BoardPlayer(
|
||||
@@ -105,7 +109,8 @@ class TvScreen extends ConsumerWidget {
|
||||
rating: bottomPlayer.rating,
|
||||
clock: Duration(seconds: bottomPlayer.seconds ?? 0),
|
||||
active: !position.isGameOver &&
|
||||
position.turn == bottomPlayer.side)
|
||||
position.turn == bottomPlayer.side,
|
||||
)
|
||||
: kEmptyWidget;
|
||||
return GameBoardLayout(
|
||||
boardData: boardData,
|
||||
@@ -126,7 +131,8 @@ class TvScreen extends ConsumerWidget {
|
||||
),
|
||||
error: (err, stackTrace) {
|
||||
debugPrint(
|
||||
'SEVERE: [TvScreen] could not load stream; $err\n$stackTrace');
|
||||
'SEVERE: [TvScreen] could not load stream; $err\n$stackTrace',
|
||||
);
|
||||
return const Text('Could not load TV stream.');
|
||||
},
|
||||
),
|
||||
|
||||
@@ -13,13 +13,16 @@ final tvStreamProvider = StreamProvider.autoDispose<FeaturedPosition>((ref) {
|
||||
tvRepository.dispose();
|
||||
});
|
||||
return tvRepository.tvFeed().map((event) {
|
||||
return event.map(featured: (featuredEvent) {
|
||||
featuredGameNotifier.onFeaturedEvent(featuredEvent);
|
||||
return FeaturedPosition.fromTvEvent(featuredEvent);
|
||||
}, fen: (fenEvent) {
|
||||
featuredGameNotifier.onFenEvent(fenEvent);
|
||||
soundService.playMove();
|
||||
return FeaturedPosition.fromTvEvent(fenEvent);
|
||||
});
|
||||
return event.map(
|
||||
featured: (featuredEvent) {
|
||||
featuredGameNotifier.onFeaturedEvent(featuredEvent);
|
||||
return FeaturedPosition.fromTvEvent(featuredEvent);
|
||||
},
|
||||
fen: (fenEvent) {
|
||||
featuredGameNotifier.onFenEvent(fenEvent);
|
||||
soundService.playMove();
|
||||
return FeaturedPosition.fromTvEvent(fenEvent);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,8 +18,11 @@ class LeaderboardRepository {
|
||||
return apiClient
|
||||
.get(Uri.parse('$kLichessHost/api/player'))
|
||||
.flatMap((response) {
|
||||
return readJsonObject(response.body,
|
||||
mapper: Leaderboard.fromJson, logger: _log);
|
||||
return readJsonObject(
|
||||
response.body,
|
||||
mapper: Leaderboard.fromJson,
|
||||
logger: _log,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -29,7 +32,9 @@ class LeaderboardRepository {
|
||||
final leaderboardRepositoryProvider = Provider<LeaderboardRepository>((ref) {
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
final repo = LeaderboardRepository(
|
||||
apiClient: apiClient, logger: Logger('LeaderboardRepository'));
|
||||
apiClient: apiClient,
|
||||
logger: Logger('LeaderboardRepository'),
|
||||
);
|
||||
ref.onDispose(() => repo.dispose());
|
||||
return repo;
|
||||
});
|
||||
|
||||
@@ -26,26 +26,42 @@ class UserRepository {
|
||||
|
||||
FutureResult<User> getUser(String username) {
|
||||
return apiClient.get(Uri.parse('$kLichessHost/api/user/$username')).then(
|
||||
(result) => result.flatMap((response) => readJsonObject(response.body,
|
||||
mapper: User.fromJson, logger: _log)));
|
||||
(result) => result.flatMap(
|
||||
(response) => readJsonObject(
|
||||
response.body,
|
||||
mapper: User.fromJson,
|
||||
logger: _log,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
FutureResult<UserPerfStats> getUserPerfStats(String username, Perf perf) {
|
||||
return apiClient
|
||||
.get(Uri.parse('$kLichessHost/api/user/$username/perf/${perf.name}'))
|
||||
.then((result) => result.flatMap((response) => readJsonObject(
|
||||
response.body,
|
||||
mapper: _userPerfStatsFromJson,
|
||||
logger: _log)));
|
||||
.then(
|
||||
(result) => result.flatMap(
|
||||
(response) => readJsonObject(
|
||||
response.body,
|
||||
mapper: _userPerfStatsFromJson,
|
||||
logger: _log,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
FutureResult<List<UserStatus>> getUsersStatus(List<String> ids) {
|
||||
return apiClient
|
||||
.get(Uri.parse('$kLichessHost/api/users/status?ids=${ids.join(',')}'))
|
||||
.then((result) => result.flatMap((response) => readJsonListOfObjects(
|
||||
response.body,
|
||||
mapper: UserStatus.fromJson,
|
||||
logger: _log)));
|
||||
.then(
|
||||
(result) => result.flatMap(
|
||||
(response) => readJsonListOfObjects(
|
||||
response.body,
|
||||
mapper: UserStatus.fromJson,
|
||||
logger: _log,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
@@ -121,18 +137,20 @@ UserStreak _userStreakFromPick(RequiredPick pick) {
|
||||
switch (type) {
|
||||
case 'time':
|
||||
return UserStreak.timeStreak(
|
||||
timePlayed: value.asDurationFromSecondsOrThrow(),
|
||||
isValueEmpty: isValueEmpty,
|
||||
startGame: startGame,
|
||||
endGame: endGame);
|
||||
timePlayed: value.asDurationFromSecondsOrThrow(),
|
||||
isValueEmpty: isValueEmpty,
|
||||
startGame: startGame,
|
||||
endGame: endGame,
|
||||
);
|
||||
case 'win':
|
||||
case 'loss':
|
||||
case 'nb':
|
||||
return UserStreak.gameStreak(
|
||||
gamesPlayed: value.asIntOrThrow(),
|
||||
isValueEmpty: isValueEmpty,
|
||||
startGame: startGame,
|
||||
endGame: endGame);
|
||||
gamesPlayed: value.asIntOrThrow(),
|
||||
isValueEmpty: isValueEmpty,
|
||||
startGame: startGame,
|
||||
endGame: endGame,
|
||||
);
|
||||
default:
|
||||
throw PickException("cannot decode $pick as 'UserStreak'");
|
||||
}
|
||||
|
||||
@@ -5,55 +5,56 @@ part 'leaderboard.freezed.dart';
|
||||
|
||||
@freezed
|
||||
class Leaderboard with _$Leaderboard {
|
||||
const factory Leaderboard(
|
||||
{required List<LeaderboardUser> bullet,
|
||||
required List<LeaderboardUser> blitz,
|
||||
required List<LeaderboardUser> rapid,
|
||||
required List<LeaderboardUser> classical,
|
||||
required List<LeaderboardUser> ultrabullet,
|
||||
required List<LeaderboardUser> crazyhouse,
|
||||
required List<LeaderboardUser> chess960,
|
||||
required List<LeaderboardUser> kingOfThehill,
|
||||
required List<LeaderboardUser> threeCheck,
|
||||
required List<LeaderboardUser> antichess,
|
||||
required List<LeaderboardUser> atomic,
|
||||
required List<LeaderboardUser> horde,
|
||||
required List<LeaderboardUser> racingKings}) = _Leaderboard;
|
||||
const factory Leaderboard({
|
||||
required List<LeaderboardUser> bullet,
|
||||
required List<LeaderboardUser> blitz,
|
||||
required List<LeaderboardUser> rapid,
|
||||
required List<LeaderboardUser> classical,
|
||||
required List<LeaderboardUser> ultrabullet,
|
||||
required List<LeaderboardUser> crazyhouse,
|
||||
required List<LeaderboardUser> chess960,
|
||||
required List<LeaderboardUser> kingOfThehill,
|
||||
required List<LeaderboardUser> threeCheck,
|
||||
required List<LeaderboardUser> antichess,
|
||||
required List<LeaderboardUser> atomic,
|
||||
required List<LeaderboardUser> horde,
|
||||
required List<LeaderboardUser> racingKings,
|
||||
}) = _Leaderboard;
|
||||
|
||||
factory Leaderboard.fromJson(Map<String, dynamic> json) =>
|
||||
Leaderboard.fromPick(pick(json).required());
|
||||
|
||||
factory Leaderboard.fromPick(RequiredPick pick) {
|
||||
return Leaderboard(
|
||||
bullet: pick('bullet').asListOrEmpty(LeaderboardUser.fromPick),
|
||||
blitz: pick('blitz').asListOrEmpty(LeaderboardUser.fromPick),
|
||||
rapid: pick('rapid').asListOrEmpty(LeaderboardUser.fromPick),
|
||||
classical: pick('classical').asListOrEmpty(LeaderboardUser.fromPick),
|
||||
ultrabullet:
|
||||
pick('ultraBullet').asListOrEmpty(LeaderboardUser.fromPick),
|
||||
crazyhouse: pick('crazyhouse').asListOrEmpty(LeaderboardUser.fromPick),
|
||||
chess960: pick('chess960').asListOrEmpty(LeaderboardUser.fromPick),
|
||||
kingOfThehill:
|
||||
pick('kingOfTheHill').asListOrEmpty(LeaderboardUser.fromPick),
|
||||
threeCheck: pick('threeCheck').asListOrEmpty(LeaderboardUser.fromPick),
|
||||
antichess: pick('antichess').asListOrEmpty(LeaderboardUser.fromPick),
|
||||
atomic: pick('atomic').asListOrEmpty(LeaderboardUser.fromPick),
|
||||
horde: pick('horde').asListOrEmpty(LeaderboardUser.fromPick),
|
||||
racingKings:
|
||||
pick('racingKings').asListOrEmpty(LeaderboardUser.fromPick));
|
||||
bullet: pick('bullet').asListOrEmpty(LeaderboardUser.fromPick),
|
||||
blitz: pick('blitz').asListOrEmpty(LeaderboardUser.fromPick),
|
||||
rapid: pick('rapid').asListOrEmpty(LeaderboardUser.fromPick),
|
||||
classical: pick('classical').asListOrEmpty(LeaderboardUser.fromPick),
|
||||
ultrabullet: pick('ultraBullet').asListOrEmpty(LeaderboardUser.fromPick),
|
||||
crazyhouse: pick('crazyhouse').asListOrEmpty(LeaderboardUser.fromPick),
|
||||
chess960: pick('chess960').asListOrEmpty(LeaderboardUser.fromPick),
|
||||
kingOfThehill:
|
||||
pick('kingOfTheHill').asListOrEmpty(LeaderboardUser.fromPick),
|
||||
threeCheck: pick('threeCheck').asListOrEmpty(LeaderboardUser.fromPick),
|
||||
antichess: pick('antichess').asListOrEmpty(LeaderboardUser.fromPick),
|
||||
atomic: pick('atomic').asListOrEmpty(LeaderboardUser.fromPick),
|
||||
horde: pick('horde').asListOrEmpty(LeaderboardUser.fromPick),
|
||||
racingKings: pick('racingKings').asListOrEmpty(LeaderboardUser.fromPick),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class LeaderboardUser with _$LeaderboardUser {
|
||||
const factory LeaderboardUser(
|
||||
{required String id,
|
||||
required String username,
|
||||
bool? patron,
|
||||
String? title,
|
||||
bool? online,
|
||||
required int rating,
|
||||
required int progress}) = _LeaderboardUser;
|
||||
const factory LeaderboardUser({
|
||||
required String id,
|
||||
required String username,
|
||||
bool? patron,
|
||||
String? title,
|
||||
bool? online,
|
||||
required int rating,
|
||||
required int progress,
|
||||
}) = _LeaderboardUser;
|
||||
|
||||
factory LeaderboardUser.fromJson(Map<String, dynamic> json) =>
|
||||
LeaderboardUser.fromPick(pick(json).required());
|
||||
@@ -62,17 +63,19 @@ class LeaderboardUser with _$LeaderboardUser {
|
||||
final prefMap = pick('perfs').asMapOrThrow<String, Map<String, dynamic>>();
|
||||
|
||||
return LeaderboardUser(
|
||||
id: pick('id').asStringOrThrow(),
|
||||
username: pick('username').asStringOrThrow(),
|
||||
title: pick('title').asStringOrNull(),
|
||||
patron: pick('patron').asBoolOrNull(),
|
||||
online: pick('online').asBoolOrNull(),
|
||||
rating: pick('perfs')
|
||||
.letOrThrow((perfsPick) => perfsPick(prefMap.keys.first, 'rating'))
|
||||
.asIntOrThrow(),
|
||||
progress: pick('perfs')
|
||||
.letOrThrow(
|
||||
(prefsPick) => prefsPick(prefMap.keys.first, 'progress'))
|
||||
.asIntOrThrow());
|
||||
id: pick('id').asStringOrThrow(),
|
||||
username: pick('username').asStringOrThrow(),
|
||||
title: pick('title').asStringOrNull(),
|
||||
patron: pick('patron').asBoolOrNull(),
|
||||
online: pick('online').asBoolOrNull(),
|
||||
rating: pick('perfs')
|
||||
.letOrThrow((perfsPick) => perfsPick(prefMap.keys.first, 'rating'))
|
||||
.asIntOrThrow(),
|
||||
progress: pick('perfs')
|
||||
.letOrThrow(
|
||||
(prefsPick) => prefsPick(prefMap.keys.first, 'progress'),
|
||||
)
|
||||
.asIntOrThrow(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,8 +64,9 @@ class PlayTime with _$PlayTime {
|
||||
|
||||
factory PlayTime.fromPick(RequiredPick pick) {
|
||||
return PlayTime(
|
||||
total: pick('total').asDurationFromSecondsOrThrow(),
|
||||
tv: pick('tv').asDurationFromSecondsOrThrow());
|
||||
total: pick('total').asDurationFromSecondsOrThrow(),
|
||||
tv: pick('tv').asDurationFromSecondsOrThrow(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,37 +155,38 @@ class UserPerfStatsParameters with _$UserPerfStatsParameters {
|
||||
|
||||
@freezed
|
||||
class UserPerfStats with _$UserPerfStats {
|
||||
const factory UserPerfStats(
|
||||
{required double rating,
|
||||
required double deviation,
|
||||
bool? provisional,
|
||||
required int totalGames,
|
||||
required int progress,
|
||||
int? rank,
|
||||
double? percentile,
|
||||
required int berserkGames,
|
||||
required int tournamentGames,
|
||||
required int ratedGames,
|
||||
required int wonGames,
|
||||
required int lostGames,
|
||||
required int drawnGames,
|
||||
required int disconnections,
|
||||
double? avgOpponent,
|
||||
required Duration timePlayed,
|
||||
int? lowestRating,
|
||||
UserPerfGame? lowestRatingGame,
|
||||
int? highestRating,
|
||||
UserPerfGame? highestRatingGame,
|
||||
UserStreak? curWinStreak,
|
||||
UserStreak? maxWinStreak,
|
||||
UserStreak? curLossStreak,
|
||||
UserStreak? maxLossStreak,
|
||||
UserStreak? curPlayStreak,
|
||||
UserStreak? maxPlayStreak,
|
||||
UserStreak? curTimeStreak,
|
||||
UserStreak? maxTimeStreak,
|
||||
List<UserPerfGame>? worstLosses,
|
||||
List<UserPerfGame>? bestWins}) = _UserPerfStats;
|
||||
const factory UserPerfStats({
|
||||
required double rating,
|
||||
required double deviation,
|
||||
bool? provisional,
|
||||
required int totalGames,
|
||||
required int progress,
|
||||
int? rank,
|
||||
double? percentile,
|
||||
required int berserkGames,
|
||||
required int tournamentGames,
|
||||
required int ratedGames,
|
||||
required int wonGames,
|
||||
required int lostGames,
|
||||
required int drawnGames,
|
||||
required int disconnections,
|
||||
double? avgOpponent,
|
||||
required Duration timePlayed,
|
||||
int? lowestRating,
|
||||
UserPerfGame? lowestRatingGame,
|
||||
int? highestRating,
|
||||
UserPerfGame? highestRatingGame,
|
||||
UserStreak? curWinStreak,
|
||||
UserStreak? maxWinStreak,
|
||||
UserStreak? curLossStreak,
|
||||
UserStreak? maxLossStreak,
|
||||
UserStreak? curPlayStreak,
|
||||
UserStreak? maxPlayStreak,
|
||||
UserStreak? curTimeStreak,
|
||||
UserStreak? maxTimeStreak,
|
||||
List<UserPerfGame>? worstLosses,
|
||||
List<UserPerfGame>? bestWins,
|
||||
}) = _UserPerfStats;
|
||||
}
|
||||
|
||||
@freezed
|
||||
@@ -206,11 +208,12 @@ class UserStreak with _$UserStreak {
|
||||
|
||||
@freezed
|
||||
class UserPerfGame with _$UserPerfGame {
|
||||
const factory UserPerfGame(
|
||||
{required DateTime finishedAt,
|
||||
required GameId gameId,
|
||||
int? opponentRating,
|
||||
String? opponentId,
|
||||
String? opponentName,
|
||||
String? opponentTitle}) = _UserPerfGame;
|
||||
const factory UserPerfGame({
|
||||
required DateTime finishedAt,
|
||||
required GameId gameId,
|
||||
int? opponentRating,
|
||||
String? opponentId,
|
||||
String? opponentName,
|
||||
String? opponentTitle,
|
||||
}) = _UserPerfGame;
|
||||
}
|
||||
|
||||
@@ -20,28 +20,37 @@ class LeaderboardScreen extends StatelessWidget {
|
||||
|
||||
Widget _buildIos(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
previousPageTitle: 'Home',
|
||||
middle: Text(context.l10n.leaderboard),
|
||||
),
|
||||
child: LayoutBuilder(builder: (context, constraints) {
|
||||
return CustomScrollView(slivers: [
|
||||
SliverSafeArea(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
previousPageTitle: 'Home',
|
||||
middle: Text(context.l10n.leaderboard),
|
||||
),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverSafeArea(
|
||||
sliver: SliverPadding(
|
||||
padding: kBodyPadding,
|
||||
sliver: constraints.maxWidth > kLargeScreenWidth
|
||||
? SliverGrid(
|
||||
gridDelegate:
|
||||
SliverGridDelegateWithFixedCrossAxisCount(
|
||||
mainAxisExtent: 644,
|
||||
crossAxisCount:
|
||||
(constraints.maxWidth / 300).floor()),
|
||||
delegate: SliverChildListDelegate(_buildList()))
|
||||
: SliverList(
|
||||
delegate: SliverChildListDelegate(_buildList()),
|
||||
)))
|
||||
]);
|
||||
}));
|
||||
padding: kBodyPadding,
|
||||
sliver: constraints.maxWidth > kLargeScreenWidth
|
||||
? SliverGrid(
|
||||
gridDelegate:
|
||||
SliverGridDelegateWithFixedCrossAxisCount(
|
||||
mainAxisExtent: 644,
|
||||
crossAxisCount:
|
||||
(constraints.maxWidth / 300).floor(),
|
||||
),
|
||||
delegate: SliverChildListDelegate(_buildList()),
|
||||
)
|
||||
: SliverList(
|
||||
delegate: SliverChildListDelegate(_buildList()),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAndroid(BuildContext context) {
|
||||
@@ -76,23 +85,47 @@ class LeaderboardScreen extends StatelessWidget {
|
||||
_BuildLeaderboard(leaderboard.blitz, LichessIcons.blitz, 'BLITZ'),
|
||||
_BuildLeaderboard(leaderboard.rapid, LichessIcons.rapid, 'RAPID'),
|
||||
_BuildLeaderboard(
|
||||
leaderboard.classical, LichessIcons.classical, 'CLASSICAL'),
|
||||
leaderboard.classical,
|
||||
LichessIcons.classical,
|
||||
'CLASSICAL',
|
||||
),
|
||||
_BuildLeaderboard(
|
||||
leaderboard.ultrabullet, LichessIcons.ultrabullet, 'ULTRA BULLET'),
|
||||
leaderboard.ultrabullet,
|
||||
LichessIcons.ultrabullet,
|
||||
'ULTRA BULLET',
|
||||
),
|
||||
_BuildLeaderboard(
|
||||
leaderboard.crazyhouse, LichessIcons.h_square, 'CRAZYHOUSE'),
|
||||
leaderboard.crazyhouse,
|
||||
LichessIcons.h_square,
|
||||
'CRAZYHOUSE',
|
||||
),
|
||||
_BuildLeaderboard(
|
||||
leaderboard.chess960, LichessIcons.die_six, 'CHESS 960'),
|
||||
leaderboard.chess960,
|
||||
LichessIcons.die_six,
|
||||
'CHESS 960',
|
||||
),
|
||||
_BuildLeaderboard(
|
||||
leaderboard.kingOfThehill, LichessIcons.bullet, 'KING OF THE HILL'),
|
||||
leaderboard.kingOfThehill,
|
||||
LichessIcons.bullet,
|
||||
'KING OF THE HILL',
|
||||
),
|
||||
_BuildLeaderboard(
|
||||
leaderboard.threeCheck, LichessIcons.three_check, 'THREE CHECK'),
|
||||
leaderboard.threeCheck,
|
||||
LichessIcons.three_check,
|
||||
'THREE CHECK',
|
||||
),
|
||||
_BuildLeaderboard(leaderboard.atomic, LichessIcons.atom, 'ATOMIC'),
|
||||
_BuildLeaderboard(leaderboard.horde, LichessIcons.horde, 'HORDE'),
|
||||
_BuildLeaderboard(
|
||||
leaderboard.antichess, LichessIcons.antichess, 'ANTICHESS'),
|
||||
leaderboard.antichess,
|
||||
LichessIcons.antichess,
|
||||
'ANTICHESS',
|
||||
),
|
||||
_BuildLeaderboard(
|
||||
leaderboard.racingKings, LichessIcons.racing_kings, 'RACING KINGS'),
|
||||
leaderboard.racingKings,
|
||||
LichessIcons.racing_kings,
|
||||
'RACING KINGS',
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -113,9 +146,10 @@ class _BuildLeaderboard extends StatelessWidget {
|
||||
title: Text(title),
|
||||
),
|
||||
...ListTile.divideTiles(
|
||||
color: dividerColor(context),
|
||||
context: context,
|
||||
tiles: userList.map((user) => LeaderboardListTile(user: user))),
|
||||
color: dividerColor(context),
|
||||
context: context,
|
||||
tiles: userList.map((user) => LeaderboardListTile(user: user)),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
);
|
||||
@@ -139,9 +173,13 @@ class LeaderboardListTile extends StatelessWidget {
|
||||
title: Row(
|
||||
children: [
|
||||
if (user.title != null) ...[
|
||||
Text(user.title!,
|
||||
style: const TextStyle(
|
||||
color: LichessColors.brag, fontWeight: FontWeight.bold)),
|
||||
Text(
|
||||
user.title!,
|
||||
style: const TextStyle(
|
||||
color: LichessColors.brag,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 5)
|
||||
],
|
||||
Flexible(
|
||||
@@ -171,14 +209,18 @@ class _RatingAndProgress extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
const Icon(LichessIcons.arrow_full_lowerright,
|
||||
size: 20, color: LichessColors.red),
|
||||
const Icon(
|
||||
LichessIcons.arrow_full_lowerright,
|
||||
size: 20,
|
||||
color: LichessColors.red,
|
||||
),
|
||||
SizedBox(
|
||||
width: 30,
|
||||
child: Text(
|
||||
'${progress.abs()}',
|
||||
style: const TextStyle(color: LichessColors.red),
|
||||
))
|
||||
width: 30,
|
||||
child: Text(
|
||||
'${progress.abs()}',
|
||||
style: const TextStyle(color: LichessColors.red),
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
else if (progress > 0)
|
||||
@@ -186,14 +228,18 @@ class _RatingAndProgress extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(LichessIcons.arrow_full_upperright,
|
||||
size: 20, color: LichessColors.good),
|
||||
const Icon(
|
||||
LichessIcons.arrow_full_upperright,
|
||||
size: 20,
|
||||
color: LichessColors.good,
|
||||
),
|
||||
SizedBox(
|
||||
width: 30,
|
||||
child: Text(
|
||||
'$progress',
|
||||
style: const TextStyle(color: LichessColors.good),
|
||||
))
|
||||
width: 30,
|
||||
child: Text(
|
||||
'$progress',
|
||||
style: const TextStyle(color: LichessColors.good),
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
else
|
||||
@@ -211,12 +257,16 @@ class _OnlineOrPatron extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (patron != null) {
|
||||
return Icon(LichessIcons.patron,
|
||||
color: online != null ? LichessColors.good : LichessColors.grey);
|
||||
return Icon(
|
||||
LichessIcons.patron,
|
||||
color: online != null ? LichessColors.good : LichessColors.grey,
|
||||
);
|
||||
} else {
|
||||
return Icon(CupertinoIcons.circle_fill,
|
||||
size: 20,
|
||||
color: online != null ? LichessColors.good : LichessColors.grey);
|
||||
return Icon(
|
||||
CupertinoIcons.circle_fill,
|
||||
size: 20,
|
||||
color: online != null ? LichessColors.good : LichessColors.grey,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,50 +23,79 @@ class LeaderboardWidget extends ConsumerWidget {
|
||||
final leaderboardState = ref.watch(leaderListProvider);
|
||||
|
||||
return leaderboardState.when(
|
||||
data: (data) {
|
||||
return Column(children: [
|
||||
data: (data) {
|
||||
return Column(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(
|
||||
context.l10n.leaderboard,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.of(context).push<void>(MaterialPageRoute(
|
||||
Navigator.of(context).push<void>(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => LeaderboardScreen(
|
||||
leaderboard: data,
|
||||
)));
|
||||
leaderboard: data,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
trailing: const Icon(CupertinoIcons.forward),
|
||||
),
|
||||
LeaderboardListTile(
|
||||
user: data.bullet[0], perfIcon: LichessIcons.bullet),
|
||||
user: data.bullet[0],
|
||||
perfIcon: LichessIcons.bullet,
|
||||
),
|
||||
LeaderboardListTile(
|
||||
user: data.blitz[0], perfIcon: LichessIcons.blitz),
|
||||
user: data.blitz[0],
|
||||
perfIcon: LichessIcons.blitz,
|
||||
),
|
||||
LeaderboardListTile(
|
||||
user: data.rapid[0], perfIcon: LichessIcons.rapid),
|
||||
user: data.rapid[0],
|
||||
perfIcon: LichessIcons.rapid,
|
||||
),
|
||||
LeaderboardListTile(
|
||||
user: data.classical[0], perfIcon: LichessIcons.classical),
|
||||
user: data.classical[0],
|
||||
perfIcon: LichessIcons.classical,
|
||||
),
|
||||
LeaderboardListTile(
|
||||
user: data.ultrabullet[0], perfIcon: LichessIcons.ultrabullet),
|
||||
user: data.ultrabullet[0],
|
||||
perfIcon: LichessIcons.ultrabullet,
|
||||
),
|
||||
LeaderboardListTile(
|
||||
user: data.crazyhouse[0], perfIcon: LichessIcons.h_square),
|
||||
user: data.crazyhouse[0],
|
||||
perfIcon: LichessIcons.h_square,
|
||||
),
|
||||
LeaderboardListTile(
|
||||
user: data.chess960[0], perfIcon: LichessIcons.die_six),
|
||||
user: data.chess960[0],
|
||||
perfIcon: LichessIcons.die_six,
|
||||
),
|
||||
LeaderboardListTile(
|
||||
user: data.threeCheck[0], perfIcon: LichessIcons.three_check),
|
||||
user: data.threeCheck[0],
|
||||
perfIcon: LichessIcons.three_check,
|
||||
),
|
||||
LeaderboardListTile(
|
||||
user: data.atomic[0], perfIcon: LichessIcons.atom),
|
||||
user: data.atomic[0],
|
||||
perfIcon: LichessIcons.atom,
|
||||
),
|
||||
LeaderboardListTile(
|
||||
user: data.horde[0], perfIcon: LichessIcons.horde),
|
||||
user: data.horde[0],
|
||||
perfIcon: LichessIcons.horde,
|
||||
),
|
||||
LeaderboardListTile(
|
||||
user: data.antichess[0], perfIcon: LichessIcons.antichess),
|
||||
]);
|
||||
},
|
||||
error: (error, stackTrace) {
|
||||
debugPrint(
|
||||
'SEVERE: [LeaderboardWidget] could not lead leaderboard data; $error\n $stackTrace');
|
||||
return const Text('could not lead leaderboard');
|
||||
},
|
||||
loading: () => const CenterLoadingIndicator());
|
||||
user: data.antichess[0],
|
||||
perfIcon: LichessIcons.antichess,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
error: (error, stackTrace) {
|
||||
debugPrint(
|
||||
'SEVERE: [LeaderboardWidget] could not lead leaderboard data; $error\n $stackTrace',
|
||||
);
|
||||
return const Text('could not lead leaderboard');
|
||||
},
|
||||
loading: () => const CenterLoadingIndicator(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,8 @@ final perfStatsProvider = FutureProvider.autoDispose
|
||||
.family<UserPerfStats, UserPerfStatsParameters>((ref, perfParams) {
|
||||
final userRepo = ref.watch(userRepositoryProvider);
|
||||
return Result.release(
|
||||
userRepo.getUserPerfStats(perfParams.username, perfParams.perf));
|
||||
userRepo.getUserPerfStats(perfParams.username, perfParams.perf),
|
||||
);
|
||||
});
|
||||
|
||||
// that one can be cached forever
|
||||
@@ -48,11 +49,12 @@ const _titleFontSize = 18.0;
|
||||
const _mainValueStyle = TextStyle(fontWeight: FontWeight.bold, fontSize: 30);
|
||||
|
||||
class PerfStatsScreen extends ConsumerWidget {
|
||||
const PerfStatsScreen(
|
||||
{required this.user,
|
||||
required this.perf,
|
||||
required this.loggedInUser,
|
||||
super.key});
|
||||
const PerfStatsScreen({
|
||||
required this.user,
|
||||
required this.perf,
|
||||
required this.loggedInUser,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final User user;
|
||||
final Perf perf;
|
||||
@@ -61,45 +63,53 @@ class PerfStatsScreen extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ConsumerPlatformWidget(
|
||||
ref: ref, androidBuilder: _androidBuilder, iosBuilder: _iosBuilder);
|
||||
ref: ref,
|
||||
androidBuilder: _androidBuilder,
|
||||
iosBuilder: _iosBuilder,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _androidBuilder(BuildContext context, WidgetRef ref) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
titleSpacing: 0,
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Icon(perf.icon),
|
||||
const SizedBox(width: 5),
|
||||
Expanded(
|
||||
child: FittedBox(
|
||||
alignment: Alignment.centerLeft,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
context.l10n.perfStats('${user.username} ${perf.title}'),
|
||||
style: const TextStyle(fontSize: _titleFontSize),
|
||||
),
|
||||
appBar: AppBar(
|
||||
titleSpacing: 0,
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Icon(perf.icon),
|
||||
const SizedBox(width: 5),
|
||||
Expanded(
|
||||
child: FittedBox(
|
||||
alignment: Alignment.centerLeft,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
context.l10n.perfStats('${user.username} ${perf.title}'),
|
||||
style: const TextStyle(fontSize: _titleFontSize),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 5)
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 5)
|
||||
],
|
||||
),
|
||||
body: _buildBody(context, ref));
|
||||
),
|
||||
body: _buildBody(context, ref),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _iosBuilder(BuildContext context, WidgetRef ref) {
|
||||
// TODO: Add perf icon to title.
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: const CupertinoNavigationBar(),
|
||||
child: _buildBody(context, ref));
|
||||
navigationBar: const CupertinoNavigationBar(),
|
||||
child: _buildBody(context, ref),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody(BuildContext context, WidgetRef ref) {
|
||||
final perfStats = ref.watch(perfStatsProvider(
|
||||
UserPerfStatsParameters(username: user.username, perf: perf)));
|
||||
final perfStats = ref.watch(
|
||||
perfStatsProvider(
|
||||
UserPerfStatsParameters(username: user.username, perf: perf),
|
||||
),
|
||||
);
|
||||
|
||||
const statGroupSpace = SizedBox(height: 15.0);
|
||||
const subStatSpace = SizedBox(height: 10);
|
||||
@@ -129,37 +139,52 @@ class PerfStatsScreen extends ConsumerWidget {
|
||||
(loggedInUser != null &&
|
||||
loggedInUser!.username == user.username)
|
||||
? context.l10n.youAreBetterThanPercentOfPerfTypePlayers(
|
||||
'${data.percentile!.toStringAsFixed(2)}%', perf.title)
|
||||
'${data.percentile!.toStringAsFixed(2)}%',
|
||||
perf.title,
|
||||
)
|
||||
: context.l10n.userIsBetterThanPercentOfPerfTypePlayers(
|
||||
user.username,
|
||||
'${data.percentile!.toStringAsFixed(2)}%',
|
||||
perf.title),
|
||||
perf.title,
|
||||
),
|
||||
style: TextStyle(color: textShade(context, 0.7)),
|
||||
),
|
||||
subStatSpace,
|
||||
// The number '12' here is not arbitrary, since the API returns the progression for the last 12 games (as far as I know).
|
||||
_CustomPlatformCard(
|
||||
context.l10n.progressOverLastXGames('12').replaceAll(':', ''),
|
||||
child: _ProgressionWidget(data.progress)),
|
||||
context.l10n.progressOverLastXGames('12').replaceAll(':', ''),
|
||||
child: _ProgressionWidget(data.progress),
|
||||
),
|
||||
_CustomPlatformCardRow([
|
||||
_CustomPlatformCard(context.l10n.rank,
|
||||
value: data.rank == null
|
||||
? '?'
|
||||
: NumberFormat.decimalPattern(Intl.getCurrentLocale())
|
||||
.format(data.rank)),
|
||||
_CustomPlatformCard(
|
||||
context.l10n.ratingDeviation('').replaceAll(': .', ''),
|
||||
value: data.deviation.toStringAsFixed(2))
|
||||
context.l10n.rank,
|
||||
value: data.rank == null
|
||||
? '?'
|
||||
: NumberFormat.decimalPattern(Intl.getCurrentLocale())
|
||||
.format(data.rank),
|
||||
),
|
||||
_CustomPlatformCard(
|
||||
context.l10n.ratingDeviation('').replaceAll(': .', ''),
|
||||
value: data.deviation.toStringAsFixed(2),
|
||||
)
|
||||
]),
|
||||
_CustomPlatformCardRow([
|
||||
_CustomPlatformCard(
|
||||
context.l10n.highestRating('').replaceAll(':', ''),
|
||||
child: _RatingWidget(data.highestRating,
|
||||
data.highestRatingGame, LichessColors.good)),
|
||||
context.l10n.highestRating('').replaceAll(':', ''),
|
||||
child: _RatingWidget(
|
||||
data.highestRating,
|
||||
data.highestRatingGame,
|
||||
LichessColors.good,
|
||||
),
|
||||
),
|
||||
_CustomPlatformCard(
|
||||
context.l10n.lowestRating('').replaceAll(':', ''),
|
||||
child: _RatingWidget(data.lowestRating,
|
||||
data.lowestRatingGame, LichessColors.red)),
|
||||
context.l10n.lowestRating('').replaceAll(':', ''),
|
||||
child: _RatingWidget(
|
||||
data.lowestRating,
|
||||
data.lowestRatingGame,
|
||||
LichessColors.red,
|
||||
),
|
||||
),
|
||||
]),
|
||||
statGroupSpace,
|
||||
Row(
|
||||
@@ -172,54 +197,91 @@ class PerfStatsScreen extends ConsumerWidget {
|
||||
),
|
||||
subStatSpace,
|
||||
_CustomPlatformCardRow([
|
||||
_CustomPlatformCard(context.l10n.wins,
|
||||
child: _PercentageValueWidget(
|
||||
data.wonGames, data.totalGames,
|
||||
color: LichessColors.good)),
|
||||
_CustomPlatformCard(context.l10n.draws,
|
||||
child: _PercentageValueWidget(
|
||||
data.drawnGames, data.totalGames,
|
||||
color: textShade(context, _customOpacity),
|
||||
isShaded: true)),
|
||||
_CustomPlatformCard(context.l10n.losses,
|
||||
child: _PercentageValueWidget(
|
||||
data.lostGames, data.totalGames,
|
||||
color: LichessColors.red)),
|
||||
]),
|
||||
_CustomPlatformCardRow([
|
||||
_CustomPlatformCard(context.l10n.rated,
|
||||
child: _PercentageValueWidget(
|
||||
data.ratedGames, data.totalGames)),
|
||||
_CustomPlatformCard(context.l10n.tournament,
|
||||
child: _PercentageValueWidget(
|
||||
data.tournamentGames, data.totalGames)),
|
||||
_CustomPlatformCard(
|
||||
context.l10n.berserkedGames
|
||||
.replaceAll(' ${context.l10n.games.toLowerCase()}', ''),
|
||||
child: _PercentageValueWidget(
|
||||
data.berserkGames, data.totalGames)),
|
||||
_CustomPlatformCard(context.l10n.disconnections,
|
||||
child: _PercentageValueWidget(
|
||||
data.disconnections, data.totalGames)),
|
||||
context.l10n.wins,
|
||||
child: _PercentageValueWidget(
|
||||
data.wonGames,
|
||||
data.totalGames,
|
||||
color: LichessColors.good,
|
||||
),
|
||||
),
|
||||
_CustomPlatformCard(
|
||||
context.l10n.draws,
|
||||
child: _PercentageValueWidget(
|
||||
data.drawnGames,
|
||||
data.totalGames,
|
||||
color: textShade(context, _customOpacity),
|
||||
isShaded: true,
|
||||
),
|
||||
),
|
||||
_CustomPlatformCard(
|
||||
context.l10n.losses,
|
||||
child: _PercentageValueWidget(
|
||||
data.lostGames,
|
||||
data.totalGames,
|
||||
color: LichessColors.red,
|
||||
),
|
||||
),
|
||||
]),
|
||||
_CustomPlatformCardRow([
|
||||
_CustomPlatformCard(context.l10n.averageOpponent,
|
||||
value: data.avgOpponent == null
|
||||
? '?'
|
||||
: data.avgOpponent.toString()),
|
||||
_CustomPlatformCard(context.l10n.timeSpentPlaying,
|
||||
value: data.timePlayed
|
||||
.toDaysHoursMinutes(AppLocalizations.of(context))),
|
||||
_CustomPlatformCard(
|
||||
context.l10n.rated,
|
||||
child: _PercentageValueWidget(
|
||||
data.ratedGames,
|
||||
data.totalGames,
|
||||
),
|
||||
),
|
||||
_CustomPlatformCard(
|
||||
context.l10n.tournament,
|
||||
child: _PercentageValueWidget(
|
||||
data.tournamentGames,
|
||||
data.totalGames,
|
||||
),
|
||||
),
|
||||
_CustomPlatformCard(
|
||||
context.l10n.berserkedGames
|
||||
.replaceAll(' ${context.l10n.games.toLowerCase()}', ''),
|
||||
child: _PercentageValueWidget(
|
||||
data.berserkGames,
|
||||
data.totalGames,
|
||||
),
|
||||
),
|
||||
_CustomPlatformCard(
|
||||
context.l10n.disconnections,
|
||||
child: _PercentageValueWidget(
|
||||
data.disconnections,
|
||||
data.totalGames,
|
||||
),
|
||||
),
|
||||
]),
|
||||
_CustomPlatformCardRow([
|
||||
_CustomPlatformCard(
|
||||
context.l10n.averageOpponent,
|
||||
value: data.avgOpponent == null
|
||||
? '?'
|
||||
: data.avgOpponent.toString(),
|
||||
),
|
||||
_CustomPlatformCard(
|
||||
context.l10n.timeSpentPlaying,
|
||||
value: data.timePlayed
|
||||
.toDaysHoursMinutes(AppLocalizations.of(context)),
|
||||
),
|
||||
]),
|
||||
_CustomPlatformCard(
|
||||
context.l10n.winningStreak,
|
||||
child: _StreakWidget(data.maxWinStreak, data.curWinStreak,
|
||||
color: LichessColors.good),
|
||||
child: _StreakWidget(
|
||||
data.maxWinStreak,
|
||||
data.curWinStreak,
|
||||
color: LichessColors.good,
|
||||
),
|
||||
),
|
||||
_CustomPlatformCard(
|
||||
context.l10n.losingStreak,
|
||||
child: _StreakWidget(data.maxLossStreak, data.curLossStreak,
|
||||
color: LichessColors.red),
|
||||
child: _StreakWidget(
|
||||
data.maxLossStreak,
|
||||
data.curLossStreak,
|
||||
color: LichessColors.red,
|
||||
),
|
||||
),
|
||||
_CustomPlatformCard(
|
||||
context.l10n.gamesInARow,
|
||||
@@ -253,7 +315,8 @@ class PerfStatsScreen extends ConsumerWidget {
|
||||
},
|
||||
error: (error, stackTrace) {
|
||||
debugPrint(
|
||||
'SEVERE: [PerfStatsScreen] could not load data; $error\n$stackTrace');
|
||||
'SEVERE: [PerfStatsScreen] could not load data; $error\n$stackTrace',
|
||||
);
|
||||
return const Center(child: Text('Could not load user stats.'));
|
||||
},
|
||||
loading: () => const CenterLoadingIndicator(),
|
||||
@@ -285,13 +348,20 @@ class _CustomPlatformCard extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
FittedBox(
|
||||
alignment: Alignment.center,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(stat,
|
||||
style: defaultStatStyle, textAlign: TextAlign.center)),
|
||||
alignment: Alignment.center,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
stat,
|
||||
style: defaultStatStyle,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
if (value != null)
|
||||
Text(value!,
|
||||
style: defaultValueStyle, textAlign: TextAlign.center)
|
||||
Text(
|
||||
value!,
|
||||
style: defaultValueStyle,
|
||||
textAlign: TextAlign.center,
|
||||
)
|
||||
else if (child != null)
|
||||
child!
|
||||
else
|
||||
@@ -351,24 +421,33 @@ class _ProgressionWidget extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
const progressionFontSize = 20.0;
|
||||
|
||||
return Row(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
if (progress != 0) ...[
|
||||
Icon(
|
||||
progress > 0
|
||||
? LichessIcons.arrow_full_upperright
|
||||
: LichessIcons.arrow_full_lowerright,
|
||||
color: progress > 0 ? LichessColors.good : LichessColors.red,
|
||||
),
|
||||
Text(progress.abs().toString(),
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (progress != 0) ...[
|
||||
Icon(
|
||||
progress > 0
|
||||
? LichessIcons.arrow_full_upperright
|
||||
: LichessIcons.arrow_full_lowerright,
|
||||
color: progress > 0 ? LichessColors.good : LichessColors.red,
|
||||
),
|
||||
Text(
|
||||
progress.abs().toString(),
|
||||
style: TextStyle(
|
||||
color: progress > 0 ? LichessColors.good : LichessColors.red,
|
||||
fontSize: progressionFontSize)),
|
||||
] else
|
||||
Text('0',
|
||||
color: progress > 0 ? LichessColors.good : LichessColors.red,
|
||||
fontSize: progressionFontSize,
|
||||
),
|
||||
),
|
||||
] else
|
||||
Text(
|
||||
'0',
|
||||
style: TextStyle(
|
||||
color: textShade(context, _customOpacity),
|
||||
fontSize: progressionFontSize))
|
||||
]);
|
||||
color: textShade(context, _customOpacity),
|
||||
fontSize: progressionFontSize,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,8 +466,10 @@ class _UserGameWidget extends StatelessWidget {
|
||||
|
||||
return game == null
|
||||
? const Text('?', style: defaultDateStyle)
|
||||
: Text(_dateFormatter.format(game!.finishedAt),
|
||||
style: defaultDateStyle);
|
||||
: Text(
|
||||
_dateFormatter.format(game!.finishedAt),
|
||||
style: defaultDateStyle,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,9 +487,10 @@ class _RatingWidget extends StatelessWidget {
|
||||
: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(rating.toString(),
|
||||
style:
|
||||
TextStyle(fontSize: _defaultValueFontSize, color: color)),
|
||||
Text(
|
||||
rating.toString(),
|
||||
style: TextStyle(fontSize: _defaultValueFontSize, color: color),
|
||||
),
|
||||
_UserGameWidget(game)
|
||||
],
|
||||
);
|
||||
@@ -421,8 +503,12 @@ class _PercentageValueWidget extends StatelessWidget {
|
||||
final Color? color;
|
||||
final bool isShaded;
|
||||
|
||||
const _PercentageValueWidget(this.value, this.denominator,
|
||||
{this.color, this.isShaded = false});
|
||||
const _PercentageValueWidget(
|
||||
this.value,
|
||||
this.denominator, {
|
||||
this.color,
|
||||
this.isShaded = false,
|
||||
});
|
||||
|
||||
String _getPercentageString(num numerator, num denominator) {
|
||||
return '${((numerator / denominator) * 100).round()}%';
|
||||
@@ -430,18 +516,24 @@ class _PercentageValueWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
Text(
|
||||
value.toString(),
|
||||
style: const TextStyle(fontSize: _defaultValueFontSize),
|
||||
),
|
||||
Text(_getPercentageString(value, denominator),
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
value.toString(),
|
||||
style: const TextStyle(fontSize: _defaultValueFontSize),
|
||||
),
|
||||
Text(
|
||||
_getPercentageString(value, denominator),
|
||||
style: TextStyle(
|
||||
fontSize: _defaultValueFontSize,
|
||||
color: isShaded
|
||||
? textShade(context, _customOpacity / 2)
|
||||
: textShade(context, _customOpacity)))
|
||||
]);
|
||||
fontSize: _defaultValueFontSize,
|
||||
color: isShaded
|
||||
? textShade(context, _customOpacity / 2)
|
||||
: textShade(context, _customOpacity),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -457,16 +549,19 @@ class _StreakWidget extends StatelessWidget {
|
||||
const valueStyle = TextStyle(fontSize: _defaultValueFontSize);
|
||||
|
||||
final streakTitleStyle = TextStyle(
|
||||
fontSize: _defaultStatFontSize,
|
||||
color: textShade(context, _customOpacity));
|
||||
fontSize: _defaultStatFontSize,
|
||||
color: textShade(context, _customOpacity),
|
||||
);
|
||||
|
||||
final longestStreakStr = context.l10n.longestStreak('').replaceAll(':', '');
|
||||
final currentStreakStr = context.l10n.currentStreak('').replaceAll(':', '');
|
||||
|
||||
final List<Widget> streakWidgets =
|
||||
[maxStreak, curStreak].mapIndexed((index, streak) {
|
||||
final streakTitle = Text(index == 0 ? longestStreakStr : currentStreakStr,
|
||||
style: streakTitleStyle);
|
||||
final streakTitle = Text(
|
||||
index == 0 ? longestStreakStr : currentStreakStr,
|
||||
style: streakTitleStyle,
|
||||
);
|
||||
|
||||
if (streak == null || streak.isValueEmpty) {
|
||||
return Expanded(
|
||||
@@ -474,40 +569,54 @@ class _StreakWidget extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
streakTitle,
|
||||
Text('-',
|
||||
style: const TextStyle(fontSize: _defaultValueFontSize),
|
||||
semanticsLabel: context.l10n.none)
|
||||
Text(
|
||||
'-',
|
||||
style: const TextStyle(fontSize: _defaultValueFontSize),
|
||||
semanticsLabel: context.l10n.none,
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final Text valueText = streak.map(timeStreak: (UserTimeStreak streak) {
|
||||
return Text(
|
||||
final Text valueText = streak.map(
|
||||
timeStreak: (UserTimeStreak streak) {
|
||||
return Text(
|
||||
streak.timePlayed.toDaysHoursMinutes(AppLocalizations.of(context)),
|
||||
style: valueStyle,
|
||||
textAlign: TextAlign.center);
|
||||
}, gameStreak: (UserGameStreak streak) {
|
||||
return Text(context.l10n.nbGames(streak.gamesPlayed),
|
||||
style: valueStyle, textAlign: TextAlign.center);
|
||||
});
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
},
|
||||
gameStreak: (UserGameStreak streak) {
|
||||
return Text(
|
||||
context.l10n.nbGames(streak.gamesPlayed),
|
||||
style: valueStyle,
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return Expanded(
|
||||
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
streakTitle,
|
||||
valueText,
|
||||
if (streak.startGame != null && streak.endGame != null)
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 5.0),
|
||||
_UserGameWidget(streak.startGame),
|
||||
Icon(Icons.arrow_downward_rounded,
|
||||
color: textShade(context, _customOpacity)),
|
||||
_UserGameWidget(streak.endGame)
|
||||
],
|
||||
)
|
||||
]),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
streakTitle,
|
||||
valueText,
|
||||
if (streak.startGame != null && streak.endGame != null)
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 5.0),
|
||||
_UserGameWidget(streak.startGame),
|
||||
Icon(
|
||||
Icons.arrow_downward_rounded,
|
||||
color: textShade(context, _customOpacity),
|
||||
),
|
||||
_UserGameWidget(streak.endGame)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(growable: false);
|
||||
|
||||
@@ -516,8 +625,9 @@ class _StreakWidget extends StatelessWidget {
|
||||
children: [
|
||||
const SizedBox(height: 5.0),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: streakWidgets)
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: streakWidgets,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -528,8 +638,11 @@ class _GameListWidget extends ConsumerWidget {
|
||||
final Perf perf;
|
||||
final User user;
|
||||
|
||||
const _GameListWidget(
|
||||
{required this.games, required this.perf, required this.user});
|
||||
const _GameListWidget({
|
||||
required this.games,
|
||||
required this.perf,
|
||||
required this.user,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -569,7 +682,9 @@ class _GameListWidget extends ConsumerWidget {
|
||||
leading: Icon(perf.icon),
|
||||
title: ListTileUser(
|
||||
user: LightUser(
|
||||
name: game.opponentName ?? '?', title: game.opponentTitle),
|
||||
name: game.opponentName ?? '?',
|
||||
title: game.opponentTitle,
|
||||
),
|
||||
rating: game.opponentRating,
|
||||
),
|
||||
subtitle: Text(
|
||||
|
||||
@@ -73,26 +73,28 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||
],
|
||||
),
|
||||
body: authState.maybeWhen(
|
||||
data: (account) {
|
||||
return account != null
|
||||
? RefreshIndicator(
|
||||
key: _androidRefreshKey,
|
||||
onRefresh: () => _refreshData(account),
|
||||
child: ListView(
|
||||
padding: kBodyPadding,
|
||||
children: _buildList(context, account),
|
||||
),
|
||||
)
|
||||
: Center(
|
||||
child: FatButton(
|
||||
semanticsLabel: context.l10n.signIn,
|
||||
onPressed: authActionsAsync.isLoading
|
||||
? null
|
||||
: () =>
|
||||
ref.read(authActionsProvider.notifier).signIn(),
|
||||
child: Text(context.l10n.signIn)));
|
||||
},
|
||||
orElse: () => const Center(child: CircularProgressIndicator())),
|
||||
data: (account) {
|
||||
return account != null
|
||||
? RefreshIndicator(
|
||||
key: _androidRefreshKey,
|
||||
onRefresh: () => _refreshData(account),
|
||||
child: ListView(
|
||||
padding: kBodyPadding,
|
||||
children: _buildList(context, account),
|
||||
),
|
||||
)
|
||||
: Center(
|
||||
child: FatButton(
|
||||
semanticsLabel: context.l10n.signIn,
|
||||
onPressed: authActionsAsync.isLoading
|
||||
? null
|
||||
: () => ref.read(authActionsProvider.notifier).signIn(),
|
||||
child: Text(context.l10n.signIn),
|
||||
),
|
||||
);
|
||||
},
|
||||
orElse: () => const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -137,14 +139,15 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||
else
|
||||
SliverFillRemaining(
|
||||
child: Center(
|
||||
child: FatButton(
|
||||
semanticsLabel: context.l10n.signIn,
|
||||
onPressed: authActionsAsync.isLoading
|
||||
? null
|
||||
: () => ref
|
||||
.read(authActionsProvider.notifier)
|
||||
.signIn(),
|
||||
child: Text(context.l10n.signIn))),
|
||||
child: FatButton(
|
||||
semanticsLabel: context.l10n.signIn,
|
||||
onPressed: authActionsAsync.isLoading
|
||||
? null
|
||||
: () =>
|
||||
ref.read(authActionsProvider.notifier).signIn(),
|
||||
child: Text(context.l10n.signIn),
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
},
|
||||
@@ -183,13 +186,18 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||
kEmptyWidget,
|
||||
const SizedBox(height: 5),
|
||||
Text(
|
||||
'${context.l10n.memberSince} ${DateFormat.yMMMMd().format(account.createdAt)}'),
|
||||
'${context.l10n.memberSince} ${DateFormat.yMMMMd().format(account.createdAt)}',
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Text(context.l10n.lastSeenActive(timeago.format(account.seenAt))),
|
||||
const SizedBox(height: 5),
|
||||
if (account.playTime != null)
|
||||
Text(context.l10n.tpTimeSpentPlaying(account.playTime!.total
|
||||
.toDaysHoursMinutes(AppLocalizations.of(context))))
|
||||
Text(
|
||||
context.l10n.tpTimeSpentPlaying(
|
||||
account.playTime!.total
|
||||
.toDaysHoursMinutes(AppLocalizations.of(context)),
|
||||
),
|
||||
)
|
||||
else
|
||||
kEmptyWidget,
|
||||
],
|
||||
@@ -246,11 +254,15 @@ class PerfCards extends StatelessWidget {
|
||||
onTap: isPerfWithoutStats
|
||||
? null
|
||||
: () => pushPlatformRoute(
|
||||
context: context,
|
||||
title: context.l10n
|
||||
.perfStats('${account.username} ${perf.title}'),
|
||||
builder: (context) => PerfStatsScreen(
|
||||
user: account, perf: perf, loggedInUser: account)),
|
||||
context: context,
|
||||
title: context.l10n
|
||||
.perfStats('${account.username} ${perf.title}'),
|
||||
builder: (context) => PerfStatsScreen(
|
||||
user: account,
|
||||
perf: perf,
|
||||
loggedInUser: account,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
@@ -281,12 +293,15 @@ class PerfCards extends StatelessWidget {
|
||||
: LichessColors.red,
|
||||
size: 12,
|
||||
),
|
||||
Text(userPerf.progression.abs().toString(),
|
||||
style: TextStyle(
|
||||
color: userPerf.progression > 0
|
||||
? LichessColors.good
|
||||
: LichessColors.red,
|
||||
fontSize: 12)),
|
||||
Text(
|
||||
userPerf.progression.abs().toString(),
|
||||
style: TextStyle(
|
||||
color: userPerf.progression > 0
|
||||
? LichessColors.good
|
||||
: LichessColors.red,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
@@ -316,52 +331,60 @@ class RecentGames extends ConsumerWidget {
|
||||
data: (data) {
|
||||
return Column(
|
||||
children: ListTile.divideTiles(
|
||||
color: dividerColor(context),
|
||||
context: context,
|
||||
tiles: data.map((game) {
|
||||
final mySide =
|
||||
game.white.id == account.id ? Side.white : Side.black;
|
||||
final opponent =
|
||||
game.white.id == account.id ? game.black : game.white;
|
||||
final opponentName = opponent.name == 'Stockfish'
|
||||
? context.l10n.aiNameLevelAiLevel(
|
||||
opponent.name, opponent.aiLevel.toString())
|
||||
: opponent.name;
|
||||
color: dividerColor(context),
|
||||
context: context,
|
||||
tiles: data.map((game) {
|
||||
final mySide =
|
||||
game.white.id == account.id ? Side.white : Side.black;
|
||||
final opponent =
|
||||
game.white.id == account.id ? game.black : game.white;
|
||||
final opponentName = opponent.name == 'Stockfish'
|
||||
? context.l10n.aiNameLevelAiLevel(
|
||||
opponent.name,
|
||||
opponent.aiLevel.toString(),
|
||||
)
|
||||
: opponent.name;
|
||||
|
||||
return ListTile(
|
||||
onTap: () {
|
||||
Navigator.of(context, rootNavigator: true).push<void>(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ArchivedGameScreen(
|
||||
gameData: game,
|
||||
orientation: account.id == game.white.id
|
||||
? Side.white
|
||||
: Side.black,
|
||||
),
|
||||
return ListTile(
|
||||
onTap: () {
|
||||
Navigator.of(context, rootNavigator: true).push<void>(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ArchivedGameScreen(
|
||||
gameData: game,
|
||||
orientation: account.id == game.white.id
|
||||
? Side.white
|
||||
: Side.black,
|
||||
),
|
||||
);
|
||||
},
|
||||
leading: Icon(game.perf.icon),
|
||||
title: ListTileUser(
|
||||
user: LightUser(name: opponentName, title: opponent.title),
|
||||
rating: opponent.rating,
|
||||
),
|
||||
subtitle: Text(
|
||||
timeago.format(game.lastMoveAt),
|
||||
style: TextStyle(color: textShade(context, 0.7)),
|
||||
),
|
||||
trailing: game.winner == mySide
|
||||
? const Icon(CupertinoIcons.plus_square_fill,
|
||||
color: LichessColors.good)
|
||||
: const Icon(CupertinoIcons.minus_square_fill,
|
||||
color: LichessColors.red),
|
||||
);
|
||||
})).toList(growable: false),
|
||||
),
|
||||
);
|
||||
},
|
||||
leading: Icon(game.perf.icon),
|
||||
title: ListTileUser(
|
||||
user: LightUser(name: opponentName, title: opponent.title),
|
||||
rating: opponent.rating,
|
||||
),
|
||||
subtitle: Text(
|
||||
timeago.format(game.lastMoveAt),
|
||||
style: TextStyle(color: textShade(context, 0.7)),
|
||||
),
|
||||
trailing: game.winner == mySide
|
||||
? const Icon(
|
||||
CupertinoIcons.plus_square_fill,
|
||||
color: LichessColors.good,
|
||||
)
|
||||
: const Icon(
|
||||
CupertinoIcons.minus_square_fill,
|
||||
color: LichessColors.red,
|
||||
),
|
||||
);
|
||||
}),
|
||||
).toList(growable: false),
|
||||
);
|
||||
},
|
||||
error: (error, stackTrace) {
|
||||
debugPrint(
|
||||
'SEVERE: [ProfileScreen] could not load user games; $error\n$stackTrace');
|
||||
'SEVERE: [ProfileScreen] could not load user games; $error\n$stackTrace',
|
||||
);
|
||||
return const Text('Could not load games.');
|
||||
},
|
||||
loading: () => const CenterLoadingIndicator(),
|
||||
@@ -376,17 +399,19 @@ class Location extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(children: [
|
||||
if (profile.country != null)
|
||||
CachedNetworkImage(
|
||||
imageUrl: lichessFlagSrc(profile.country!),
|
||||
errorWidget: (_, __, ___) => kEmptyWidget,
|
||||
)
|
||||
else
|
||||
kEmptyWidget,
|
||||
const SizedBox(width: 10),
|
||||
Text(profile.location ?? ''),
|
||||
]);
|
||||
return Row(
|
||||
children: [
|
||||
if (profile.country != null)
|
||||
CachedNetworkImage(
|
||||
imageUrl: lichessFlagSrc(profile.country!),
|
||||
errorWidget: (_, __, ___) => kEmptyWidget,
|
||||
)
|
||||
else
|
||||
kEmptyWidget,
|
||||
const SizedBox(width: 10),
|
||||
Text(profile.location ?? ''),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,9 +14,10 @@ extension ChessgroundMoveCompat on Move {
|
||||
}
|
||||
|
||||
return chessground.Move(
|
||||
from: toAlgebraic((this as NormalMove).from),
|
||||
to: toAlgebraic(to),
|
||||
promotion: (this as NormalMove).promotion?.cg);
|
||||
from: toAlgebraic((this as NormalMove).from),
|
||||
to: toAlgebraic(to),
|
||||
promotion: (this as NormalMove).promotion?.cg,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+26
-13
@@ -21,11 +21,13 @@ extension Dartchess on Pick {
|
||||
return move;
|
||||
} else {
|
||||
throw PickException(
|
||||
"value $value at $debugParsingExit can't be casted to Move: invalid UCI string.");
|
||||
"value $value at $debugParsingExit can't be casted to Move: invalid UCI string.",
|
||||
);
|
||||
}
|
||||
}
|
||||
throw PickException(
|
||||
"value $value at $debugParsingExit can't be casted to Move");
|
||||
"value $value at $debugParsingExit can't be casted to Move",
|
||||
);
|
||||
}
|
||||
|
||||
Move? asUciMoveOrNull() {
|
||||
@@ -46,7 +48,8 @@ extension Dartchess on Pick {
|
||||
return value == 'white' ? Side.white : Side.black;
|
||||
}
|
||||
throw PickException(
|
||||
"value $value at $debugParsingExit can't be casted to Side");
|
||||
"value $value at $debugParsingExit can't be casted to Side",
|
||||
);
|
||||
}
|
||||
|
||||
Side? asSideOrNull() {
|
||||
@@ -70,7 +73,8 @@ extension GameExtension on Pick {
|
||||
.firstWhere((v) => v.name == value, orElse: () => Speed.blitz);
|
||||
}
|
||||
throw PickException(
|
||||
"value $value at $debugParsingExit can't be casted to Speed");
|
||||
"value $value at $debugParsingExit can't be casted to Speed",
|
||||
);
|
||||
}
|
||||
|
||||
Speed? asSpeedOrNull() {
|
||||
@@ -92,7 +96,8 @@ extension GameExtension on Pick {
|
||||
.firstWhere((v) => v.title == value, orElse: () => Perf.blitz);
|
||||
}
|
||||
throw PickException(
|
||||
"value $value at $debugParsingExit can't be casted to Perf");
|
||||
"value $value at $debugParsingExit can't be casted to Perf",
|
||||
);
|
||||
}
|
||||
|
||||
Perf? asPerfOrNull() {
|
||||
@@ -114,7 +119,8 @@ extension GameExtension on Pick {
|
||||
.firstWhere((e) => e.name == value, orElse: () => GameStatus.unknown);
|
||||
}
|
||||
throw PickException(
|
||||
"value $value at $debugParsingExit can't be casted to GameStatus");
|
||||
"value $value at $debugParsingExit can't be casted to GameStatus",
|
||||
);
|
||||
}
|
||||
|
||||
GameStatus? asGameStatusOrNull() {
|
||||
@@ -136,7 +142,8 @@ extension GameExtension on Pick {
|
||||
.firstWhere((e) => e.name == value, orElse: () => Variant.standard);
|
||||
}
|
||||
throw PickException(
|
||||
"value $value at $debugParsingExit can't be casted to GameStatus");
|
||||
"value $value at $debugParsingExit can't be casted to GameStatus",
|
||||
);
|
||||
}
|
||||
|
||||
Variant? asVariantOrNull() {
|
||||
@@ -160,7 +167,8 @@ extension TimeExtension on Pick {
|
||||
return DateTime.fromMillisecondsSinceEpoch(value);
|
||||
}
|
||||
throw PickException(
|
||||
"value $value at $debugParsingExit can't be casted to DateTime");
|
||||
"value $value at $debugParsingExit can't be casted to DateTime",
|
||||
);
|
||||
}
|
||||
|
||||
/// Matches a DateTime from milliseconds since unix epoch.
|
||||
@@ -183,7 +191,8 @@ extension TimeExtension on Pick {
|
||||
return Duration(seconds: value);
|
||||
}
|
||||
throw PickException(
|
||||
"value $value at $debugParsingExit can't be casted to Duration");
|
||||
"value $value at $debugParsingExit can't be casted to Duration",
|
||||
);
|
||||
}
|
||||
|
||||
/// Matches a DateTime from milliseconds since unix epoch.
|
||||
@@ -207,7 +216,8 @@ extension ModelsPick on Pick {
|
||||
return UserId(value);
|
||||
}
|
||||
throw PickException(
|
||||
"value $value at $debugParsingExit can't be casted to UserId");
|
||||
"value $value at $debugParsingExit can't be casted to UserId",
|
||||
);
|
||||
}
|
||||
|
||||
UserId? asUserIdOrNull() {
|
||||
@@ -228,7 +238,8 @@ extension ModelsPick on Pick {
|
||||
return GameId(value);
|
||||
}
|
||||
throw PickException(
|
||||
"value $value at $debugParsingExit can't be casted to GameId");
|
||||
"value $value at $debugParsingExit can't be casted to GameId",
|
||||
);
|
||||
}
|
||||
|
||||
GameId? asGameIdOrNull() {
|
||||
@@ -249,7 +260,8 @@ extension ModelsPick on Pick {
|
||||
return GameFullId(value);
|
||||
}
|
||||
throw PickException(
|
||||
"value $value at $debugParsingExit can't be casted to GameId");
|
||||
"value $value at $debugParsingExit can't be casted to GameId",
|
||||
);
|
||||
}
|
||||
|
||||
GameFullId? asGameFullIdOrNull() {
|
||||
@@ -270,7 +282,8 @@ extension ModelsPick on Pick {
|
||||
return PuzzleId(value);
|
||||
}
|
||||
throw PickException(
|
||||
"value $value at $debugParsingExit can't be casted to PuzzleId");
|
||||
"value $value at $debugParsingExit can't be casted to PuzzleId",
|
||||
);
|
||||
}
|
||||
|
||||
PuzzleId? asPuzzleIdOrNull() {
|
||||
|
||||
@@ -10,9 +10,11 @@ void pushPlatformRoute({
|
||||
required WidgetBuilder builder,
|
||||
String? title,
|
||||
}) {
|
||||
Navigator.of(context).push<void>(defaultTargetPlatform == TargetPlatform.iOS
|
||||
? CupertinoPageRoute(builder: builder, title: title)
|
||||
: MaterialPageRoute(
|
||||
builder: builder,
|
||||
));
|
||||
Navigator.of(context).push<void>(
|
||||
defaultTargetPlatform == TargetPlatform.iOS
|
||||
? CupertinoPageRoute(builder: builder, title: title)
|
||||
: MaterialPageRoute(
|
||||
builder: builder,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -48,15 +48,17 @@ Future<T?> _showCupertinoBottomSheet<T>(
|
||||
return CupertinoActionSheet(
|
||||
title: title,
|
||||
actions: actions
|
||||
.map((action) => CupertinoActionSheetAction(
|
||||
onPressed: () {
|
||||
action.onPressed(context);
|
||||
if (action.dismissOnPress) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
child: action.label,
|
||||
))
|
||||
.map(
|
||||
(action) => CupertinoActionSheetAction(
|
||||
onPressed: () {
|
||||
action.onPressed(context);
|
||||
if (action.dismissOnPress) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
child: action.label,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
isDefaultAction: true,
|
||||
|
||||
@@ -178,14 +178,17 @@ class _TabSwitchingViewState extends State<_TabSwitchingView> {
|
||||
if (tabFocusNodes.length > BottomTab.values.length) {
|
||||
discardedNodes.addAll(tabFocusNodes.sublist(BottomTab.values.length));
|
||||
tabFocusNodes.removeRange(
|
||||
BottomTab.values.length, tabFocusNodes.length);
|
||||
BottomTab.values.length,
|
||||
tabFocusNodes.length,
|
||||
);
|
||||
} else {
|
||||
tabFocusNodes.addAll(
|
||||
List<FocusScopeNode>.generate(
|
||||
BottomTab.values.length - tabFocusNodes.length,
|
||||
(int index) => FocusScopeNode(
|
||||
debugLabel:
|
||||
'$BottomNavScaffold Tab ${index + tabFocusNodes.length}'),
|
||||
debugLabel:
|
||||
'$BottomNavScaffold Tab ${index + tabFocusNodes.length}',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -221,11 +224,13 @@ class _TabSwitchingViewState extends State<_TabSwitchingView> {
|
||||
enabled: active,
|
||||
child: FocusScope(
|
||||
node: tabFocusNodes[index],
|
||||
child: Builder(builder: (BuildContext context) {
|
||||
return shouldBuildTab[index]
|
||||
? widget.tabBuilder(context, index)
|
||||
: Container();
|
||||
}),
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
return shouldBuildTab[index]
|
||||
? widget.tabBuilder(context, index)
|
||||
: Container();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -6,11 +6,12 @@ import 'package:flutter/material.dart';
|
||||
///
|
||||
/// Will use an [ElevatedButton] on Android and a [CupertinoButton.filled] on iOS.
|
||||
class FatButton extends StatelessWidget {
|
||||
const FatButton(
|
||||
{required this.semanticsLabel,
|
||||
required this.child,
|
||||
required this.onPressed,
|
||||
super.key});
|
||||
const FatButton({
|
||||
required this.semanticsLabel,
|
||||
required this.child,
|
||||
required this.onPressed,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String semanticsLabel;
|
||||
final VoidCallback? onPressed;
|
||||
@@ -29,7 +30,8 @@ class FatButton extends StatelessWidget {
|
||||
: ElevatedButton(
|
||||
onPressed: onPressed,
|
||||
style: ElevatedButton.styleFrom(
|
||||
textStyle: const TextStyle(fontSize: 18)),
|
||||
textStyle: const TextStyle(fontSize: 18),
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
@@ -38,12 +40,13 @@ class FatButton extends StatelessWidget {
|
||||
|
||||
/// Platform agnostic button meant for medium importance actions.
|
||||
class SecondaryButton extends StatelessWidget {
|
||||
const SecondaryButton(
|
||||
{required this.semanticsLabel,
|
||||
required this.child,
|
||||
required this.onPressed,
|
||||
this.textStyle,
|
||||
super.key});
|
||||
const SecondaryButton({
|
||||
required this.semanticsLabel,
|
||||
required this.child,
|
||||
required this.onPressed,
|
||||
this.textStyle,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String semanticsLabel;
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
@@ -15,8 +15,11 @@ class CountdownClock extends ConsumerStatefulWidget {
|
||||
final Duration duration;
|
||||
final bool active;
|
||||
|
||||
const CountdownClock(
|
||||
{required this.duration, required this.active, super.key});
|
||||
const CountdownClock({
|
||||
required this.duration,
|
||||
required this.active,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<CountdownClock> createState() => _CountdownClockState();
|
||||
@@ -118,16 +121,18 @@ class ClockStyle {
|
||||
});
|
||||
|
||||
static const darkThemeStyle = ClockStyle(
|
||||
textColor: Colors.grey,
|
||||
activeTextColor: Colors.black,
|
||||
backgroundColor: Colors.black,
|
||||
activeBackgroundColor: Colors.white);
|
||||
textColor: Colors.grey,
|
||||
activeTextColor: Colors.black,
|
||||
backgroundColor: Colors.black,
|
||||
activeBackgroundColor: Colors.white,
|
||||
);
|
||||
|
||||
static const lightThemeStyle = ClockStyle(
|
||||
textColor: Colors.grey,
|
||||
activeTextColor: Colors.black,
|
||||
backgroundColor: Colors.white,
|
||||
activeBackgroundColor: Color(0xFFD0E0BD));
|
||||
textColor: Colors.grey,
|
||||
activeTextColor: Colors.black,
|
||||
backgroundColor: Colors.white,
|
||||
activeBackgroundColor: Color(0xFFD0E0BD),
|
||||
);
|
||||
|
||||
final Color textColor;
|
||||
final Color activeTextColor;
|
||||
|
||||
@@ -9,11 +9,12 @@ class ButtonLoadingIndicator extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2,
|
||||
));
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,8 +31,9 @@ class CenterLoadingIndicator extends StatelessWidget {
|
||||
}
|
||||
|
||||
ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showAdaptiveSnackBar(
|
||||
BuildContext context,
|
||||
{required Widget content}) {
|
||||
BuildContext context, {
|
||||
required Widget content,
|
||||
}) {
|
||||
return ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: content,
|
||||
|
||||
@@ -23,8 +23,10 @@ class GameBoardLayout extends StatelessWidget {
|
||||
this.currentMoveIndex,
|
||||
this.onSelectMove,
|
||||
super.key,
|
||||
}) : assert(moves == null || currentMoveIndex != null,
|
||||
'You must provide `currentMoveIndex` along with `moves`');
|
||||
}) : assert(
|
||||
moves == null || currentMoveIndex != null,
|
||||
'You must provide `currentMoveIndex` along with `moves`',
|
||||
);
|
||||
|
||||
final BoardData boardData;
|
||||
|
||||
@@ -44,68 +46,70 @@ class GameBoardLayout extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
final aspectRatio = constraints.biggest.aspectRatio;
|
||||
final defaultBoardSize = constraints.biggest.shortestSide;
|
||||
final double boardSize = aspectRatio < 1 && aspectRatio >= 0.84 ||
|
||||
aspectRatio > 1 && aspectRatio <= 1.18
|
||||
? defaultBoardSize * 0.94
|
||||
: defaultBoardSize;
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final aspectRatio = constraints.biggest.aspectRatio;
|
||||
final defaultBoardSize = constraints.biggest.shortestSide;
|
||||
final double boardSize = aspectRatio < 1 && aspectRatio >= 0.84 ||
|
||||
aspectRatio > 1 && aspectRatio <= 1.18
|
||||
? defaultBoardSize * 0.94
|
||||
: defaultBoardSize;
|
||||
|
||||
final board = boardSettings != null
|
||||
? Board(size: boardSize, data: boardData, settings: boardSettings!)
|
||||
: Board(size: boardSize, data: boardData);
|
||||
final List<List<MapEntry<int, String>>>? slicedMoves =
|
||||
moves?.asMap().entries.slices(2).toList(growable: false);
|
||||
return aspectRatio > 1
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
board,
|
||||
Flexible(
|
||||
fit: FlexFit.loose,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
topPlayer,
|
||||
if (slicedMoves != null)
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: MoveList(
|
||||
type: MoveListType.stacked,
|
||||
slicedMoves: slicedMoves,
|
||||
currentMoveIndex: currentMoveIndex ?? 0,
|
||||
onSelectMove: onSelectMove,
|
||||
final board = boardSettings != null
|
||||
? Board(size: boardSize, data: boardData, settings: boardSettings!)
|
||||
: Board(size: boardSize, data: boardData);
|
||||
final List<List<MapEntry<int, String>>>? slicedMoves =
|
||||
moves?.asMap().entries.slices(2).toList(growable: false);
|
||||
return aspectRatio > 1
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
board,
|
||||
Flexible(
|
||||
fit: FlexFit.loose,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
topPlayer,
|
||||
if (slicedMoves != null)
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: MoveList(
|
||||
type: MoveListType.stacked,
|
||||
slicedMoves: slicedMoves,
|
||||
currentMoveIndex: currentMoveIndex ?? 0,
|
||||
onSelectMove: onSelectMove,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomPlayer,
|
||||
],
|
||||
bottomPlayer,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (slicedMoves != null)
|
||||
MoveList(
|
||||
type: MoveListType.inline,
|
||||
slicedMoves: slicedMoves,
|
||||
currentMoveIndex: currentMoveIndex ?? 0,
|
||||
onSelectMove: onSelectMove,
|
||||
),
|
||||
topPlayer,
|
||||
board,
|
||||
bottomPlayer,
|
||||
],
|
||||
);
|
||||
});
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (slicedMoves != null)
|
||||
MoveList(
|
||||
type: MoveListType.inline,
|
||||
slicedMoves: slicedMoves,
|
||||
currentMoveIndex: currentMoveIndex ?? 0,
|
||||
onSelectMove: onSelectMove,
|
||||
),
|
||||
topPlayer,
|
||||
board,
|
||||
bottomPlayer,
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,12 +289,15 @@ class InlineMoveItem extends StatelessWidget {
|
||||
color: current == true
|
||||
? defaultTargetPlatform == TargetPlatform.iOS
|
||||
? CupertinoDynamicColor.resolve(
|
||||
CupertinoColors.secondarySystemBackground, context)
|
||||
CupertinoColors.secondarySystemBackground,
|
||||
context,
|
||||
)
|
||||
: null
|
||||
// TODO add bg color on android
|
||||
: null,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(4.0))),
|
||||
borderRadius: BorderRadius.all(Radius.circular(4.0)),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
move.value,
|
||||
|
||||
@@ -27,21 +27,22 @@ class ListTileChoice<T extends Enum> extends StatelessWidget {
|
||||
return PlatformCard(
|
||||
child: Column(
|
||||
children: ListTile.divideTiles(
|
||||
color: dividerColor(context),
|
||||
context: context,
|
||||
tiles: choices.map((value) {
|
||||
return ListTile(
|
||||
selected: selectedItem == value,
|
||||
trailing: selectedItem == value
|
||||
? defaultTargetPlatform == TargetPlatform.iOS
|
||||
? const Icon(CupertinoIcons.check_mark_circled_solid)
|
||||
: const Icon(Icons.check)
|
||||
: null,
|
||||
title: titleBuilder(value),
|
||||
subtitle: subtitleBuilder?.call(value),
|
||||
onTap: () => onSelectedItemChanged(value),
|
||||
);
|
||||
})).toList(growable: false),
|
||||
color: dividerColor(context),
|
||||
context: context,
|
||||
tiles: choices.map((value) {
|
||||
return ListTile(
|
||||
selected: selectedItem == value,
|
||||
trailing: selectedItem == value
|
||||
? defaultTargetPlatform == TargetPlatform.iOS
|
||||
? const Icon(CupertinoIcons.check_mark_circled_solid)
|
||||
: const Icon(Icons.check)
|
||||
: null,
|
||||
title: titleBuilder(value),
|
||||
subtitle: subtitleBuilder?.call(value),
|
||||
onTap: () => onSelectedItemChanged(value),
|
||||
);
|
||||
}),
|
||||
).toList(growable: false),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,9 @@ class PlatformWidget extends StatelessWidget {
|
||||
}
|
||||
|
||||
typedef ConsumerWidgetBuilder = Widget Function(
|
||||
BuildContext context, WidgetRef ref);
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
);
|
||||
|
||||
/// A widget that builds different things on different platforms with riverpod.
|
||||
class ConsumerPlatformWidget extends StatelessWidget {
|
||||
@@ -96,7 +98,9 @@ class PlatformCard extends StatelessWidget {
|
||||
margin: margin ?? EdgeInsets.zero,
|
||||
elevation: 0,
|
||||
color: CupertinoDynamicColor.resolve(
|
||||
CupertinoColors.secondarySystemBackground, context),
|
||||
CupertinoColors.secondarySystemBackground,
|
||||
context,
|
||||
),
|
||||
shape: kPlatformCardBorder,
|
||||
semanticContainer: semanticContainer,
|
||||
child: child,
|
||||
|
||||
+34
-26
@@ -29,32 +29,40 @@ class BoardPlayer extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 20),
|
||||
child: Row(mainAxisAlignment: MainAxisAlignment.start, children: [
|
||||
if (title != null) ...[
|
||||
Text(
|
||||
title!,
|
||||
style: const TextStyle(fontSize: 18, color: Colors.orange),
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
],
|
||||
Flexible(
|
||||
child: Text(
|
||||
name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style:
|
||||
const TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
|
||||
)),
|
||||
const SizedBox(width: 3),
|
||||
if (rating != null)
|
||||
Text(
|
||||
rating.toString(),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
]),
|
||||
)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 20),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
if (title != null) ...[
|
||||
Text(
|
||||
title!,
|
||||
style:
|
||||
const TextStyle(fontSize: 18, color: Colors.orange),
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
],
|
||||
Flexible(
|
||||
child: Text(
|
||||
name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 3),
|
||||
if (rating != null)
|
||||
Text(
|
||||
rating.toString(),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (clock != null)
|
||||
CountdownClock(
|
||||
duration: clock!,
|
||||
|
||||
@@ -47,12 +47,15 @@ class ListTileUser extends StatelessWidget {
|
||||
return Row(
|
||||
children: [
|
||||
if (user.title != null) ...[
|
||||
Text(user.title!,
|
||||
style: TextStyle(
|
||||
color: user.title == 'BOT'
|
||||
? LichessColors.fancy
|
||||
: LichessColors.brag,
|
||||
fontWeight: FontWeight.bold)),
|
||||
Text(
|
||||
user.title!,
|
||||
style: TextStyle(
|
||||
color: user.title == 'BOT'
|
||||
? LichessColors.fancy
|
||||
: LichessColors.brag,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 5)
|
||||
],
|
||||
Flexible(
|
||||
|
||||
+65
-45
@@ -25,14 +25,20 @@ void main() {
|
||||
final apiClient = ApiClient(mockLogger, FakeClient());
|
||||
|
||||
for (final method in [apiClient.get, apiClient.post, apiClient.delete]) {
|
||||
expect(await method.call(Uri.parse('http://api.test/will/return/200')),
|
||||
isA<ValueResult<http.Response>>());
|
||||
expect(
|
||||
await method.call(Uri.parse('http://api.test/will/return/200')),
|
||||
isA<ValueResult<http.Response>>(),
|
||||
);
|
||||
|
||||
expect(await method.call(Uri.parse('http://api.test/will/return/301')),
|
||||
isA<ValueResult<http.Response>>());
|
||||
expect(
|
||||
await method.call(Uri.parse('http://api.test/will/return/301')),
|
||||
isA<ValueResult<http.Response>>(),
|
||||
);
|
||||
|
||||
expect(await method.call(Uri.parse('http://api.test/will/return/204')),
|
||||
isA<ValueResult<http.Response>>());
|
||||
expect(
|
||||
await method.call(Uri.parse('http://api.test/will/return/204')),
|
||||
isA<ValueResult<http.Response>>(),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -41,34 +47,39 @@ void main() {
|
||||
|
||||
for (final method in [apiClient.get, apiClient.post, apiClient.delete]) {
|
||||
expect(
|
||||
await method
|
||||
.call(Uri.parse('http://api.test/will/return/401'))
|
||||
.then((r) => r.asError?.error),
|
||||
isA<UnauthorizedException>());
|
||||
await method
|
||||
.call(Uri.parse('http://api.test/will/return/401'))
|
||||
.then((r) => r.asError?.error),
|
||||
isA<UnauthorizedException>(),
|
||||
);
|
||||
|
||||
expect(
|
||||
await method
|
||||
.call(Uri.parse('http://api.test/will/return/403'))
|
||||
.then((r) => r.asError?.error),
|
||||
isA<ForbiddenException>());
|
||||
await method
|
||||
.call(Uri.parse('http://api.test/will/return/403'))
|
||||
.then((r) => r.asError?.error),
|
||||
isA<ForbiddenException>(),
|
||||
);
|
||||
|
||||
expect(
|
||||
await method
|
||||
.call(Uri.parse('http://api.test/will/return/404'))
|
||||
.then((r) => r.asError?.error),
|
||||
isA<NotFoundException>());
|
||||
await method
|
||||
.call(Uri.parse('http://api.test/will/return/404'))
|
||||
.then((r) => r.asError?.error),
|
||||
isA<NotFoundException>(),
|
||||
);
|
||||
|
||||
expect(
|
||||
await method
|
||||
.call(Uri.parse('http://api.test/will/return/500'))
|
||||
.then((r) => r.asError?.error),
|
||||
isA<ApiRequestException>());
|
||||
await method
|
||||
.call(Uri.parse('http://api.test/will/return/500'))
|
||||
.then((r) => r.asError?.error),
|
||||
isA<ApiRequestException>(),
|
||||
);
|
||||
|
||||
expect(
|
||||
await method
|
||||
.call(Uri.parse('http://api.test/will/return/503'))
|
||||
.then((r) => r.asError?.error),
|
||||
isA<ApiRequestException>());
|
||||
await method
|
||||
.call(Uri.parse('http://api.test/will/return/503'))
|
||||
.then((r) => r.asError?.error),
|
||||
isA<ApiRequestException>(),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -107,49 +118,58 @@ void main() {
|
||||
final apiClient = ApiClient(mockLogger, FakeClient());
|
||||
|
||||
expect(
|
||||
await apiClient.stream(Uri.parse('http://api.test/will/return/200')),
|
||||
isA<http.StreamedResponse>());
|
||||
await apiClient.stream(Uri.parse('http://api.test/will/return/200')),
|
||||
isA<http.StreamedResponse>(),
|
||||
);
|
||||
|
||||
expect(
|
||||
await apiClient.stream(Uri.parse('http://api.test/will/return/301')),
|
||||
isA<http.StreamedResponse>());
|
||||
await apiClient.stream(Uri.parse('http://api.test/will/return/301')),
|
||||
isA<http.StreamedResponse>(),
|
||||
);
|
||||
|
||||
expect(
|
||||
await apiClient.stream(Uri.parse('http://api.test/will/return/204')),
|
||||
isA<http.StreamedResponse>());
|
||||
await apiClient.stream(Uri.parse('http://api.test/will/return/204')),
|
||||
isA<http.StreamedResponse>(),
|
||||
);
|
||||
});
|
||||
|
||||
test('throws on error', () {
|
||||
final apiClient = ApiClient(mockLogger, FakeClient());
|
||||
|
||||
expect(
|
||||
() => apiClient.stream(Uri.parse('http://api.test/will/return/401')),
|
||||
throwsA(isA<UnauthorizedException>()));
|
||||
() => apiClient.stream(Uri.parse('http://api.test/will/return/401')),
|
||||
throwsA(isA<UnauthorizedException>()),
|
||||
);
|
||||
|
||||
expect(
|
||||
() => apiClient.stream(Uri.parse('http://api.test/will/return/403')),
|
||||
throwsA(isA<ForbiddenException>()));
|
||||
() => apiClient.stream(Uri.parse('http://api.test/will/return/403')),
|
||||
throwsA(isA<ForbiddenException>()),
|
||||
);
|
||||
|
||||
expect(
|
||||
() => apiClient.stream(Uri.parse('http://api.test/will/return/404')),
|
||||
throwsA(isA<NotFoundException>()));
|
||||
() => apiClient.stream(Uri.parse('http://api.test/will/return/404')),
|
||||
throwsA(isA<NotFoundException>()),
|
||||
);
|
||||
|
||||
expect(
|
||||
() => apiClient.stream(Uri.parse('http://api.test/will/return/500')),
|
||||
throwsA(isA<ApiRequestException>()));
|
||||
() => apiClient.stream(Uri.parse('http://api.test/will/return/500')),
|
||||
throwsA(isA<ApiRequestException>()),
|
||||
);
|
||||
|
||||
expect(
|
||||
() => apiClient.stream(Uri.parse('http://api.test/will/return/503')),
|
||||
throwsA(isA<ApiRequestException>()));
|
||||
() => apiClient.stream(Uri.parse('http://api.test/will/return/503')),
|
||||
throwsA(isA<ApiRequestException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('socket error is a GenericIOException', () {
|
||||
final apiClient = ApiClient(mockLogger, FakeClient());
|
||||
|
||||
expect(
|
||||
() => apiClient
|
||||
.stream(Uri.parse('http://api.test/will/throw/socket/exception')),
|
||||
throwsA(isA<GenericIOException>()));
|
||||
() => apiClient
|
||||
.stream(Uri.parse('http://api.test/will/throw/socket/exception')),
|
||||
throwsA(isA<GenericIOException>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -79,4 +79,8 @@ final fakeUser = User(
|
||||
);
|
||||
|
||||
const _fakePerf = UserPerf(
|
||||
rating: 1500, ratingDeviation: 0, progression: 0, numberOfGames: 0);
|
||||
rating: 1500,
|
||||
ratingDeviation: 0,
|
||||
progression: 0,
|
||||
numberOfGames: 0,
|
||||
);
|
||||
|
||||
@@ -9,41 +9,47 @@ import '../data/fake_auth_repository.dart';
|
||||
import '../../../utils.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Auth widget sign in and sign out', (WidgetTester tester) async {
|
||||
final app = await buildTestApp(
|
||||
tester,
|
||||
home: Consumer(builder: (context, ref, _) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
actions: const [
|
||||
SignInWidget(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
authRepositoryProvider.overrideWithValue(FakeAuthRepository(null)),
|
||||
selectedBrigthnessProvider.overrideWithValue(Brightness.dark),
|
||||
],
|
||||
child: app,
|
||||
),
|
||||
);
|
||||
testWidgets(
|
||||
'Auth widget sign in and sign out',
|
||||
(WidgetTester tester) async {
|
||||
final app = await buildTestApp(
|
||||
tester,
|
||||
home: Consumer(
|
||||
builder: (context, ref, _) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
actions: const [
|
||||
SignInWidget(),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
authRepositoryProvider.overrideWithValue(FakeAuthRepository(null)),
|
||||
selectedBrigthnessProvider.overrideWithValue(Brightness.dark),
|
||||
],
|
||||
child: app,
|
||||
),
|
||||
);
|
||||
|
||||
// first frame is a loading state
|
||||
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
||||
await tester.pump();
|
||||
// first frame is a loading state
|
||||
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Sign in'), findsOneWidget);
|
||||
expect(find.text('Sign in'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Sign in'));
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('Sign in'));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
||||
await tester.pump(const Duration(seconds: 1)); // wait for sign in future
|
||||
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
||||
await tester.pump(const Duration(seconds: 1)); // wait for sign in future
|
||||
|
||||
expect(find.text('Sign in'), findsNothing);
|
||||
}, variant: kPlatformVariant);
|
||||
expect(find.text('Sign in'), findsNothing);
|
||||
},
|
||||
variant: kPlatformVariant,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,12 +37,14 @@ void main() {
|
||||
{"id":"9WLmxmiB","rated":true,"variant":"standard","speed":"blitz","perf":"blitz","createdAt":1673553299064,"lastMoveAt":1673553615438,"status":"resign","players":{"white":{"user":{"name":"Dr-Alaakour","id":"dr-alaakour"},"rating":1806,"ratingDiff":5},"black":{"user":{"name":"Thibault","patron":true,"id":"thibault"},"rating":1772,"ratingDiff":-5}},"winner":"white","clock":{"initial":180,"increment":0,"totalTime":180},"lastFen":"2b1Q1k1/p1r4p/1p2p1p1/3pN3/2qP4/P4R2/1P3PPP/4R1K1 b - - 0 1"}
|
||||
''';
|
||||
|
||||
when(() => mockApiClient.get(
|
||||
Uri.parse(
|
||||
'$kLichessHost/api/games/user/testUser?max=10&moves=false&lastFen=true'),
|
||||
headers: {'Accept': 'application/x-ndjson'},
|
||||
))
|
||||
.thenAnswer((_) async => Result.value(http.Response(response, 200)));
|
||||
when(
|
||||
() => mockApiClient.get(
|
||||
Uri.parse(
|
||||
'$kLichessHost/api/games/user/testUser?max=10&moves=false&lastFen=true',
|
||||
),
|
||||
headers: {'Accept': 'application/x-ndjson'},
|
||||
),
|
||||
).thenAnswer((_) async => Result.value(http.Response(response, 200)));
|
||||
|
||||
final result = await repo.getUserGames(const UserId('testUser'));
|
||||
|
||||
@@ -52,10 +54,11 @@ void main() {
|
||||
|
||||
group('GameRepository.events', () {
|
||||
test('can read all supported types of events', () async {
|
||||
when(() =>
|
||||
mockApiClient.stream(Uri.parse('$kLichessHost/api/stream/event')))
|
||||
.thenAnswer((_) => mockHttpStreamFromIterable([
|
||||
'''
|
||||
when(
|
||||
() => mockApiClient.stream(Uri.parse('$kLichessHost/api/stream/event')),
|
||||
).thenAnswer(
|
||||
(_) => mockHttpStreamFromIterable([
|
||||
'''
|
||||
{
|
||||
"type": "gameStart",
|
||||
"game": {
|
||||
@@ -87,21 +90,25 @@ void main() {
|
||||
}
|
||||
}
|
||||
'''
|
||||
]));
|
||||
]),
|
||||
);
|
||||
|
||||
expect(
|
||||
repo.events(),
|
||||
emitsInOrder([
|
||||
predicate((value) => value is GameStartEvent),
|
||||
]));
|
||||
repo.events(),
|
||||
emitsInOrder([
|
||||
predicate((value) => value is GameStartEvent),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('filter out unsupported types of events', () async {
|
||||
when(() =>
|
||||
mockApiClient.stream(Uri.parse('$kLichessHost/api/stream/event')))
|
||||
.thenAnswer((_) => mockHttpStreamFromIterable([
|
||||
'{ "type": "challenge", "challenge": {}}',
|
||||
]));
|
||||
when(
|
||||
() => mockApiClient.stream(Uri.parse('$kLichessHost/api/stream/event')),
|
||||
).thenAnswer(
|
||||
(_) => mockHttpStreamFromIterable([
|
||||
'{ "type": "challenge", "challenge": {}}',
|
||||
]),
|
||||
);
|
||||
|
||||
expect(repo.events(), emitsInOrder([emitsDone]));
|
||||
});
|
||||
@@ -109,36 +116,46 @@ void main() {
|
||||
|
||||
group('GameRepository.gameStateEvents', () {
|
||||
test('can read all supported types of events', () async {
|
||||
when(() => mockApiClient.stream(
|
||||
Uri.parse('$kLichessHost/api/board/game/stream/$gameIdTest')))
|
||||
.thenAnswer((_) => mockHttpStreamFromIterable([
|
||||
'{ "type": "gameFull", "id": "$gameIdTest", "initialFen": "startPos", "state": { "type": "gameState", "moves": "e2e4 c7c5 f2f4 d7d6 g1f3", "wtime": 7598040, "btime": 8395220, "status": "started" }}',
|
||||
'{ "type": "gameState", "moves": "e2e4 c7c5 f2f4 d7d6 g1f3 b8c6", "wtime": 7598140, "btime": 8395220, "status": "started" }',
|
||||
'{ "type": "gameState", "moves": "e2e4 c7c5 f2f4 d7d6 g1f3 b8c6 f1c4", "wtime": 7598140, "btime": 8398220, "status": "started" }',
|
||||
]));
|
||||
when(
|
||||
() => mockApiClient.stream(
|
||||
Uri.parse('$kLichessHost/api/board/game/stream/$gameIdTest'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) => mockHttpStreamFromIterable([
|
||||
'{ "type": "gameFull", "id": "$gameIdTest", "initialFen": "startPos", "state": { "type": "gameState", "moves": "e2e4 c7c5 f2f4 d7d6 g1f3", "wtime": 7598040, "btime": 8395220, "status": "started" }}',
|
||||
'{ "type": "gameState", "moves": "e2e4 c7c5 f2f4 d7d6 g1f3 b8c6", "wtime": 7598140, "btime": 8395220, "status": "started" }',
|
||||
'{ "type": "gameState", "moves": "e2e4 c7c5 f2f4 d7d6 g1f3 b8c6 f1c4", "wtime": 7598140, "btime": 8398220, "status": "started" }',
|
||||
]),
|
||||
);
|
||||
|
||||
expect(
|
||||
repo.gameStateEvents(gameIdTest),
|
||||
emitsInOrder([
|
||||
predicate((value) => value is GameFullEvent),
|
||||
predicate((value) => value is GameStateEvent),
|
||||
predicate((value) => value is GameStateEvent),
|
||||
]));
|
||||
repo.gameStateEvents(gameIdTest),
|
||||
emitsInOrder([
|
||||
predicate((value) => value is GameFullEvent),
|
||||
predicate((value) => value is GameStateEvent),
|
||||
predicate((value) => value is GameStateEvent),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('filter out unsupported types of events', () async {
|
||||
when(() => mockApiClient.stream(
|
||||
Uri.parse('$kLichessHost/api/board/game/stream/$gameIdTest')))
|
||||
.thenAnswer((_) => mockHttpStreamFromIterable([
|
||||
'{ "type": "gameState", "moves": "e2e4 c7c5 f2f4 d7d6 g1f3 b8c6", "wtime": 7598140, "btime": 8395220, "status": "started" }',
|
||||
'{ "type": "chatLine", "username": "testUser", "message": "oops" }',
|
||||
]));
|
||||
when(
|
||||
() => mockApiClient.stream(
|
||||
Uri.parse('$kLichessHost/api/board/game/stream/$gameIdTest'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) => mockHttpStreamFromIterable([
|
||||
'{ "type": "gameState", "moves": "e2e4 c7c5 f2f4 d7d6 g1f3 b8c6", "wtime": 7598140, "btime": 8395220, "status": "started" }',
|
||||
'{ "type": "chatLine", "username": "testUser", "message": "oops" }',
|
||||
]),
|
||||
);
|
||||
|
||||
expect(
|
||||
repo.gameStateEvents(gameIdTest),
|
||||
emitsInOrder([
|
||||
predicate((value) => value is GameStateEvent),
|
||||
]));
|
||||
repo.gameStateEvents(gameIdTest),
|
||||
emitsInOrder([
|
||||
predicate((value) => value is GameStateEvent),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -148,12 +165,14 @@ void main() {
|
||||
{"id":"qVChCOTc","rated":false,"variant":"standard","speed":"blitz","perf":"blitz","createdAt":1673443822389,"lastMoveAt":1673444036416,"status":"mate","players":{"white":{"aiLevel":1},"black":{"user":{"name":"veloce","patron":true,"id":"veloce"},"rating":1435,"provisional":true}},"winner":"black","opening":{"eco":"C20","name":"King's Pawn Game: Wayward Queen Attack, Kiddie Countergambit","ply":4},"moves":"e4 e5 Qh5 Nf6 Qxe5+ Be7 b3 d6 Qb5+ Bd7 Qxb7 Nc6 Ba3 Rb8 Qa6 Nxe4 Bb2 O-O Nc3 Nb4 Nf3 Nxa6 Nd5 Nb4 Nxe7+ Qxe7 Nd4 Qf6 f4 Qe7 Ke2 Ng3+ Kd1 Nxh1 Bc4 Nf2+ Kc1 Qe1#","clocks":[18003,18003,17915,17627,17771,16691,17667,16243,17475,15459,17355,14779,17155,13795,16915,13267,14771,11955,14451,10995,14339,10203,13899,9099,12427,8379,12003,7547,11787,6691,11355,6091,11147,5763,10851,5099,10635,4657],"clock":{"initial":180,"increment":0,"totalTime":180}}
|
||||
''';
|
||||
|
||||
when(() => mockApiClient.get(
|
||||
Uri.parse('$kLichessHost/game/export/qVChCOTc'),
|
||||
headers: {'Accept': 'application/json'},
|
||||
))
|
||||
.thenAnswer(
|
||||
(_) async => Result.value(http.Response(testResponse, 200)));
|
||||
when(
|
||||
() => mockApiClient.get(
|
||||
Uri.parse('$kLichessHost/game/export/qVChCOTc'),
|
||||
headers: {'Accept': 'application/json'},
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => Result.value(http.Response(testResponse, 200)),
|
||||
);
|
||||
|
||||
final result = await repo.getGame(const GameId('qVChCOTc'));
|
||||
|
||||
@@ -165,12 +184,14 @@ void main() {
|
||||
{"id":"3zfAoBZs","rated":false,"variant":"standard","speed":"bullet","perf":"bullet","createdAt":1674191740792,"lastMoveAt":1674191772185,"status":"outoftime","players":{"white":{"user":{"name":"penguingim1","title":"GM","patron":true,"id":"penguingim1"},"rating":3302,"analysis":{"inaccuracy":1,"mistake":1,"blunder":0,"acpl":30}},"black":{"user":{"name":"TackoFall","title":"FM","id":"tackofall"},"rating":2817,"analysis":{"inaccuracy":0,"mistake":0,"blunder":2,"acpl":95}}},"winner":"white","opening":{"eco":"A01","name":"Nimzo-Larsen Attack: Classical Variation","ply":2},"moves":"b3 d5 Bb2 Nc6 Nf3 Nf6 e3 g6 c4 Bg7 cxd5 Qxd5 Nc3 Qd8 h4 O-O h5 Nxh5 Rxh5 gxh5 Ng5 Bg4 Qc2 Re8 Qxh7+ Kf8 Nd5 e5 Ba3+ Ne7 Nf6","analysis":[{"eval":0},{"eval":0},{"eval":0},{"eval":51},{"eval":56},{"eval":53},{"eval":51},{"eval":87},{"eval":79},{"eval":79},{"eval":79},{"eval":85},{"eval":69},{"eval":74},{"eval":20},{"eval":27},{"eval":-57,"best":"d2d4","variation":"d4","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. d4 was best."}},{"eval":-71},{"eval":-216,"best":"d2d4","variation":"d4 e5 d5 Ne7 Qd2 Bd7 Rg1 e4 Ng5 Bg4 Ngxe4 Nxd5 Nxd5 Bxb2","judgment":{"name":"Mistake","comment":"Mistake. d4 was best."}},{"eval":-199},{"eval":-259},{"eval":223,"best":"c8f5","variation":"Bf5","judgment":{"name":"Blunder","comment":"Blunder. Bf5 was best."}},{"eval":208},{"eval":745,"best":"f7f5","variation":"f5 Bc4+ Kh8 Ne6 Qd7 Nxg7 Kxg7 f3 e5 d4 f4 O-O-O Bf5 e4","judgment":{"name":"Blunder","comment":"Blunder. f5 was best."}},{"eval":737},{"eval":773},{"eval":757},{"eval":806},{"eval":802},{"eval":1002},{"eval":945}],"clock":{"initial":30,"increment":0,"totalTime":30}}
|
||||
''';
|
||||
|
||||
when(() => mockApiClient.get(
|
||||
Uri.parse('$kLichessHost/game/export/3zfAoBZs'),
|
||||
headers: {'Accept': 'application/json'},
|
||||
))
|
||||
.thenAnswer(
|
||||
(_) async => Result.value(http.Response(testResponse, 200)));
|
||||
when(
|
||||
() => mockApiClient.get(
|
||||
Uri.parse('$kLichessHost/game/export/3zfAoBZs'),
|
||||
headers: {'Accept': 'application/json'},
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => Result.value(http.Response(testResponse, 200)),
|
||||
);
|
||||
|
||||
final result = await repo.getGame(const GameId('3zfAoBZs'));
|
||||
|
||||
@@ -182,12 +203,14 @@ void main() {
|
||||
{"id":"1vdsvmxp","rated":true,"variant":"threeCheck","speed":"bullet","perf":"threeCheck","createdAt":1604194310939,"lastMoveAt":1604194361831,"status":"variantEnd","players":{"white":{"user":{"name":"Zhigalko_Sergei","title":"GM","patron":true,"id":"zhigalko_sergei"},"rating":2448,"ratingDiff":6,"analysis":{"inaccuracy":1,"mistake":1,"blunder":1,"acpl":75}},"black":{"user":{"name":"catask","id":"catask"},"rating":2485,"ratingDiff":-6,"analysis":{"inaccuracy":1,"mistake":0,"blunder":2,"acpl":115}}},"winner":"white","opening":{"eco":"B02","name":"Alekhine Defense: Scandinavian Variation, Geschev Gambit","ply":6},"moves":"e4 c6 Nc3 d5 exd5 Nf6 Nf3 e5 Bc4 Bd6 O-O O-O h3 e4 Kh1 exf3 Qxf3 cxd5 Nxd5 Nxd5 Bxd5 Nc6 Re1 Be6 Rxe6 fxe6 Bxe6+ Kh8 Qh5 h6 Qg6 Qf6 Qh7+ Kxh7 Bf5+","analysis":[{"eval":340},{"eval":359},{"eval":231},{"eval":300,"best":"g8f6","variation":"Nf6 e5 d5 d4 Ne4 Bd3 Bf5 Nf3 e6 O-O","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. Nf6 was best."}},{"eval":262},{"eval":286},{"eval":184,"best":"f1c4","variation":"Bc4 e6 dxe6 Bxe6 Bxe6 fxe6 Qe2 Qd7 Nf3 Bd6","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. Bc4 was best."}},{"eval":235},{"eval":193},{"eval":243},{"eval":269},{"eval":219},{"eval":-358,"best":"d2d3","variation":"d3 Bg4 h3 e4 Nxe4 Bh2+ Kh1 Nxe4 dxe4 Qf6","judgment":{"name":"Blunder","comment":"Blunder. d3 was best."}},{"eval":-376},{"eval":-386},{"eval":-383},{"eval":-405},{"eval":-363},{"eval":-372},{"eval":-369},{"eval":-345},{"eval":-276},{"eval":-507,"best":"b2b3","variation":"b3 Be6","judgment":{"name":"Mistake","comment":"Mistake. b3 was best."}},{"eval":-49,"best":"c6e5","variation":"Ne5 Qh5","judgment":{"name":"Blunder","comment":"Blunder. Ne5 was best."}},{"eval":-170},{"mate":7,"best":"g8h8","variation":"Kh8 Rh6","judgment":{"name":"Blunder","comment":"Checkmate is now unavoidable. Kh8 was best."}},{"mate":6},{"mate":6},{"mate":5},{"mate":3},{"mate":2},{"mate":2},{"mate":1},{"mate":1}],"clock":{"initial":60,"increment":0,"totalTime":60}}
|
||||
''';
|
||||
|
||||
when(() => mockApiClient.get(
|
||||
Uri.parse('$kLichessHost/game/export/1vdsvmxp'),
|
||||
headers: {'Accept': 'application/json'},
|
||||
))
|
||||
.thenAnswer(
|
||||
(_) async => Result.value(http.Response(testResponse, 200)));
|
||||
when(
|
||||
() => mockApiClient.get(
|
||||
Uri.parse('$kLichessHost/game/export/1vdsvmxp'),
|
||||
headers: {'Accept': 'application/json'},
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => Result.value(http.Response(testResponse, 200)),
|
||||
);
|
||||
|
||||
final result = await repo.getGame(const GameId('1vdsvmxp'));
|
||||
|
||||
|
||||
@@ -34,82 +34,95 @@ void main() {
|
||||
|
||||
setUpAll(() {
|
||||
when(
|
||||
() => mockClient.get(Uri.parse('$kLichessHost/game/export/qVChCOTc'),
|
||||
headers: any(
|
||||
named: 'headers',
|
||||
that: sameHeaders({'Accept': 'application/json'}))),
|
||||
() => mockClient.get(
|
||||
Uri.parse('$kLichessHost/game/export/qVChCOTc'),
|
||||
headers: any(
|
||||
named: 'headers',
|
||||
that: sameHeaders({'Accept': 'application/json'}),
|
||||
),
|
||||
),
|
||||
).thenAnswer((_) => mockResponse(gameResponse, 200));
|
||||
registerFallbackValue(http.Request('GET', Uri.parse('http://api.test')));
|
||||
});
|
||||
|
||||
group('ArchivedGameScreen', () {
|
||||
testWidgets('displays game data and last fen immediately, then moves',
|
||||
(tester) async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
testWidgets(
|
||||
'displays game data and last fen immediately, then moves',
|
||||
(tester) async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
|
||||
final app = await buildTestApp(
|
||||
tester,
|
||||
home: Consumer(builder: (context, ref, _) {
|
||||
return ArchivedGameScreen(
|
||||
gameData: gameData, orientation: Side.white);
|
||||
}),
|
||||
);
|
||||
final app = await buildTestApp(
|
||||
tester,
|
||||
home: Consumer(
|
||||
builder: (context, ref, _) {
|
||||
return ArchivedGameScreen(
|
||||
gameData: gameData,
|
||||
orientation: Side.white,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
sharedPreferencesProvider.overrideWithValue(sharedPreferences),
|
||||
apiClientProvider
|
||||
.overrideWithValue(ApiClient(mockLogger, mockClient)),
|
||||
soundServiceProvider.overrideWithValue(mockSoundService),
|
||||
],
|
||||
child: app,
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
sharedPreferencesProvider.overrideWithValue(sharedPreferences),
|
||||
apiClientProvider
|
||||
.overrideWithValue(ApiClient(mockLogger, mockClient)),
|
||||
soundServiceProvider.overrideWithValue(mockSoundService),
|
||||
],
|
||||
child: app,
|
||||
),
|
||||
);
|
||||
|
||||
// data shown immediately
|
||||
expect(find.byType(cg.Board), findsOneWidget);
|
||||
expect(find.byType(cg.PieceWidget), findsNWidgets(25));
|
||||
expect(find.widgetWithText(BoardPlayer, 'veloce'), findsOneWidget);
|
||||
expect(find.widgetWithText(BoardPlayer, 'Stockfish'), findsOneWidget);
|
||||
// data shown immediately
|
||||
expect(find.byType(cg.Board), findsOneWidget);
|
||||
expect(find.byType(cg.PieceWidget), findsNWidgets(25));
|
||||
expect(find.widgetWithText(BoardPlayer, 'veloce'), findsOneWidget);
|
||||
expect(find.widgetWithText(BoardPlayer, 'Stockfish'), findsOneWidget);
|
||||
|
||||
// cannot interact with board
|
||||
expect(
|
||||
// cannot interact with board
|
||||
expect(
|
||||
tester.widget<cg.Board>(find.byType(cg.Board)).data.interactableSide,
|
||||
cg.InteractableSide.none);
|
||||
cg.InteractableSide.none,
|
||||
);
|
||||
|
||||
// moves are not loaded
|
||||
expect(find.byType(MoveList), findsNothing);
|
||||
expect(
|
||||
// moves are not loaded
|
||||
expect(find.byType(MoveList), findsNothing);
|
||||
expect(
|
||||
tester
|
||||
.widget<IconButton>(find.byKey(const ValueKey('cursor-back')))
|
||||
.onPressed,
|
||||
isNull);
|
||||
isNull,
|
||||
);
|
||||
|
||||
// wait for game steps loading
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
// wait for move list ensureVisible animation to finish
|
||||
await tester.pumpAndSettle();
|
||||
// wait for game steps loading
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
// wait for move list ensureVisible animation to finish
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// same info still displayed
|
||||
expect(find.byType(cg.Board), findsOneWidget);
|
||||
expect(find.byType(cg.PieceWidget), findsNWidgets(25));
|
||||
expect(find.widgetWithText(BoardPlayer, 'veloce'), findsOneWidget);
|
||||
expect(find.widgetWithText(BoardPlayer, 'Stockfish'), findsOneWidget);
|
||||
// same info still displayed
|
||||
expect(find.byType(cg.Board), findsOneWidget);
|
||||
expect(find.byType(cg.PieceWidget), findsNWidgets(25));
|
||||
expect(find.widgetWithText(BoardPlayer, 'veloce'), findsOneWidget);
|
||||
expect(find.widgetWithText(BoardPlayer, 'Stockfish'), findsOneWidget);
|
||||
|
||||
// now with the clocks
|
||||
expect(find.widgetWithText(CountdownClock, '1:46'), findsNWidgets(1));
|
||||
expect(find.widgetWithText(CountdownClock, '0:46'), findsNWidgets(1));
|
||||
// now with the clocks
|
||||
expect(find.widgetWithText(CountdownClock, '1:46'), findsNWidgets(1));
|
||||
expect(find.widgetWithText(CountdownClock, '0:46'), findsNWidgets(1));
|
||||
|
||||
// moves are loaded
|
||||
expect(find.byType(MoveList), findsOneWidget);
|
||||
expect(
|
||||
// moves are loaded
|
||||
expect(find.byType(MoveList), findsOneWidget);
|
||||
expect(
|
||||
tester
|
||||
.widget<IconButton>(find.byKey(const ValueKey('cursor-back')))
|
||||
.onPressed,
|
||||
isNotNull);
|
||||
}, variant: kPlatformVariant);
|
||||
isNotNull,
|
||||
);
|
||||
},
|
||||
variant: kPlatformVariant,
|
||||
);
|
||||
|
||||
testWidgets('navigate game positions', (tester) async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
@@ -117,10 +130,14 @@ void main() {
|
||||
|
||||
final app = await buildTestApp(
|
||||
tester,
|
||||
home: Consumer(builder: (context, ref, _) {
|
||||
return ArchivedGameScreen(
|
||||
gameData: gameData, orientation: Side.white);
|
||||
}),
|
||||
home: Consumer(
|
||||
builder: (context, ref, _) {
|
||||
return ArchivedGameScreen(
|
||||
gameData: gameData,
|
||||
orientation: Side.white,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
@@ -150,11 +167,13 @@ void main() {
|
||||
.toList();
|
||||
|
||||
expect(
|
||||
tester
|
||||
.widget<InlineMoveItem>(
|
||||
find.widgetWithText(InlineMoveItem, 'Qe1#'))
|
||||
.current,
|
||||
isTrue);
|
||||
tester
|
||||
.widget<InlineMoveItem>(
|
||||
find.widgetWithText(InlineMoveItem, 'Qe1#'),
|
||||
)
|
||||
.current,
|
||||
isTrue,
|
||||
);
|
||||
|
||||
for (var i = 0; i < movesAfterE4.length; i++) {
|
||||
// go back in history
|
||||
@@ -173,10 +192,11 @@ void main() {
|
||||
|
||||
// cannot go backward anymore
|
||||
expect(
|
||||
tester
|
||||
.widget<IconButton>(find.byKey(const Key('cursor-back')))
|
||||
.onPressed,
|
||||
isNull);
|
||||
tester
|
||||
.widget<IconButton>(find.byKey(const Key('cursor-back')))
|
||||
.onPressed,
|
||||
isNull,
|
||||
);
|
||||
|
||||
// go to last
|
||||
await tester.tap(find.byKey(const Key('cursor-last')));
|
||||
@@ -185,16 +205,19 @@ void main() {
|
||||
|
||||
// cannot go forward anymore
|
||||
expect(
|
||||
tester
|
||||
.widget<IconButton>(find.byKey(const Key('cursor-forward')))
|
||||
.onPressed,
|
||||
isNull);
|
||||
tester
|
||||
.widget<IconButton>(find.byKey(const Key('cursor-forward')))
|
||||
.onPressed,
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
tester
|
||||
.widget<InlineMoveItem>(
|
||||
find.widgetWithText(InlineMoveItem, 'Qe1#'))
|
||||
.current,
|
||||
isTrue);
|
||||
tester
|
||||
.widget<InlineMoveItem>(
|
||||
find.widgetWithText(InlineMoveItem, 'Qe1#'),
|
||||
)
|
||||
.current,
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -43,281 +43,344 @@ void main() {
|
||||
|
||||
group('PlayableGameScreen', () {
|
||||
testWidgets(
|
||||
'displays game info during loading state and update state after 1st gameFull event',
|
||||
(tester) async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
'displays game info during loading state and update state after 1st gameFull event',
|
||||
(tester) async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
|
||||
when(() => mockClient.send(any(
|
||||
that: sameRequest(http.Request(
|
||||
when(
|
||||
() => mockClient.send(
|
||||
any(
|
||||
that: sameRequest(
|
||||
http.Request(
|
||||
'GET',
|
||||
Uri.parse(
|
||||
'$kLichessHost/api/board/game/stream/$gameIdTest'))))))
|
||||
.thenAnswer((_) => mockHttpStreamFromIterable([
|
||||
'{ "type": "gameFull", "id": "$gameIdTest", "speed": "blitz", "initialFen": "$kInitialFEN", "white": { "id": "white", "name": "White", "rating": 1405 }, "black": { "id": "black", "name": "Black", "rating": 1789 }, "state": { "type": "gameState", "moves": "", "wtime": 180000, "btime": 180000, "status": "started" }}'
|
||||
]));
|
||||
'$kLichessHost/api/board/game/stream/$gameIdTest',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) => mockHttpStreamFromIterable([
|
||||
'{ "type": "gameFull", "id": "$gameIdTest", "speed": "blitz", "initialFen": "$kInitialFEN", "white": { "id": "white", "name": "White", "rating": 1405 }, "black": { "id": "black", "name": "Black", "rating": 1789 }, "state": { "type": "gameState", "moves": "", "wtime": 180000, "btime": 180000, "status": "started" }}'
|
||||
]),
|
||||
);
|
||||
|
||||
final app = await buildTestApp(
|
||||
tester,
|
||||
home: Consumer(builder: (context, ref, _) {
|
||||
return PlayableGameScreen(game: testGame, account: fakeUser);
|
||||
}),
|
||||
);
|
||||
final app = await buildTestApp(
|
||||
tester,
|
||||
home: Consumer(
|
||||
builder: (context, ref, _) {
|
||||
return PlayableGameScreen(game: testGame, account: fakeUser);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
sharedPreferencesProvider.overrideWithValue(sharedPreferences),
|
||||
apiClientProvider
|
||||
.overrideWithValue(ApiClient(mockLogger, mockClient)),
|
||||
soundServiceProvider.overrideWithValue(mockSoundService),
|
||||
],
|
||||
child: app,
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
sharedPreferencesProvider.overrideWithValue(sharedPreferences),
|
||||
apiClientProvider
|
||||
.overrideWithValue(ApiClient(mockLogger, mockClient)),
|
||||
soundServiceProvider.overrideWithValue(mockSoundService),
|
||||
],
|
||||
child: app,
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(cg.Board), findsOneWidget);
|
||||
expect(find.byType(cg.PieceWidget), findsNWidgets(32));
|
||||
expect(find.widgetWithText(BoardPlayer, 'White'), findsOneWidget);
|
||||
expect(find.widgetWithText(BoardPlayer, 'Black'), findsOneWidget);
|
||||
expect(find.widgetWithText(BoardPlayer, '1405'), findsOneWidget);
|
||||
expect(find.widgetWithText(BoardPlayer, '1789'), findsOneWidget);
|
||||
expect(find.widgetWithText(CountdownClock, '0:00'), findsNWidgets(2));
|
||||
expect(find.byType(cg.Board), findsOneWidget);
|
||||
expect(find.byType(cg.PieceWidget), findsNWidgets(32));
|
||||
expect(find.widgetWithText(BoardPlayer, 'White'), findsOneWidget);
|
||||
expect(find.widgetWithText(BoardPlayer, 'Black'), findsOneWidget);
|
||||
expect(find.widgetWithText(BoardPlayer, '1405'), findsOneWidget);
|
||||
expect(find.widgetWithText(BoardPlayer, '1789'), findsOneWidget);
|
||||
expect(find.widgetWithText(CountdownClock, '0:00'), findsNWidgets(2));
|
||||
|
||||
// cannot interact with board during loading state
|
||||
await tester.tap(find.byKey(const Key('e2-whitepawn')));
|
||||
await tester.pump();
|
||||
expect(find.byKey(const Key('e2-selected')), findsNothing);
|
||||
// cannot interact with board during loading state
|
||||
await tester.tap(find.byKey(const Key('e2-whitepawn')));
|
||||
await tester.pump();
|
||||
expect(find.byKey(const Key('e2-selected')), findsNothing);
|
||||
|
||||
// wait for stream loading
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
// wait for stream loading
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
// same info displayed
|
||||
expect(find.byType(cg.Board), findsOneWidget);
|
||||
expect(find.byType(cg.PieceWidget), findsNWidgets(32));
|
||||
expect(find.widgetWithText(BoardPlayer, 'White'), findsOneWidget);
|
||||
expect(find.widgetWithText(BoardPlayer, 'Black'), findsOneWidget);
|
||||
expect(find.widgetWithText(BoardPlayer, '1405'), findsOneWidget);
|
||||
expect(find.widgetWithText(BoardPlayer, '1789'), findsOneWidget);
|
||||
// clock is updated
|
||||
expect(find.widgetWithText(CountdownClock, '3:00'), findsNWidgets(2));
|
||||
// same info displayed
|
||||
expect(find.byType(cg.Board), findsOneWidget);
|
||||
expect(find.byType(cg.PieceWidget), findsNWidgets(32));
|
||||
expect(find.widgetWithText(BoardPlayer, 'White'), findsOneWidget);
|
||||
expect(find.widgetWithText(BoardPlayer, 'Black'), findsOneWidget);
|
||||
expect(find.widgetWithText(BoardPlayer, '1405'), findsOneWidget);
|
||||
expect(find.widgetWithText(BoardPlayer, '1789'), findsOneWidget);
|
||||
// clock is updated
|
||||
expect(find.widgetWithText(CountdownClock, '3:00'), findsNWidgets(2));
|
||||
|
||||
// board interaction is now possible
|
||||
await tester.tap(find.byKey(const Key('e2-whitepawn')));
|
||||
await tester.pump();
|
||||
expect(find.byKey(const Key('e2-selected')), findsOneWidget);
|
||||
}, variant: kPlatformVariant);
|
||||
// board interaction is now possible
|
||||
await tester.tap(find.byKey(const Key('e2-whitepawn')));
|
||||
await tester.pump();
|
||||
expect(find.byKey(const Key('e2-selected')), findsOneWidget);
|
||||
},
|
||||
variant: kPlatformVariant,
|
||||
);
|
||||
|
||||
testWidgets('play two moves', (tester) async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
testWidgets(
|
||||
'play two moves',
|
||||
(tester) async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
|
||||
final app = await buildTestApp(
|
||||
tester,
|
||||
home: Consumer(builder: (context, ref, _) {
|
||||
return PlayableGameScreen(game: testGame, account: fakeUser);
|
||||
}),
|
||||
);
|
||||
final app = await buildTestApp(
|
||||
tester,
|
||||
home: Consumer(
|
||||
builder: (context, ref, _) {
|
||||
return PlayableGameScreen(game: testGame, account: fakeUser);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
sharedPreferencesProvider.overrideWithValue(sharedPreferences),
|
||||
apiClientProvider.overrideWithValue(
|
||||
ApiClient(mockLogger, FakeGameClient(), retries: [])),
|
||||
soundServiceProvider.overrideWithValue(mockSoundService),
|
||||
],
|
||||
child: app,
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
sharedPreferencesProvider.overrideWithValue(sharedPreferences),
|
||||
apiClientProvider.overrideWithValue(
|
||||
ApiClient(mockLogger, FakeGameClient(), retries: []),
|
||||
),
|
||||
soundServiceProvider.overrideWithValue(mockSoundService),
|
||||
],
|
||||
child: app,
|
||||
),
|
||||
);
|
||||
|
||||
await tester
|
||||
.pump(const Duration(milliseconds: 100)); // wait for stream loading
|
||||
await tester
|
||||
.pump(const Duration(milliseconds: 100)); // wait for stream loading
|
||||
|
||||
final boardRect = tester.getRect(find.byType(cg.Board));
|
||||
final boardRect = tester.getRect(find.byType(cg.Board));
|
||||
|
||||
// both clocks are not active first
|
||||
_checkActiveClocks(tester, whiteActive: false, blackActive: false);
|
||||
// both clocks are not active first
|
||||
_checkActiveClocks(tester, whiteActive: false, blackActive: false);
|
||||
|
||||
await makeMove(tester, boardRect, 'e2', 'e4');
|
||||
await makeMove(tester, boardRect, 'e2', 'e4');
|
||||
|
||||
// move made
|
||||
verify(() => mockSoundService.playMove()).called(1);
|
||||
expect(find.byKey(const Key('e4-whitepawn')), findsOneWidget);
|
||||
expect(find.byKey(const Key('e2-whitepawn')), findsNothing);
|
||||
// move made
|
||||
verify(() => mockSoundService.playMove()).called(1);
|
||||
expect(find.byKey(const Key('e4-whitepawn')), findsOneWidget);
|
||||
expect(find.byKey(const Key('e2-whitepawn')), findsNothing);
|
||||
|
||||
// can interact to make premoves
|
||||
await tester.tapAt(squareOffset('f1', boardRect));
|
||||
await tester.pump();
|
||||
expect(find.byKey(const Key('f1-selected')), findsOneWidget);
|
||||
// move cursor updated, can go backward
|
||||
expect(
|
||||
// can interact to make premoves
|
||||
await tester.tapAt(squareOffset('f1', boardRect));
|
||||
await tester.pump();
|
||||
expect(find.byKey(const Key('f1-selected')), findsOneWidget);
|
||||
// move cursor updated, can go backward
|
||||
expect(
|
||||
tester
|
||||
.widget<IconButton>(find.byKey(const ValueKey('cursor-first')))
|
||||
.onPressed,
|
||||
isNotNull);
|
||||
expect(
|
||||
isNotNull,
|
||||
);
|
||||
expect(
|
||||
tester
|
||||
.widget<IconButton>(find.byKey(const ValueKey('cursor-back')))
|
||||
.onPressed,
|
||||
isNotNull);
|
||||
isNotNull,
|
||||
);
|
||||
|
||||
// wait for both white and black move events;
|
||||
await tester.pump(const Duration(seconds: 2));
|
||||
// wait for both white and black move events;
|
||||
await tester.pump(const Duration(seconds: 2));
|
||||
|
||||
// black move happened
|
||||
verify(() => mockSoundService.playMove()).called(1);
|
||||
expect(find.byKey(const Key('e7-blackpawn')), findsNothing);
|
||||
expect(find.byKey(const Key('e5-blackpawn')), findsOneWidget);
|
||||
// black move happened
|
||||
verify(() => mockSoundService.playMove()).called(1);
|
||||
expect(find.byKey(const Key('e7-blackpawn')), findsNothing);
|
||||
expect(find.byKey(const Key('e5-blackpawn')), findsOneWidget);
|
||||
|
||||
_checkActiveClocks(tester, whiteActive: true, blackActive: false);
|
||||
_checkActiveClocks(tester, whiteActive: true, blackActive: false);
|
||||
|
||||
// white plays a second move
|
||||
await makeMove(tester, boardRect, 'd2', 'd4');
|
||||
verify(() => mockSoundService.playMove()).called(1);
|
||||
expect(find.byKey(const Key('d4-whitepawn')), findsOneWidget);
|
||||
expect(find.byKey(const Key('d2-whitepawn')), findsNothing);
|
||||
// white plays a second move
|
||||
await makeMove(tester, boardRect, 'd2', 'd4');
|
||||
verify(() => mockSoundService.playMove()).called(1);
|
||||
expect(find.byKey(const Key('d4-whitepawn')), findsOneWidget);
|
||||
expect(find.byKey(const Key('d2-whitepawn')), findsNothing);
|
||||
|
||||
_checkActiveClocks(tester, whiteActive: false, blackActive: true);
|
||||
_checkActiveClocks(tester, whiteActive: false, blackActive: true);
|
||||
|
||||
// wait for both white and black move events;
|
||||
await tester.pump(const Duration(seconds: 2));
|
||||
// wait for both white and black move events;
|
||||
await tester.pump(const Duration(seconds: 2));
|
||||
|
||||
// black 2nd move happened
|
||||
verify(() => mockSoundService.playMove()).called(1);
|
||||
expect(find.byKey(const Key('b8-blackknight')), findsNothing);
|
||||
expect(find.byKey(const Key('c6-blackknight')), findsOneWidget);
|
||||
// black 2nd move happened
|
||||
verify(() => mockSoundService.playMove()).called(1);
|
||||
expect(find.byKey(const Key('b8-blackknight')), findsNothing);
|
||||
expect(find.byKey(const Key('c6-blackknight')), findsOneWidget);
|
||||
|
||||
_checkActiveClocks(tester, whiteActive: true, blackActive: false);
|
||||
_checkActiveClocks(tester, whiteActive: true, blackActive: false);
|
||||
|
||||
// can go back in history
|
||||
await tester.tap(find.byKey(const Key('cursor-back')));
|
||||
await tester.pump();
|
||||
// cannot interact anymore
|
||||
expect(
|
||||
// can go back in history
|
||||
await tester.tap(find.byKey(const Key('cursor-back')));
|
||||
await tester.pump();
|
||||
// cannot interact anymore
|
||||
expect(
|
||||
tester.widget<cg.Board>(find.byType(cg.Board)).data.interactableSide,
|
||||
cg.InteractableSide.none);
|
||||
cg.InteractableSide.none,
|
||||
);
|
||||
|
||||
// can go back 3 more times
|
||||
await tester.tap(find.byKey(const Key('cursor-back')));
|
||||
await tester.pump();
|
||||
await tester.tap(find.byKey(const Key('cursor-back')));
|
||||
await tester.pump();
|
||||
await tester.tap(find.byKey(const Key('cursor-back')));
|
||||
await tester.pump();
|
||||
// back at initial position
|
||||
expect(find.byKey(const Key('e2-whitepawn')), findsOneWidget);
|
||||
// cannot go backward anymore
|
||||
expect(
|
||||
// can go back 3 more times
|
||||
await tester.tap(find.byKey(const Key('cursor-back')));
|
||||
await tester.pump();
|
||||
await tester.tap(find.byKey(const Key('cursor-back')));
|
||||
await tester.pump();
|
||||
await tester.tap(find.byKey(const Key('cursor-back')));
|
||||
await tester.pump();
|
||||
// back at initial position
|
||||
expect(find.byKey(const Key('e2-whitepawn')), findsOneWidget);
|
||||
// cannot go backward anymore
|
||||
expect(
|
||||
tester
|
||||
.widget<IconButton>(find.byKey(const Key('cursor-back')))
|
||||
.onPressed,
|
||||
isNull);
|
||||
// go back to last position
|
||||
await tester.tap(find.byKey(const Key('cursor-last')));
|
||||
// need to wait for move list animation
|
||||
await tester.pumpAndSettle();
|
||||
// board is interactable again
|
||||
expect(
|
||||
isNull,
|
||||
);
|
||||
// go back to last position
|
||||
await tester.tap(find.byKey(const Key('cursor-last')));
|
||||
// need to wait for move list animation
|
||||
await tester.pumpAndSettle();
|
||||
// board is interactable again
|
||||
expect(
|
||||
tester.widget<cg.Board>(find.byType(cg.Board)).data.interactableSide,
|
||||
cg.InteractableSide.white);
|
||||
}, variant: kPlatformVariant);
|
||||
cg.InteractableSide.white,
|
||||
);
|
||||
},
|
||||
variant: kPlatformVariant,
|
||||
);
|
||||
|
||||
testWidgets('reacts to abort event', (tester) async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
final streamController = StreamController<String>();
|
||||
streamController.onListen = () {
|
||||
streamController.add(
|
||||
'{ "type": "gameFull", "id": "$gameIdTest", "speed": "blitz", "initialFen": "$kInitialFEN", "white": { "id": "white", "name": "White", "rating": 1405 }, "black": { "id": "black", "name": "Black", "rating": 1789 }, "state": { "type": "gameState", "moves": "", "wtime": 180000, "btime": 180000, "status": "started" }}');
|
||||
};
|
||||
testWidgets(
|
||||
'reacts to abort event',
|
||||
(tester) async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
final streamController = StreamController<String>();
|
||||
streamController.onListen = () {
|
||||
streamController.add(
|
||||
'{ "type": "gameFull", "id": "$gameIdTest", "speed": "blitz", "initialFen": "$kInitialFEN", "white": { "id": "white", "name": "White", "rating": 1405 }, "black": { "id": "black", "name": "Black", "rating": 1789 }, "state": { "type": "gameState", "moves": "", "wtime": 180000, "btime": 180000, "status": "started" }}',
|
||||
);
|
||||
};
|
||||
|
||||
when(() => mockClient.send(any(
|
||||
that: sameRequest(http.Request(
|
||||
when(
|
||||
() => mockClient.send(
|
||||
any(
|
||||
that: sameRequest(
|
||||
http.Request(
|
||||
'GET',
|
||||
Uri.parse(
|
||||
'$kLichessHost/api/board/game/stream/$gameIdTest'))))))
|
||||
.thenAnswer((_) => mockHttpStream(streamController.stream));
|
||||
'$kLichessHost/api/board/game/stream/$gameIdTest',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
).thenAnswer((_) => mockHttpStream(streamController.stream));
|
||||
|
||||
final app = await buildTestApp(
|
||||
tester,
|
||||
home: Consumer(builder: (context, ref, _) {
|
||||
return PlayableGameScreen(game: testGame, account: fakeUser);
|
||||
}),
|
||||
);
|
||||
final app = await buildTestApp(
|
||||
tester,
|
||||
home: Consumer(
|
||||
builder: (context, ref, _) {
|
||||
return PlayableGameScreen(game: testGame, account: fakeUser);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
sharedPreferencesProvider.overrideWithValue(sharedPreferences),
|
||||
apiClientProvider
|
||||
.overrideWithValue(ApiClient(mockLogger, mockClient)),
|
||||
soundServiceProvider.overrideWithValue(mockSoundService),
|
||||
],
|
||||
child: app,
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
sharedPreferencesProvider.overrideWithValue(sharedPreferences),
|
||||
apiClientProvider
|
||||
.overrideWithValue(ApiClient(mockLogger, mockClient)),
|
||||
soundServiceProvider.overrideWithValue(mockSoundService),
|
||||
],
|
||||
child: app,
|
||||
),
|
||||
);
|
||||
|
||||
// wait for stream loading
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
// board is interactable
|
||||
expect(
|
||||
// wait for stream loading
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
// board is interactable
|
||||
expect(
|
||||
tester.widget<cg.Board>(find.byType(cg.Board)).data.interactableSide,
|
||||
cg.InteractableSide.white);
|
||||
cg.InteractableSide.white,
|
||||
);
|
||||
|
||||
// trying to exit game will show an alert
|
||||
await tapBackButton(tester);
|
||||
await tester.pump();
|
||||
expect(
|
||||
find.text('Are you sure you want to quit the game?'), findsOneWidget);
|
||||
// trying to exit game will show an alert
|
||||
await tapBackButton(tester);
|
||||
await tester.pump();
|
||||
expect(
|
||||
find.text('Are you sure you want to quit the game?'),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
// cancel popup
|
||||
await tester.tap(find.widgetWithText(TextButton, 'Cancel'));
|
||||
await tester.pump();
|
||||
// cancel popup
|
||||
await tester.tap(find.widgetWithText(TextButton, 'Cancel'));
|
||||
await tester.pump();
|
||||
|
||||
streamController.add(
|
||||
'{ "type": "gameState", "moves": "", "wtime": 180000, "btime": 180000, "status": "aborted" }');
|
||||
// wait for abort event
|
||||
await tester.pump(const Duration(milliseconds: 20));
|
||||
streamController.add(
|
||||
'{ "type": "gameState", "moves": "", "wtime": 180000, "btime": 180000, "status": "aborted" }',
|
||||
);
|
||||
// wait for abort event
|
||||
await tester.pump(const Duration(milliseconds: 20));
|
||||
|
||||
verify(() => mockSoundService.playDong()).called(1);
|
||||
// board is not interactable anymore
|
||||
expect(
|
||||
verify(() => mockSoundService.playDong()).called(1);
|
||||
// board is not interactable anymore
|
||||
expect(
|
||||
tester.widget<cg.Board>(find.byType(cg.Board)).data.interactableSide,
|
||||
cg.InteractableSide.none);
|
||||
cg.InteractableSide.none,
|
||||
);
|
||||
|
||||
// trying now to exit will not show the alert
|
||||
await tapBackButton(tester);
|
||||
// needs this to avoid timer pending
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.text('Are you sure you want to quit the game?'), findsNothing);
|
||||
}, variant: kPlatformVariant);
|
||||
// trying now to exit will not show the alert
|
||||
await tapBackButton(tester);
|
||||
// needs this to avoid timer pending
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.text('Are you sure you want to quit the game?'),
|
||||
findsNothing,
|
||||
);
|
||||
},
|
||||
variant: kPlatformVariant,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// --
|
||||
|
||||
void _checkActiveClocks(WidgetTester tester,
|
||||
{required bool whiteActive, required bool blackActive}) {
|
||||
void _checkActiveClocks(
|
||||
WidgetTester tester, {
|
||||
required bool whiteActive,
|
||||
required bool blackActive,
|
||||
}) {
|
||||
expect(
|
||||
tester
|
||||
.widget<CountdownClock>(find.descendant(
|
||||
of: find.byKey(const ValueKey('white-player')),
|
||||
matching: find.byType(CountdownClock)))
|
||||
.active,
|
||||
whiteActive);
|
||||
tester
|
||||
.widget<CountdownClock>(
|
||||
find.descendant(
|
||||
of: find.byKey(const ValueKey('white-player')),
|
||||
matching: find.byType(CountdownClock),
|
||||
),
|
||||
)
|
||||
.active,
|
||||
whiteActive,
|
||||
);
|
||||
expect(
|
||||
tester
|
||||
.widget<CountdownClock>(find.descendant(
|
||||
of: find.byKey(const ValueKey('black-player')),
|
||||
matching: find.byType(CountdownClock)))
|
||||
.active,
|
||||
blackActive);
|
||||
tester
|
||||
.widget<CountdownClock>(
|
||||
find.descendant(
|
||||
of: find.byKey(const ValueKey('black-player')),
|
||||
matching: find.byType(CountdownClock),
|
||||
),
|
||||
)
|
||||
.active,
|
||||
blackActive,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> makeMove(
|
||||
WidgetTester tester, Rect boardRect, String from, String to) async {
|
||||
WidgetTester tester,
|
||||
Rect boardRect,
|
||||
String from,
|
||||
String to,
|
||||
) async {
|
||||
await tester.tapAt(squareOffset(from, boardRect));
|
||||
await tester.pump();
|
||||
await tester.tapAt(squareOffset(to, boardRect));
|
||||
@@ -336,7 +399,8 @@ class FakeGameClient extends Fake implements http.Client {
|
||||
if (request.url.path.contains('game/stream')) {
|
||||
streamController.onListen = () {
|
||||
streamController.add(
|
||||
'{ "type": "gameFull", "id": "$gameIdTest", "speed": "blitz", "initialFen": "$kInitialFEN", "white": { "id": "white", "name": "White", "rating": 1405 }, "black": { "id": "black", "name": "Black", "rating": 1789 }, "state": { "type": "gameState", "moves": "", "wtime": 180000, "btime": 180000, "status": "started" }}');
|
||||
'{ "type": "gameFull", "id": "$gameIdTest", "speed": "blitz", "initialFen": "$kInitialFEN", "white": { "id": "white", "name": "White", "rating": 1405 }, "black": { "id": "black", "name": "Black", "rating": 1789 }, "state": { "type": "gameState", "moves": "", "wtime": 180000, "btime": 180000, "status": "started" }}',
|
||||
);
|
||||
};
|
||||
|
||||
return mockHttpStream(streamController.stream);
|
||||
@@ -348,7 +412,8 @@ class FakeGameClient extends Fake implements http.Client {
|
||||
whiteTime -= 5000;
|
||||
position = position.play(Move.fromUci(move)!);
|
||||
_sendGameEvent(
|
||||
'{ "type": "gameState", "moves": "${moves.join(' ')}", "wtime": $whiteTime, "btime": $blackTime, "status": "started" }');
|
||||
'{ "type": "gameState", "moves": "${moves.join(' ')}", "wtime": $whiteTime, "btime": $blackTime, "status": "started" }',
|
||||
);
|
||||
|
||||
// black response
|
||||
await Future<void>.delayed(const Duration(milliseconds: 500));
|
||||
@@ -358,14 +423,16 @@ class FakeGameClient extends Fake implements http.Client {
|
||||
blackTime -= 3000;
|
||||
position = position.play(Move.fromUci('e7e5')!);
|
||||
_sendGameEvent(
|
||||
'{ "type": "gameState", "moves": "${moves.join(' ')}", "wtime": $whiteTime, "btime": $blackTime, "status": "started" }');
|
||||
'{ "type": "gameState", "moves": "${moves.join(' ')}", "wtime": $whiteTime, "btime": $blackTime, "status": "started" }',
|
||||
);
|
||||
break;
|
||||
case 3:
|
||||
moves.add('b8c6');
|
||||
blackTime -= 6000;
|
||||
position = position.play(Move.fromUci('b8c6')!);
|
||||
_sendGameEvent(
|
||||
'{ "type": "gameState", "moves": "${moves.join(' ')}", "wtime": $whiteTime, "btime": $blackTime, "status": "started" }');
|
||||
'{ "type": "gameState", "moves": "${moves.join(' ')}", "wtime": $whiteTime, "btime": $blackTime, "status": "started" }',
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -44,168 +44,215 @@ void main() {
|
||||
|
||||
when(() => mockClient.get(Uri.parse('$kLichessHost/api/user/maia9')))
|
||||
.thenAnswer((_) => mockResponse(maiaResponses['maia9']!, 200));
|
||||
when(() => mockClient.get(
|
||||
Uri.parse('$kLichessHost/api/users/status?ids=maia1,maia5,maia9')))
|
||||
.thenAnswer((_) => mockResponse(maiaStatusResponses, 200));
|
||||
when(
|
||||
() => mockClient.get(
|
||||
Uri.parse('$kLichessHost/api/users/status?ids=maia1,maia5,maia9'),
|
||||
),
|
||||
).thenAnswer((_) => mockResponse(maiaStatusResponses, 200));
|
||||
|
||||
registerFallbackValue(http.Request('GET', Uri.parse('http://api.test')));
|
||||
});
|
||||
|
||||
group('PlayScreen', () {
|
||||
testWidgets('meets accessibility guidelines', (tester) async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
testWidgets(
|
||||
'meets accessibility guidelines',
|
||||
(tester) async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
|
||||
final SemanticsHandle handle = tester.ensureSemantics();
|
||||
final SemanticsHandle handle = tester.ensureSemantics();
|
||||
|
||||
final app = await buildTestApp(
|
||||
tester,
|
||||
home: Consumer(builder: (context, ref, _) {
|
||||
return const PlayScreen();
|
||||
}),
|
||||
);
|
||||
final app = await buildTestApp(
|
||||
tester,
|
||||
home: Consumer(
|
||||
builder: (context, ref, _) {
|
||||
return const PlayScreen();
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
sharedPreferencesProvider.overrideWithValue(sharedPreferences),
|
||||
authRepositoryProvider
|
||||
.overrideWithValue(FakeAuthRepository(fakeUser)),
|
||||
apiClientProvider
|
||||
.overrideWithValue(ApiClient(mockLogger, mockClient)),
|
||||
],
|
||||
child: app,
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
sharedPreferencesProvider.overrideWithValue(sharedPreferences),
|
||||
authRepositoryProvider
|
||||
.overrideWithValue(FakeAuthRepository(fakeUser)),
|
||||
apiClientProvider
|
||||
.overrideWithValue(ApiClient(mockLogger, mockClient)),
|
||||
],
|
||||
child: app,
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
||||
await tester.pump();
|
||||
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
||||
await tester.pump();
|
||||
|
||||
// wait for maia bots request to return
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
// wait for maia bots request to return
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
|
||||
handle.dispose();
|
||||
}, variant: kPlatformVariant);
|
||||
await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
|
||||
handle.dispose();
|
||||
},
|
||||
variant: kPlatformVariant,
|
||||
);
|
||||
|
||||
testWidgets('loads maia bots info', (tester) async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
testWidgets(
|
||||
'loads maia bots info',
|
||||
(tester) async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
|
||||
final app = await buildTestApp(
|
||||
tester,
|
||||
home: Consumer(builder: (context, ref, _) {
|
||||
return const PlayScreen();
|
||||
}),
|
||||
);
|
||||
final app = await buildTestApp(
|
||||
tester,
|
||||
home: Consumer(
|
||||
builder: (context, ref, _) {
|
||||
return const PlayScreen();
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
authRepositoryProvider
|
||||
.overrideWithValue(FakeAuthRepository(fakeUser)),
|
||||
sharedPreferencesProvider.overrideWithValue(sharedPreferences),
|
||||
apiClientProvider
|
||||
.overrideWithValue(ApiClient(mockLogger, mockClient)),
|
||||
],
|
||||
child: app,
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
authRepositoryProvider
|
||||
.overrideWithValue(FakeAuthRepository(fakeUser)),
|
||||
sharedPreferencesProvider.overrideWithValue(sharedPreferences),
|
||||
apiClientProvider
|
||||
.overrideWithValue(ApiClient(mockLogger, mockClient)),
|
||||
],
|
||||
child: app,
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
||||
await tester.pump();
|
||||
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
||||
await tester.pump();
|
||||
|
||||
// maia bots loading state
|
||||
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
||||
// maia bots loading state
|
||||
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
||||
|
||||
await tester.pump(const Duration(
|
||||
milliseconds: 100)); // wait for maia bots request to return
|
||||
await tester.pump(
|
||||
const Duration(
|
||||
milliseconds: 100,
|
||||
),
|
||||
); // wait for maia bots request to return
|
||||
|
||||
// loaded maia ratings
|
||||
expect(find.widgetWithIcon(PlatformCard, LichessIcons.blitz),
|
||||
findsNWidgets(3));
|
||||
expect(find.widgetWithText(PlatformCard, '1541'), findsOneWidget);
|
||||
expect(find.widgetWithIcon(PlatformCard, LichessIcons.rapid),
|
||||
findsNWidgets(3));
|
||||
expect(find.widgetWithText(PlatformCard, '1477'), findsOneWidget);
|
||||
expect(find.widgetWithIcon(PlatformCard, LichessIcons.classical),
|
||||
findsNWidgets(3));
|
||||
expect(find.widgetWithText(PlatformCard, '1421'), findsOneWidget);
|
||||
// loaded maia ratings
|
||||
expect(
|
||||
find.widgetWithIcon(PlatformCard, LichessIcons.blitz),
|
||||
findsNWidgets(3),
|
||||
);
|
||||
expect(find.widgetWithText(PlatformCard, '1541'), findsOneWidget);
|
||||
expect(
|
||||
find.widgetWithIcon(PlatformCard, LichessIcons.rapid),
|
||||
findsNWidgets(3),
|
||||
);
|
||||
expect(find.widgetWithText(PlatformCard, '1477'), findsOneWidget);
|
||||
expect(
|
||||
find.widgetWithIcon(PlatformCard, LichessIcons.classical),
|
||||
findsNWidgets(3),
|
||||
);
|
||||
expect(find.widgetWithText(PlatformCard, '1421'), findsOneWidget);
|
||||
|
||||
// change maia opponent
|
||||
await tester.tap(find.text('maia5'));
|
||||
await tester.pump();
|
||||
// change maia opponent
|
||||
await tester.tap(find.text('maia5'));
|
||||
await tester.pump();
|
||||
|
||||
expect(
|
||||
expect(
|
||||
tester
|
||||
.widget<ListTileChoice>(find.byType(ListTileChoice<MaiaStrength>))
|
||||
.selectedItem,
|
||||
equals(MaiaStrength.maia5));
|
||||
equals(MaiaStrength.maia5),
|
||||
);
|
||||
|
||||
// choose stockfish opponent
|
||||
await tester.tap(find.text('Stockfish 14'));
|
||||
await tester.pump();
|
||||
expect(find.text('Strength'), findsOneWidget);
|
||||
}, variant: kPlatformVariant);
|
||||
// choose stockfish opponent
|
||||
await tester.tap(find.text('Stockfish 14'));
|
||||
await tester.pump();
|
||||
expect(find.text('Strength'), findsOneWidget);
|
||||
},
|
||||
variant: kPlatformVariant,
|
||||
);
|
||||
|
||||
testWidgets('changes time control', (tester) async {
|
||||
SharedPreferences.setMockInitialValues({
|
||||
'play.timeControl': '3 + 2',
|
||||
});
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
testWidgets(
|
||||
'changes time control',
|
||||
(tester) async {
|
||||
SharedPreferences.setMockInitialValues({
|
||||
'play.timeControl': '3 + 2',
|
||||
});
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
|
||||
final app = await buildTestApp(
|
||||
tester,
|
||||
home: Consumer(builder: (context, ref, _) {
|
||||
return const PlayScreen();
|
||||
}),
|
||||
);
|
||||
final app = await buildTestApp(
|
||||
tester,
|
||||
home: Consumer(
|
||||
builder: (context, ref, _) {
|
||||
return const PlayScreen();
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
authRepositoryProvider
|
||||
.overrideWithValue(FakeAuthRepository(fakeUser)),
|
||||
sharedPreferencesProvider.overrideWithValue(sharedPreferences),
|
||||
apiClientProvider
|
||||
.overrideWithValue(ApiClient(mockLogger, mockClient)),
|
||||
],
|
||||
child: app,
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
authRepositoryProvider
|
||||
.overrideWithValue(FakeAuthRepository(fakeUser)),
|
||||
sharedPreferencesProvider.overrideWithValue(sharedPreferences),
|
||||
apiClientProvider
|
||||
.overrideWithValue(ApiClient(mockLogger, mockClient)),
|
||||
],
|
||||
child: app,
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
||||
await tester.pump();
|
||||
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.text('3 + 2'));
|
||||
await tester.pumpAndSettle(); // wait for the animation to finish
|
||||
await tester.tap(find.text('3 + 2'));
|
||||
await tester.pumpAndSettle(); // wait for the animation to finish
|
||||
|
||||
await tester.tap(find.byKey(const ValueKey(TimeControl.rapid1)));
|
||||
await tester.pumpAndSettle(); // wait for the animation to finish
|
||||
await tester.tap(find.byKey(const ValueKey(TimeControl.rapid1)));
|
||||
await tester.pumpAndSettle(); // wait for the animation to finish
|
||||
|
||||
expect(find.widgetWithText(OutlinedButton, '10 + 0'), findsOneWidget);
|
||||
expect(find.widgetWithIcon(OutlinedButton, LichessIcons.rapid),
|
||||
findsOneWidget);
|
||||
}, variant: kPlatformVariant);
|
||||
expect(find.widgetWithText(OutlinedButton, '10 + 0'), findsOneWidget);
|
||||
expect(
|
||||
find.widgetWithIcon(OutlinedButton, LichessIcons.rapid),
|
||||
findsOneWidget,
|
||||
);
|
||||
},
|
||||
variant: kPlatformVariant,
|
||||
);
|
||||
|
||||
testWidgets('creates a game', (tester) async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
testWidgets(
|
||||
'creates a game',
|
||||
(tester) async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
|
||||
const gameIdTest = 'rCRw1AuO';
|
||||
const gameIdTest = 'rCRw1AuO';
|
||||
|
||||
when(() => mockClient
|
||||
.post(Uri.parse('$kLichessHost/api/challenge/maia1'), body: {
|
||||
'clock.limit': '${5 * 60}',
|
||||
'clock.increment': '3',
|
||||
'color': 'random',
|
||||
})).thenAnswer((_) => mockResponse('ok', 200));
|
||||
when(
|
||||
() => mockClient.post(
|
||||
Uri.parse('$kLichessHost/api/challenge/maia1'),
|
||||
body: {
|
||||
'clock.limit': '${5 * 60}',
|
||||
'clock.increment': '3',
|
||||
'color': 'random',
|
||||
},
|
||||
),
|
||||
).thenAnswer((_) => mockResponse('ok', 200));
|
||||
|
||||
when(() => mockClient.send(any(
|
||||
that: sameRequest(http.Request(
|
||||
'GET', Uri.parse('$kLichessHost/api/stream/event'))))))
|
||||
.thenAnswer((_) => mockHttpStreamFromIterable([
|
||||
'''
|
||||
when(
|
||||
() => mockClient.send(
|
||||
any(
|
||||
that: sameRequest(
|
||||
http.Request(
|
||||
'GET',
|
||||
Uri.parse('$kLichessHost/api/stream/event'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) => mockHttpStreamFromIterable([
|
||||
'''
|
||||
{
|
||||
"type": "gameStart",
|
||||
"game": {
|
||||
@@ -236,55 +283,73 @@ void main() {
|
||||
}
|
||||
}
|
||||
'''
|
||||
]));
|
||||
]),
|
||||
);
|
||||
|
||||
when(() => mockClient.send(any(
|
||||
that: sameRequest(http.Request(
|
||||
when(
|
||||
() => mockClient.send(
|
||||
any(
|
||||
that: sameRequest(
|
||||
http.Request(
|
||||
'GET',
|
||||
Uri.parse(
|
||||
'$kLichessHost/api/board/game/stream/$gameIdTest'))))))
|
||||
.thenAnswer((_) => mockHttpStreamFromIterable([
|
||||
'{ "type": "gameFull", "id": "$gameIdTest", "speed": "blitz", "initialFen": "$kInitialFEN", "white": { "id": "white", "name": "White", "rating": 1405 }, "black": { "id": "black", "name": "Black", "rating": 1789 }, "state": { "type": "gameState", "moves": "", "wtime": 180000, "btime": 180000, "status": "started" }}'
|
||||
]));
|
||||
'$kLichessHost/api/board/game/stream/$gameIdTest',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) => mockHttpStreamFromIterable([
|
||||
'{ "type": "gameFull", "id": "$gameIdTest", "speed": "blitz", "initialFen": "$kInitialFEN", "white": { "id": "white", "name": "White", "rating": 1405 }, "black": { "id": "black", "name": "Black", "rating": 1789 }, "state": { "type": "gameState", "moves": "", "wtime": 180000, "btime": 180000, "status": "started" }}'
|
||||
]),
|
||||
);
|
||||
|
||||
final app = await buildTestApp(
|
||||
tester,
|
||||
home: Consumer(builder: (context, ref, _) {
|
||||
return const PlayScreen();
|
||||
}),
|
||||
);
|
||||
final app = await buildTestApp(
|
||||
tester,
|
||||
home: Consumer(
|
||||
builder: (context, ref, _) {
|
||||
return const PlayScreen();
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
authRepositoryProvider
|
||||
.overrideWithValue(FakeAuthRepository(fakeUser)),
|
||||
sharedPreferencesProvider.overrideWithValue(sharedPreferences),
|
||||
apiClientProvider
|
||||
.overrideWithValue(ApiClient(mockLogger, mockClient)),
|
||||
soundServiceProvider.overrideWithValue(mockSoundService),
|
||||
],
|
||||
child: app,
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
authRepositoryProvider
|
||||
.overrideWithValue(FakeAuthRepository(fakeUser)),
|
||||
sharedPreferencesProvider.overrideWithValue(sharedPreferences),
|
||||
apiClientProvider
|
||||
.overrideWithValue(ApiClient(mockLogger, mockClient)),
|
||||
soundServiceProvider.overrideWithValue(mockSoundService),
|
||||
],
|
||||
child: app,
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
||||
await tester.pump();
|
||||
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
||||
await tester.pump();
|
||||
|
||||
await tester.pump(const Duration(
|
||||
milliseconds: 100)); // wait for maia bots request to return
|
||||
await tester.pump(
|
||||
const Duration(
|
||||
milliseconds: 100,
|
||||
),
|
||||
); // wait for maia bots request to return
|
||||
|
||||
expect(find.byType(CircularProgressIndicator), findsNothing);
|
||||
await tester.tap(find.widgetWithText(FatButton, 'Play'));
|
||||
await tester.pump(); // play action tapped
|
||||
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
||||
await tester
|
||||
.pump(const Duration(seconds: 3)); // wait for create game service
|
||||
await tester.pumpAndSettle(); // wait for page change animation
|
||||
expect(find.byType(CircularProgressIndicator), findsNothing);
|
||||
await tester.tap(find.widgetWithText(FatButton, 'Play'));
|
||||
await tester.pump(); // play action tapped
|
||||
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
||||
await tester
|
||||
.pump(const Duration(seconds: 3)); // wait for create game service
|
||||
await tester.pumpAndSettle(); // wait for page change animation
|
||||
|
||||
expect(find.byType(PlayableGameScreen), findsOneWidget);
|
||||
expect(find.byType(cg.PieceWidget), findsNWidgets(32));
|
||||
}, variant: kPlatformVariant);
|
||||
expect(find.byType(PlayableGameScreen), findsOneWidget);
|
||||
expect(find.byType(cg.PieceWidget), findsNWidgets(32));
|
||||
},
|
||||
variant: kPlatformVariant,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -17,31 +17,40 @@ void main() {
|
||||
final db = PuzzleLocalDB(sharedPreferences);
|
||||
|
||||
final data = PuzzleLocalData(
|
||||
solved: IList(const [
|
||||
PuzzleSolution(id: PuzzleId('pId'), win: true, rated: true),
|
||||
PuzzleSolution(id: PuzzleId('pId2'), win: false, rated: true),
|
||||
]),
|
||||
unsolved: IList([
|
||||
Puzzle(
|
||||
puzzle: PuzzleData(
|
||||
id: const PuzzleId('pId3'),
|
||||
rating: 1988,
|
||||
plays: 5,
|
||||
initialPly: 23,
|
||||
solution: IList(const ['a6a7', 'b2a2', 'c4c2']),
|
||||
themes: ISet(const ['endgame', 'advantage'])),
|
||||
game: const PuzzleGame(
|
||||
id: GameId('PrlkCqOv'),
|
||||
perf: Perf.blitz,
|
||||
rated: true,
|
||||
white: PuzzlePlayer(
|
||||
side: Side.white, userId: 'user1', name: 'user1'),
|
||||
black: PuzzlePlayer(
|
||||
side: Side.black, userId: 'user2', name: 'user2'),
|
||||
pgn:
|
||||
'e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2'),
|
||||
solved: IList(const [
|
||||
PuzzleSolution(id: PuzzleId('pId'), win: true, rated: true),
|
||||
PuzzleSolution(id: PuzzleId('pId2'), win: false, rated: true),
|
||||
]),
|
||||
unsolved: IList([
|
||||
Puzzle(
|
||||
puzzle: PuzzleData(
|
||||
id: const PuzzleId('pId3'),
|
||||
rating: 1988,
|
||||
plays: 5,
|
||||
initialPly: 23,
|
||||
solution: IList(const ['a6a7', 'b2a2', 'c4c2']),
|
||||
themes: ISet(const ['endgame', 'advantage']),
|
||||
),
|
||||
]));
|
||||
game: const PuzzleGame(
|
||||
id: GameId('PrlkCqOv'),
|
||||
perf: Perf.blitz,
|
||||
rated: true,
|
||||
white: PuzzlePlayer(
|
||||
side: Side.white,
|
||||
userId: 'user1',
|
||||
name: 'user1',
|
||||
),
|
||||
black: PuzzlePlayer(
|
||||
side: Side.black,
|
||||
userId: 'user2',
|
||||
name: 'user2',
|
||||
),
|
||||
pgn:
|
||||
'e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2',
|
||||
),
|
||||
),
|
||||
]),
|
||||
);
|
||||
|
||||
await db.save(angle: PuzzleTheme.mix, data: data);
|
||||
|
||||
|
||||
@@ -17,8 +17,10 @@ void main() {
|
||||
final mockLogger = MockLogger();
|
||||
final mockClient = MockClient();
|
||||
// final mockApiClient = MockApiClient();
|
||||
final repo = PuzzleRepository(mockLogger,
|
||||
apiClient: ApiClient(mockLogger, mockClient));
|
||||
final repo = PuzzleRepository(
|
||||
mockLogger,
|
||||
apiClient: ApiClient(mockLogger, mockClient),
|
||||
);
|
||||
|
||||
setUp(() {
|
||||
reset(mockClient);
|
||||
@@ -28,9 +30,11 @@ void main() {
|
||||
const batchResponse = '''
|
||||
{"puzzles":[{"game":{"id":"PrlkCqOv","perf":{"key":"rapid","name":"Rapid"},"rated":true,"players":[{"userId":"silverjo","name":"silverjo (1777)","color":"white"},{"userId":"robyarchitetto","name":"Robyarchitetto (1742)","color":"black"}],"pgn":"e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2 Bb7 Bh6 d5 e5 d4 Bxg7 Kxg7 Qf4 Bxf3 Qxf3 dxc3 Nxc3 Nac6 Qf6+ Kg8 Rd1 Nd4 O-O c5 Ne4 Nef5 Rd2 Qxf6 Nxf6+ Kg7 Re1 h5 h3 Rad8 b4 Nh4 Re3 Nhf5 Re1 a5 bxc5 bxc5 Bc4 Ra8 Rb1 Nh4 Rdb2 Nc6 Rb7 Nxe5 Bxe6 Kxf6 Bd5 Nf5 R7b6+ Kg7 Bxa8 Rxa8 R6b3 Nd4 Rb7 Nxd3 Rd1 Ne2+ Kh2 Ndf4 Rdd7 Rf8 Ra7 c4 Rxa5 c3 Rc5 Ne6 Rc4 Ra8 a4 Rb8 a5 Rb2 a6 c2","clock":"5+8"},"puzzle":{"id":"20yWT","rating":1859,"plays":551,"initialPly":93,"solution":["a6a7","b2a2","c4c2","a2a7","d7a7"],"themes":["endgame","long","advantage","advancedPawn"]}},{"game":{"id":"0lwkiJbZ","perf":{"key":"classical","name":"Classical"},"rated":true,"players":[{"userId":"nirdosh","name":"nirdosh (2035)","color":"white"},{"userId":"burn_it_down","name":"burn_it_down (2139)","color":"black"}],"pgn":"d4 Nf6 Nf3 c5 e3 g6 Bd3 Bg7 c3 Qc7 O-O O-O Nbd2 d6 Qe2 Nbd7 e4 cxd4 cxd4 e5 dxe5 dxe5 b3 Nc5 Bb2 Nh5 g3 Bh3 Rfc1 Qd6 Bc4 Rac8 Bd5 Qb8 Ng5 Bd7 Ba3 b6 Rc2 h6 Ngf3 Rfe8 Rac1 Ne6 Nc4 Bb5 Qe3 Bxc4 Bxc4 Nd4 Nxd4 exd4 Qd3 Rcd8 f4 Nf6 e5 Ng4 Qxg6 Ne3 Bxf7+ Kh8 Rc7 Qa8 Qxg7+ Kxg7 Bd5+ Kg6 Bxa8 Rxa8 Rd7 Rad8 Rc6+ Kf5 Rcd6 Rxd7 Rxd7 Ke4 Bb2 Nc2 Kf2 d3 Bc1 Nd4 h3","clock":"15+15"},"puzzle":{"id":"7H5EV","rating":1852,"plays":410,"initialPly":84,"solution":["e8c8","d7d4","e4d4"],"themes":["endgame","short","advantage"]}},{"game":{"id":"eWGRX5AI","perf":{"key":"rapid","name":"Rapid"},"rated":true,"players":[{"userId":"sacalot","name":"sacalot (2151)","color":"white"},{"userId":"landitirana","name":"landitirana (1809)","color":"black"}],"pgn":"e4 e5 Nf3 Nc6 d4 exd4 Bc4 Nf6 O-O Nxe4 Re1 d5 Bxd5 Qxd5 Nc3 Qd8 Rxe4+ Be6 Nxd4 Nxd4 Rxd4 Qf6 Ne4 Qe5 f4 Qf5 Ng3 Qa5 Bd2 Qb6 Be3 Bc5 f5 Bd5 Rxd5 Bxe3+ Kh1 O-O Rd3 Rfe8 Qf3 Qxb2 Rf1 Bd4 Nh5 Bf6 Rb3 Qd4 Rxb7 Re3 Nxf6+ gxf6 Qf2 Rae8 Rxc7 Qe5 Rc4 Re1 Rf4 Qa1 h3","clock":"10+0"},"puzzle":{"id":"1qUth","rating":1556,"plays":2661,"initialPly":60,"solution":["e1f1","f2f1","e8e1","f1e1","a1e1"],"themes":["endgame","master","advantage","fork","long","pin"]}}]}
|
||||
''';
|
||||
when(() => mockClient.get(
|
||||
Uri.parse('$kLichessHost/api/puzzle/batch/mix?nb=30'),
|
||||
)).thenAnswer((_) => mockResponse(batchResponse, 200));
|
||||
when(
|
||||
() => mockClient.get(
|
||||
Uri.parse('$kLichessHost/api/puzzle/batch/mix?nb=30'),
|
||||
),
|
||||
).thenAnswer((_) => mockResponse(batchResponse, 200));
|
||||
|
||||
final result = await repo.selectBatch();
|
||||
|
||||
|
||||
@@ -26,12 +26,15 @@ void main() {
|
||||
final mockLogger = MockLogger();
|
||||
final mockClient = MockClient();
|
||||
|
||||
final puzzleRepo = PuzzleRepository(mockLogger,
|
||||
apiClient: ApiClient(mockLogger, mockClient));
|
||||
final puzzleRepo = PuzzleRepository(
|
||||
mockLogger,
|
||||
apiClient: ApiClient(mockLogger, mockClient),
|
||||
);
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(
|
||||
Uri.parse('$kLichessHost/api/puzzle/batch/mix?nb=30'));
|
||||
Uri.parse('$kLichessHost/api/puzzle/batch/mix?nb=30'),
|
||||
);
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
@@ -46,14 +49,19 @@ void main() {
|
||||
final db = PuzzleLocalDB(sharedPreferences);
|
||||
final service = PuzzleService(mockLogger, db: db, repository: puzzleRepo);
|
||||
|
||||
when(() => mockClient.get(
|
||||
Uri.parse('$kLichessHost/api/puzzle/batch/mix?nb=30'),
|
||||
)).thenAnswer((_) => mockResponse(batchOf3, 200));
|
||||
when(
|
||||
() => mockClient.get(
|
||||
Uri.parse('$kLichessHost/api/puzzle/batch/mix?nb=30'),
|
||||
),
|
||||
).thenAnswer((_) => mockResponse(batchOf3, 200));
|
||||
|
||||
final puzzle = await service.nextPuzzle();
|
||||
expect(puzzle?.puzzle.id, equals(const PuzzleId('20yWT')));
|
||||
verify(() => mockClient.get(
|
||||
Uri.parse('$kLichessHost/api/puzzle/batch/mix?nb=30'))).called(1);
|
||||
verify(
|
||||
() => mockClient.get(
|
||||
Uri.parse('$kLichessHost/api/puzzle/batch/mix?nb=30'),
|
||||
),
|
||||
).called(1);
|
||||
expect(db.fetch()?.solved, equals(IList(const [])));
|
||||
expect(db.fetch()?.unsolved.length, equals(3));
|
||||
});
|
||||
@@ -65,8 +73,12 @@ void main() {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
|
||||
final db = PuzzleLocalDB(sharedPreferences);
|
||||
final service = PuzzleService(mockLogger,
|
||||
db: db, repository: puzzleRepo, localQueueLength: 1);
|
||||
final service = PuzzleService(
|
||||
mockLogger,
|
||||
db: db,
|
||||
repository: puzzleRepo,
|
||||
localQueueLength: 1,
|
||||
);
|
||||
|
||||
final puzzle = await service.nextPuzzle();
|
||||
expect(puzzle?.puzzle.id, equals(const PuzzleId('pId3')));
|
||||
@@ -81,18 +93,26 @@ void main() {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
|
||||
final db = PuzzleLocalDB(sharedPreferences);
|
||||
final service = PuzzleService(mockLogger,
|
||||
db: db, repository: puzzleRepo, localQueueLength: 2);
|
||||
final service = PuzzleService(
|
||||
mockLogger,
|
||||
db: db,
|
||||
repository: puzzleRepo,
|
||||
localQueueLength: 2,
|
||||
);
|
||||
|
||||
// will fetch only 1 since localQueueLength is 2
|
||||
when(() => mockClient.get(
|
||||
Uri.parse('$kLichessHost/api/puzzle/batch/mix?nb=1'),
|
||||
)).thenAnswer((_) => mockResponse(batchOf1, 200));
|
||||
when(
|
||||
() => mockClient.get(
|
||||
Uri.parse('$kLichessHost/api/puzzle/batch/mix?nb=1'),
|
||||
),
|
||||
).thenAnswer((_) => mockResponse(batchOf1, 200));
|
||||
|
||||
final puzzle = await service.nextPuzzle();
|
||||
expect(puzzle?.puzzle.id, equals(const PuzzleId('pId3')));
|
||||
verify(() => mockClient
|
||||
.get(Uri.parse('$kLichessHost/api/puzzle/batch/mix?nb=1'))).called(1);
|
||||
verify(
|
||||
() => mockClient
|
||||
.get(Uri.parse('$kLichessHost/api/puzzle/batch/mix?nb=1')),
|
||||
).called(1);
|
||||
expect(db.fetch()?.unsolved.length, equals(2));
|
||||
});
|
||||
|
||||
@@ -105,8 +125,12 @@ void main() {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
|
||||
final db = PuzzleLocalDB(sharedPreferences);
|
||||
final service = PuzzleService(mockLogger,
|
||||
db: db, repository: puzzleRepo, localQueueLength: 1);
|
||||
final service = PuzzleService(
|
||||
mockLogger,
|
||||
db: db,
|
||||
repository: puzzleRepo,
|
||||
localQueueLength: 1,
|
||||
);
|
||||
|
||||
final puzzle = await service.nextPuzzle();
|
||||
expect(puzzle?.puzzle.id, equals(const PuzzleId('pId3')));
|
||||
@@ -124,17 +148,25 @@ void main() {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
|
||||
final db = PuzzleLocalDB(sharedPreferences);
|
||||
final service = PuzzleService(mockLogger,
|
||||
db: db, repository: puzzleRepo, localQueueLength: 1);
|
||||
final service = PuzzleService(
|
||||
mockLogger,
|
||||
db: db,
|
||||
repository: puzzleRepo,
|
||||
localQueueLength: 1,
|
||||
);
|
||||
|
||||
when(() => mockClient.get(
|
||||
Uri.parse('$kLichessHost/api/puzzle/batch/mix?nb=1'),
|
||||
)).thenAnswer((_) => mockResponse(batchOf1, 200));
|
||||
when(
|
||||
() => mockClient.get(
|
||||
Uri.parse('$kLichessHost/api/puzzle/batch/mix?nb=1'),
|
||||
),
|
||||
).thenAnswer((_) => mockResponse(batchOf1, 200));
|
||||
|
||||
final puzzle = await service.nextPuzzle(userId: 'testUserId');
|
||||
expect(puzzle?.puzzle.id, equals(const PuzzleId('20yWT')));
|
||||
verify(() => mockClient
|
||||
.get(Uri.parse('$kLichessHost/api/puzzle/batch/mix?nb=1'))).called(1);
|
||||
verify(
|
||||
() => mockClient
|
||||
.get(Uri.parse('$kLichessHost/api/puzzle/batch/mix?nb=1')),
|
||||
).called(1);
|
||||
|
||||
expect(db.fetch(userId: 'testUserId')?.unsolved.length, equals(1));
|
||||
});
|
||||
@@ -146,17 +178,26 @@ void main() {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
|
||||
final db = PuzzleLocalDB(sharedPreferences);
|
||||
final service = PuzzleService(mockLogger,
|
||||
db: db, repository: puzzleRepo, localQueueLength: 1);
|
||||
final service = PuzzleService(
|
||||
mockLogger,
|
||||
db: db,
|
||||
repository: puzzleRepo,
|
||||
localQueueLength: 1,
|
||||
);
|
||||
|
||||
when(() => mockClient.get(
|
||||
Uri.parse('$kLichessHost/api/puzzle/batch/opening?nb=1'),
|
||||
)).thenAnswer((_) => mockResponse(batchOf1, 200));
|
||||
when(
|
||||
() => mockClient.get(
|
||||
Uri.parse('$kLichessHost/api/puzzle/batch/opening?nb=1'),
|
||||
),
|
||||
).thenAnswer((_) => mockResponse(batchOf1, 200));
|
||||
|
||||
final puzzle = await service.nextPuzzle(angle: PuzzleTheme.opening);
|
||||
expect(puzzle?.puzzle.id, equals(const PuzzleId('20yWT')));
|
||||
verify(() => mockClient.get(
|
||||
Uri.parse('$kLichessHost/api/puzzle/batch/opening?nb=1'))).called(1);
|
||||
verify(
|
||||
() => mockClient.get(
|
||||
Uri.parse('$kLichessHost/api/puzzle/batch/opening?nb=1'),
|
||||
),
|
||||
).called(1);
|
||||
|
||||
expect(db.fetch(angle: PuzzleTheme.opening)?.unsolved.length, equals(1));
|
||||
});
|
||||
@@ -168,21 +209,32 @@ void main() {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
|
||||
final db = PuzzleLocalDB(sharedPreferences);
|
||||
final service = PuzzleService(mockLogger,
|
||||
db: db, repository: puzzleRepo, localQueueLength: 1);
|
||||
final service = PuzzleService(
|
||||
mockLogger,
|
||||
db: db,
|
||||
repository: puzzleRepo,
|
||||
localQueueLength: 1,
|
||||
);
|
||||
|
||||
when(() => mockClient.post(
|
||||
Uri.parse('$kLichessHost/api/puzzle/batch/mix?nb=1'),
|
||||
headers: any(
|
||||
named: 'headers',
|
||||
that: sameHeaders({'Content-type': 'application/json'})),
|
||||
body:
|
||||
'{"solutions":[{"id":{"value":"pId3"},"win":true,"rated":true}]}',
|
||||
)).thenAnswer((_) => mockResponse(batchOf1, 200));
|
||||
when(
|
||||
() => mockClient.post(
|
||||
Uri.parse('$kLichessHost/api/puzzle/batch/mix?nb=1'),
|
||||
headers: any(
|
||||
named: 'headers',
|
||||
that: sameHeaders({'Content-type': 'application/json'}),
|
||||
),
|
||||
body:
|
||||
'{"solutions":[{"id":{"value":"pId3"},"win":true,"rated":true}]}',
|
||||
),
|
||||
).thenAnswer((_) => mockResponse(batchOf1, 200));
|
||||
|
||||
await service.solve(
|
||||
solution: const PuzzleSolution(
|
||||
id: PuzzleId('pId3'), win: true, rated: true));
|
||||
solution: const PuzzleSolution(
|
||||
id: PuzzleId('pId3'),
|
||||
win: true,
|
||||
rated: true,
|
||||
),
|
||||
);
|
||||
final data = db.fetch();
|
||||
expect(data?.solved, equals(IList(const [])));
|
||||
expect(data?.unsolved[0].puzzle.id, equals(const PuzzleId('20yWT')));
|
||||
@@ -195,30 +247,40 @@ void main() {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
|
||||
final db = PuzzleLocalDB(sharedPreferences);
|
||||
final service = PuzzleService(mockLogger,
|
||||
db: db, repository: puzzleRepo, localQueueLength: 1);
|
||||
final service = PuzzleService(
|
||||
mockLogger,
|
||||
db: db,
|
||||
repository: puzzleRepo,
|
||||
localQueueLength: 1,
|
||||
);
|
||||
|
||||
when(() => mockClient.post(
|
||||
Uri.parse('$kLichessHost/api/puzzle/batch/mix?nb=1'),
|
||||
headers: any(
|
||||
named: 'headers',
|
||||
that: sameHeaders({'Content-type': 'application/json'})),
|
||||
body:
|
||||
'{"solutions":[{"id":{"value":"pId3"},"win":true,"rated":true}]}',
|
||||
)).thenAnswer((_) => Future.error(const SocketException('offline')));
|
||||
when(
|
||||
() => mockClient.post(
|
||||
Uri.parse('$kLichessHost/api/puzzle/batch/mix?nb=1'),
|
||||
headers: any(
|
||||
named: 'headers',
|
||||
that: sameHeaders({'Content-type': 'application/json'}),
|
||||
),
|
||||
body:
|
||||
'{"solutions":[{"id":{"value":"pId3"},"win":true,"rated":true}]}',
|
||||
),
|
||||
).thenAnswer((_) => Future.error(const SocketException('offline')));
|
||||
|
||||
const solution =
|
||||
PuzzleSolution(id: PuzzleId('pId3'), win: true, rated: true);
|
||||
await service.solve(solution: solution);
|
||||
|
||||
verify(() => mockClient.post(
|
||||
Uri.parse('$kLichessHost/api/puzzle/batch/mix?nb=1'),
|
||||
headers: any(
|
||||
named: 'headers',
|
||||
that: sameHeaders({'Content-type': 'application/json'})),
|
||||
body:
|
||||
'{"solutions":[{"id":{"value":"pId3"},"win":true,"rated":true}]}',
|
||||
)).called(1);
|
||||
verify(
|
||||
() => mockClient.post(
|
||||
Uri.parse('$kLichessHost/api/puzzle/batch/mix?nb=1'),
|
||||
headers: any(
|
||||
named: 'headers',
|
||||
that: sameHeaders({'Content-type': 'application/json'}),
|
||||
),
|
||||
body:
|
||||
'{"solutions":[{"id":{"value":"pId3"},"win":true,"rated":true}]}',
|
||||
),
|
||||
).called(1);
|
||||
|
||||
final data = db.fetch();
|
||||
expect(data?.solved, equals(IList(const [solution])));
|
||||
@@ -235,26 +297,28 @@ const batchOf1 = '''
|
||||
{"puzzles":[{"game":{"id":"PrlkCqOv","perf":{"key":"rapid","name":"Rapid"},"rated":true,"players":[{"userId":"silverjo","name":"silverjo (1777)","color":"white"},{"userId":"robyarchitetto","name":"Robyarchitetto (1742)","color":"black"}],"pgn":"e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2 Bb7 Bh6 d5 e5 d4 Bxg7 Kxg7 Qf4 Bxf3 Qxf3 dxc3 Nxc3 Nac6 Qf6+ Kg8 Rd1 Nd4 O-O c5 Ne4 Nef5 Rd2 Qxf6 Nxf6+ Kg7 Re1 h5 h3 Rad8 b4 Nh4 Re3 Nhf5 Re1 a5 bxc5 bxc5 Bc4 Ra8 Rb1 Nh4 Rdb2 Nc6 Rb7 Nxe5 Bxe6 Kxf6 Bd5 Nf5 R7b6+ Kg7 Bxa8 Rxa8 R6b3 Nd4 Rb7 Nxd3 Rd1 Ne2+ Kh2 Ndf4 Rdd7 Rf8 Ra7 c4 Rxa5 c3 Rc5 Ne6 Rc4 Ra8 a4 Rb8 a5 Rb2 a6 c2","clock":"5+8"},"puzzle":{"id":"20yWT","rating":1859,"plays":551,"initialPly":93,"solution":["a6a7","b2a2","c4c2","a2a7","d7a7"],"themes":["endgame","long","advantage","advancedPawn"]}}]}
|
||||
''';
|
||||
|
||||
final String oneSavedPuzzle = jsonEncode(PuzzleLocalData(
|
||||
final String oneSavedPuzzle = jsonEncode(
|
||||
PuzzleLocalData(
|
||||
solved: IList(const []),
|
||||
unsolved: IList([
|
||||
Puzzle(
|
||||
puzzle: PuzzleData(
|
||||
id: const PuzzleId('pId3'),
|
||||
rating: 1988,
|
||||
plays: 5,
|
||||
initialPly: 23,
|
||||
solution: IList(const ['a6a7', 'b2a2', 'c4c2']),
|
||||
themes: ISet(const ['endgame', 'advantage'])),
|
||||
id: const PuzzleId('pId3'),
|
||||
rating: 1988,
|
||||
plays: 5,
|
||||
initialPly: 23,
|
||||
solution: IList(const ['a6a7', 'b2a2', 'c4c2']),
|
||||
themes: ISet(const ['endgame', 'advantage']),
|
||||
),
|
||||
game: const PuzzleGame(
|
||||
id: GameId('PrlkCqOv'),
|
||||
perf: Perf.blitz,
|
||||
rated: true,
|
||||
white:
|
||||
PuzzlePlayer(side: Side.white, userId: 'user1', name: 'user1'),
|
||||
black:
|
||||
PuzzlePlayer(side: Side.black, userId: 'user2', name: 'user2'),
|
||||
pgn:
|
||||
'e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2'),
|
||||
id: GameId('PrlkCqOv'),
|
||||
perf: Perf.blitz,
|
||||
rated: true,
|
||||
white: PuzzlePlayer(side: Side.white, userId: 'user1', name: 'user1'),
|
||||
black: PuzzlePlayer(side: Side.black, userId: 'user2', name: 'user2'),
|
||||
pgn: 'e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2',
|
||||
),
|
||||
),
|
||||
])).toJson());
|
||||
]),
|
||||
).toJson(),
|
||||
);
|
||||
|
||||
@@ -8,31 +8,37 @@ import '../../../utils.dart';
|
||||
|
||||
void main() {
|
||||
group('SettingsScreen', () {
|
||||
testWidgets('meets accessibility guidelines', (WidgetTester tester) async {
|
||||
final SemanticsHandle handle = tester.ensureSemantics();
|
||||
final app = await buildTestApp(
|
||||
tester,
|
||||
home: Consumer(builder: (context, ref, _) {
|
||||
return const SettingsScreen();
|
||||
}),
|
||||
);
|
||||
testWidgets(
|
||||
'meets accessibility guidelines',
|
||||
(WidgetTester tester) async {
|
||||
final SemanticsHandle handle = tester.ensureSemantics();
|
||||
final app = await buildTestApp(
|
||||
tester,
|
||||
home: Consumer(
|
||||
builder: (context, ref, _) {
|
||||
return const SettingsScreen();
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
settingsRepositoryProvider
|
||||
.overrideWithValue(FakeSettingsRepository()),
|
||||
],
|
||||
child: app,
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
settingsRepositoryProvider
|
||||
.overrideWithValue(FakeSettingsRepository()),
|
||||
],
|
||||
child: app,
|
||||
),
|
||||
);
|
||||
|
||||
await meetsTapTargetGuideline(tester);
|
||||
await meetsTapTargetGuideline(tester);
|
||||
|
||||
await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
|
||||
await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
|
||||
|
||||
await expectLater(tester, meetsGuideline(textContrastGuideline));
|
||||
handle.dispose();
|
||||
}, variant: kPlatformVariant);
|
||||
await expectLater(tester, meetsGuideline(textContrastGuideline));
|
||||
handle.dispose();
|
||||
},
|
||||
variant: kPlatformVariant,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -27,47 +27,55 @@ void main() {
|
||||
when(() => mockApiClient.stream(Uri.parse('$kLichessHost/api/tv/feed')))
|
||||
.thenAnswer((_) async {
|
||||
return http.StreamedResponse(
|
||||
Stream.fromIterable([
|
||||
utf8.encode(
|
||||
'{ "t": "featured", "d": { "id": "qVSOPtMc", "orientation": "black", "fen": "rnbqk1r1/ppp1ppbp/8/N2p2p1/8/1PQPP3/P1P2PPn/R1B1K1NR", "players": [{ "color": "white", "user": { "name": "lizen9", "id": "lizen9", "title": "GM" }, "rating": 2531 }, { "color": "black", "user": { "name": "lizen29", "id": "lizen29", "title": "WGM" }, "rating": 2594 }]}}'),
|
||||
utf8.encode(
|
||||
'{ "t": "fen", "d": { "lm": "g1f3", "fen": "rnbqk1r1/ppp1ppbp/8/N2p2p1/8/1PQPP3/P1P2PPn/R1B1K1NR", "wc": 123124, "bc": 103235 }}'),
|
||||
utf8.encode(
|
||||
'{ "t": "fen", "d": { "lm": "g1f3", "fen": "rnbqk1r1/ppp1ppbp/8/N2p2p1/8/1QPP3/P1P2PPn/R1B1K1NR", "wc": 123120, "bc": 103230 }}'),
|
||||
]),
|
||||
200);
|
||||
Stream.fromIterable([
|
||||
utf8.encode(
|
||||
'{ "t": "featured", "d": { "id": "qVSOPtMc", "orientation": "black", "fen": "rnbqk1r1/ppp1ppbp/8/N2p2p1/8/1PQPP3/P1P2PPn/R1B1K1NR", "players": [{ "color": "white", "user": { "name": "lizen9", "id": "lizen9", "title": "GM" }, "rating": 2531 }, { "color": "black", "user": { "name": "lizen29", "id": "lizen29", "title": "WGM" }, "rating": 2594 }]}}',
|
||||
),
|
||||
utf8.encode(
|
||||
'{ "t": "fen", "d": { "lm": "g1f3", "fen": "rnbqk1r1/ppp1ppbp/8/N2p2p1/8/1PQPP3/P1P2PPn/R1B1K1NR", "wc": 123124, "bc": 103235 }}',
|
||||
),
|
||||
utf8.encode(
|
||||
'{ "t": "fen", "d": { "lm": "g1f3", "fen": "rnbqk1r1/ppp1ppbp/8/N2p2p1/8/1QPP3/P1P2PPn/R1B1K1NR", "wc": 123120, "bc": 103230 }}',
|
||||
),
|
||||
]),
|
||||
200,
|
||||
);
|
||||
});
|
||||
|
||||
final stream = repo.tvFeed();
|
||||
|
||||
expect(
|
||||
stream,
|
||||
emitsInOrder([
|
||||
predicate((value) => value is TvFeaturedEvent),
|
||||
predicate((value) => value is TvFenEvent),
|
||||
predicate((value) => value is TvFenEvent),
|
||||
]));
|
||||
stream,
|
||||
emitsInOrder([
|
||||
predicate((value) => value is TvFeaturedEvent),
|
||||
predicate((value) => value is TvFenEvent),
|
||||
predicate((value) => value is TvFenEvent),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('filter out unsupported types of events', () async {
|
||||
when(() => mockApiClient.stream(Uri.parse('$kLichessHost/api/tv/feed')))
|
||||
.thenAnswer((_) async {
|
||||
return http.StreamedResponse(
|
||||
Stream.fromIterable([
|
||||
utf8.encode(
|
||||
'{ "t": "fen", "d": { "lm": "g1f3", "fen": "rnbqk1r1/ppp1ppbp/8/N2p2p1/8/1QPP3/P1P2PPn/R1B1K1NR", "wc": 123120, "bc": 103230 }}'),
|
||||
utf8.encode('{ "t": "oops", "d": {}}'),
|
||||
]),
|
||||
200);
|
||||
Stream.fromIterable([
|
||||
utf8.encode(
|
||||
'{ "t": "fen", "d": { "lm": "g1f3", "fen": "rnbqk1r1/ppp1ppbp/8/N2p2p1/8/1QPP3/P1P2PPn/R1B1K1NR", "wc": 123120, "bc": 103230 }}',
|
||||
),
|
||||
utf8.encode('{ "t": "oops", "d": {}}'),
|
||||
]),
|
||||
200,
|
||||
);
|
||||
});
|
||||
|
||||
final stream = repo.tvFeed();
|
||||
|
||||
expect(
|
||||
stream,
|
||||
emitsInOrder([
|
||||
predicate((value) => value is TvFenEvent),
|
||||
]));
|
||||
stream,
|
||||
emitsInOrder([
|
||||
predicate((value) => value is TvFenEvent),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -36,10 +36,12 @@ void main() {
|
||||
}
|
||||
}
|
||||
''';
|
||||
when(() => mockApiClient
|
||||
.get(Uri.parse('$kLichessHost/api/user/$testUserId')))
|
||||
.thenAnswer((_) async =>
|
||||
Result.value(http.Response(testUserResponseMinimal, 200)));
|
||||
when(
|
||||
() =>
|
||||
mockApiClient.get(Uri.parse('$kLichessHost/api/user/$testUserId')),
|
||||
).thenAnswer(
|
||||
(_) async => Result.value(http.Response(testUserResponseMinimal, 200)),
|
||||
);
|
||||
|
||||
final result = await repo.getUser(testUserId);
|
||||
|
||||
@@ -86,10 +88,12 @@ void main() {
|
||||
}
|
||||
}
|
||||
''';
|
||||
when(() => mockApiClient
|
||||
.get(Uri.parse('$kLichessHost/api/user/$testUserId')))
|
||||
.thenAnswer(
|
||||
(_) async => Result.value(http.Response(testUserResponse, 200)));
|
||||
when(
|
||||
() =>
|
||||
mockApiClient.get(Uri.parse('$kLichessHost/api/user/$testUserId')),
|
||||
).thenAnswer(
|
||||
(_) async => Result.value(http.Response(testUserResponse, 200)),
|
||||
);
|
||||
|
||||
final result = await repo.getUser(testUserId);
|
||||
|
||||
@@ -133,7 +137,8 @@ void main() {
|
||||
}
|
||||
''';
|
||||
when(() => mockApiClient.get(Uri.parse(uriString))).thenAnswer(
|
||||
(_) async => Result.value(http.Response(responseMinimal, 200)));
|
||||
(_) async => Result.value(http.Response(responseMinimal, 200)),
|
||||
);
|
||||
|
||||
final result = await repo.getUserPerfStats(testUserId, testPerf);
|
||||
|
||||
@@ -379,7 +384,8 @@ void main() {
|
||||
}
|
||||
''';
|
||||
when(() => mockApiClient.get(Uri.parse(uriString))).thenAnswer(
|
||||
(_) async => Result.value(http.Response(responseFull, 200)));
|
||||
(_) async => Result.value(http.Response(responseFull, 200)),
|
||||
);
|
||||
|
||||
final result = await repo.getUserPerfStats(testUserId, testPerf);
|
||||
|
||||
@@ -390,9 +396,11 @@ void main() {
|
||||
group('UserRepository.getUsersStatusTask', () {
|
||||
test('json read, minimal example', () async {
|
||||
final ids = ['maia1', 'maia5', 'maia9'];
|
||||
when(() => mockApiClient.get(
|
||||
Uri.parse('$kLichessHost/api/users/status?ids=${ids.join(',')}')))
|
||||
.thenAnswer((_) async => Result.value(http.Response('[]', 200)));
|
||||
when(
|
||||
() => mockApiClient.get(
|
||||
Uri.parse('$kLichessHost/api/users/status?ids=${ids.join(',')}'),
|
||||
),
|
||||
).thenAnswer((_) async => Result.value(http.Response('[]', 200)));
|
||||
|
||||
final result = await repo.getUsersStatus(ids);
|
||||
|
||||
@@ -421,9 +429,11 @@ void main() {
|
||||
]
|
||||
''';
|
||||
final ids = ['maia1', 'maia5', 'maia9'];
|
||||
when(() => mockApiClient.get(
|
||||
Uri.parse('$kLichessHost/api/users/status?ids=${ids.join(',')}')))
|
||||
.thenAnswer((_) async => Result.value(http.Response(response, 200)));
|
||||
when(
|
||||
() => mockApiClient.get(
|
||||
Uri.parse('$kLichessHost/api/users/status?ids=${ids.join(',')}'),
|
||||
),
|
||||
).thenAnswer((_) async => Result.value(http.Response(response, 200)));
|
||||
|
||||
final result = await repo.getUsersStatus(['maia1', 'maia5', 'maia9']);
|
||||
|
||||
|
||||
@@ -28,75 +28,98 @@ void main() {
|
||||
.thenAnswer((_) => mockResponse(testRes, 200));
|
||||
});
|
||||
|
||||
testWidgets('meets accessibility guidelines', (WidgetTester tester) async {
|
||||
final SemanticsHandle handle = tester.ensureSemantics();
|
||||
testWidgets(
|
||||
'meets accessibility guidelines',
|
||||
(WidgetTester tester) async {
|
||||
final SemanticsHandle handle = tester.ensureSemantics();
|
||||
|
||||
final app = await buildTestApp(
|
||||
tester,
|
||||
home: Consumer(builder: (context, ref, _) {
|
||||
return LeaderboardScreen(leaderboard: testLeaderboard);
|
||||
}),
|
||||
);
|
||||
await tester.pumpWidget(ProviderScope(child: app));
|
||||
|
||||
await tester.pump();
|
||||
|
||||
await meetsTapTargetGuideline(tester);
|
||||
|
||||
await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
|
||||
|
||||
if (debugDefaultTargetPlatformOverride == TargetPlatform.android) {
|
||||
await expectLater(tester, meetsGuideline(textContrastGuideline));
|
||||
}
|
||||
handle.dispose();
|
||||
}, variant: kPlatformVariant);
|
||||
|
||||
testWidgets('leaderbord widget test', (WidgetTester tester) async {
|
||||
final SemanticsHandle handle = tester.ensureSemantics();
|
||||
|
||||
final app = await buildTestApp(tester,
|
||||
final app = await buildTestApp(
|
||||
tester,
|
||||
home: Consumer(
|
||||
builder: (context, ref, _) =>
|
||||
Column(children: [LeaderboardWidget()])));
|
||||
builder: (context, ref, _) {
|
||||
return LeaderboardScreen(leaderboard: testLeaderboard);
|
||||
},
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(ProviderScope(child: app));
|
||||
|
||||
await tester.pumpWidget(ProviderScope(overrides: [
|
||||
apiClientProvider.overrideWithValue(ApiClient(mockLogger, mockClient))
|
||||
], child: app));
|
||||
await tester.pump();
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
await meetsTapTargetGuideline(tester);
|
||||
|
||||
// 11 leaderboard rows with username test
|
||||
expect(find.widgetWithText(Row, 'test'), findsNWidgets(11));
|
||||
await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
|
||||
|
||||
await meetsTapTargetGuideline(tester);
|
||||
if (debugDefaultTargetPlatformOverride == TargetPlatform.android) {
|
||||
await expectLater(tester, meetsGuideline(textContrastGuideline));
|
||||
}
|
||||
handle.dispose();
|
||||
},
|
||||
variant: kPlatformVariant,
|
||||
);
|
||||
|
||||
await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
|
||||
testWidgets(
|
||||
'leaderbord widget test',
|
||||
(WidgetTester tester) async {
|
||||
final SemanticsHandle handle = tester.ensureSemantics();
|
||||
|
||||
if (debugDefaultTargetPlatformOverride == TargetPlatform.android) {
|
||||
await expectLater(tester, meetsGuideline(textContrastGuideline));
|
||||
}
|
||||
handle.dispose();
|
||||
}, variant: kPlatformVariant);
|
||||
final app = await buildTestApp(
|
||||
tester,
|
||||
home: Consumer(
|
||||
builder: (context, ref, _) => Column(children: [LeaderboardWidget()]),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
apiClientProvider
|
||||
.overrideWithValue(ApiClient(mockLogger, mockClient))
|
||||
],
|
||||
child: app,
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
|
||||
// 11 leaderboard rows with username test
|
||||
expect(find.widgetWithText(Row, 'test'), findsNWidgets(11));
|
||||
|
||||
await meetsTapTargetGuideline(tester);
|
||||
|
||||
await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
|
||||
|
||||
if (debugDefaultTargetPlatformOverride == TargetPlatform.android) {
|
||||
await expectLater(tester, meetsGuideline(textContrastGuideline));
|
||||
}
|
||||
handle.dispose();
|
||||
},
|
||||
variant: kPlatformVariant,
|
||||
);
|
||||
}
|
||||
|
||||
final testLeaderboard = Leaderboard(
|
||||
bullet: _fakeList,
|
||||
blitz: _fakeList,
|
||||
rapid: _fakeList,
|
||||
classical: _fakeList,
|
||||
ultrabullet: _fakeList,
|
||||
crazyhouse: _fakeList,
|
||||
chess960: _fakeList,
|
||||
kingOfThehill: _fakeList,
|
||||
threeCheck: _fakeList,
|
||||
antichess: _fakeList,
|
||||
atomic: _fakeList,
|
||||
horde: _fakeList,
|
||||
racingKings: _fakeList);
|
||||
bullet: _fakeList,
|
||||
blitz: _fakeList,
|
||||
rapid: _fakeList,
|
||||
classical: _fakeList,
|
||||
ultrabullet: _fakeList,
|
||||
crazyhouse: _fakeList,
|
||||
chess960: _fakeList,
|
||||
kingOfThehill: _fakeList,
|
||||
threeCheck: _fakeList,
|
||||
antichess: _fakeList,
|
||||
atomic: _fakeList,
|
||||
horde: _fakeList,
|
||||
racingKings: _fakeList,
|
||||
);
|
||||
|
||||
final _fakeList = [
|
||||
const LeaderboardUser(
|
||||
id: 'test', username: 'test', rating: 1000, progress: 10)
|
||||
id: 'test',
|
||||
username: 'test',
|
||||
rating: 1000,
|
||||
progress: 10,
|
||||
)
|
||||
];
|
||||
|
||||
const testRes = '''
|
||||
|
||||
@@ -33,69 +33,107 @@ void main() {
|
||||
});
|
||||
|
||||
group('PerfStatsScreen', () {
|
||||
testWidgets('meets accessibility guidelines', (WidgetTester tester) async {
|
||||
final SemanticsHandle handle = tester.ensureSemantics();
|
||||
testWidgets(
|
||||
'meets accessibility guidelines',
|
||||
(WidgetTester tester) async {
|
||||
final SemanticsHandle handle = tester.ensureSemantics();
|
||||
|
||||
final app =
|
||||
await buildTestApp(tester, home: Consumer(builder: (context, ref, _) {
|
||||
return PerfStatsScreen(
|
||||
user: fakeUser, perf: testPerf, loggedInUser: null);
|
||||
}));
|
||||
final app = await buildTestApp(
|
||||
tester,
|
||||
home: Consumer(
|
||||
builder: (context, ref, _) {
|
||||
return PerfStatsScreen(
|
||||
user: fakeUser,
|
||||
perf: testPerf,
|
||||
loggedInUser: null,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(ProviderScope(overrides: [
|
||||
// Don't need a logged in user to test this screen.
|
||||
authRepositoryProvider.overrideWithValue(FakeAuthRepository(null)),
|
||||
apiClientProvider.overrideWithValue(ApiClient(mockLogger, mockClient)),
|
||||
], child: app));
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
// Don't need a logged in user to test this screen.
|
||||
authRepositoryProvider
|
||||
.overrideWithValue(FakeAuthRepository(null)),
|
||||
apiClientProvider
|
||||
.overrideWithValue(ApiClient(mockLogger, mockClient)),
|
||||
],
|
||||
child: app,
|
||||
),
|
||||
);
|
||||
|
||||
// wait for auth state and perf stats
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
// wait for auth state and perf stats
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
|
||||
await meetsTapTargetGuideline(tester);
|
||||
await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
|
||||
await meetsTapTargetGuideline(tester);
|
||||
await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
|
||||
|
||||
if (debugDefaultTargetPlatformOverride == TargetPlatform.android) {
|
||||
await expectLater(tester, meetsGuideline(textContrastGuideline));
|
||||
}
|
||||
handle.dispose();
|
||||
}, variant: kPlatformVariant);
|
||||
if (debugDefaultTargetPlatformOverride == TargetPlatform.android) {
|
||||
await expectLater(tester, meetsGuideline(textContrastGuideline));
|
||||
}
|
||||
handle.dispose();
|
||||
},
|
||||
variant: kPlatformVariant,
|
||||
);
|
||||
|
||||
testWidgets('screen loads, required stats are shown',
|
||||
(WidgetTester tester) async {
|
||||
final app =
|
||||
await buildTestApp(tester, home: Consumer(builder: (context, ref, _) {
|
||||
return PerfStatsScreen(
|
||||
user: fakeUser, perf: testPerf, loggedInUser: null);
|
||||
}));
|
||||
testWidgets(
|
||||
'screen loads, required stats are shown',
|
||||
(WidgetTester tester) async {
|
||||
final app = await buildTestApp(
|
||||
tester,
|
||||
home: Consumer(
|
||||
builder: (context, ref, _) {
|
||||
return PerfStatsScreen(
|
||||
user: fakeUser,
|
||||
perf: testPerf,
|
||||
loggedInUser: null,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(ProviderScope(overrides: [
|
||||
// Don't need a logged in user to test this screen.
|
||||
authRepositoryProvider.overrideWithValue(FakeAuthRepository(null)),
|
||||
apiClientProvider.overrideWithValue(ApiClient(mockLogger, mockClient)),
|
||||
], child: app));
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
// Don't need a logged in user to test this screen.
|
||||
authRepositoryProvider
|
||||
.overrideWithValue(FakeAuthRepository(null)),
|
||||
apiClientProvider
|
||||
.overrideWithValue(ApiClient(mockLogger, mockClient)),
|
||||
],
|
||||
child: app,
|
||||
),
|
||||
);
|
||||
|
||||
// wait for auth state and perf stats
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
// wait for auth state and perf stats
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
|
||||
final requiredStatsValues = [
|
||||
'50.24', // Deviation
|
||||
'20', // Progression in last 12 games
|
||||
'0', // Berserked games
|
||||
'0', // Tournament games
|
||||
'3', // Rated games
|
||||
'2', // Won games
|
||||
'2', // Lost games
|
||||
'1', // Drawn games
|
||||
'1' // Disconnections
|
||||
];
|
||||
final requiredStatsValues = [
|
||||
'50.24', // Deviation
|
||||
'20', // Progression in last 12 games
|
||||
'0', // Berserked games
|
||||
'0', // Tournament games
|
||||
'3', // Rated games
|
||||
'2', // Won games
|
||||
'2', // Lost games
|
||||
'1', // Drawn games
|
||||
'1' // Disconnections
|
||||
];
|
||||
|
||||
// rating
|
||||
expect(find.text('1500.42'), findsOneWidget);
|
||||
// rating
|
||||
expect(find.text('1500.42'), findsOneWidget);
|
||||
|
||||
for (final val in requiredStatsValues) {
|
||||
expect(find.widgetWithText(PlatformCard, val), findsAtLeastNWidgets(1));
|
||||
}
|
||||
}, variant: kPlatformVariant);
|
||||
for (final val in requiredStatsValues) {
|
||||
expect(
|
||||
find.widgetWithText(PlatformCard, val),
|
||||
findsAtLeastNWidgets(1),
|
||||
);
|
||||
}
|
||||
},
|
||||
variant: kPlatformVariant,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -28,90 +28,105 @@ void main() {
|
||||
setUpAll(() {
|
||||
when(
|
||||
() => mockClient.get(
|
||||
Uri.parse(
|
||||
'$kLichessHost/api/games/user/$testUserId?max=10&moves=false&lastFen=true'),
|
||||
headers: any(
|
||||
named: 'headers',
|
||||
that: sameHeaders({'Accept': 'application/x-ndjson'}))),
|
||||
Uri.parse(
|
||||
'$kLichessHost/api/games/user/$testUserId?max=10&moves=false&lastFen=true',
|
||||
),
|
||||
headers: any(
|
||||
named: 'headers',
|
||||
that: sameHeaders({'Accept': 'application/x-ndjson'}),
|
||||
),
|
||||
),
|
||||
).thenAnswer((_) => mockResponse(userGameResponse, 200));
|
||||
});
|
||||
|
||||
group('ProfileScreen', () {
|
||||
testWidgets('meets accessibility guidelines', (WidgetTester tester) async {
|
||||
final SemanticsHandle handle = tester.ensureSemantics();
|
||||
testWidgets(
|
||||
'meets accessibility guidelines',
|
||||
(WidgetTester tester) async {
|
||||
final SemanticsHandle handle = tester.ensureSemantics();
|
||||
|
||||
final app = await buildTestApp(
|
||||
tester,
|
||||
home: Consumer(builder: (context, ref, _) {
|
||||
return const ProfileScreen();
|
||||
}),
|
||||
);
|
||||
final app = await buildTestApp(
|
||||
tester,
|
||||
home: Consumer(
|
||||
builder: (context, ref, _) {
|
||||
return const ProfileScreen();
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
authRepositoryProvider
|
||||
.overrideWithValue(FakeAuthRepository(testUser)),
|
||||
apiClientProvider
|
||||
.overrideWithValue(ApiClient(mockLogger, mockClient)),
|
||||
],
|
||||
child: app,
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
authRepositoryProvider
|
||||
.overrideWithValue(FakeAuthRepository(testUser)),
|
||||
apiClientProvider
|
||||
.overrideWithValue(ApiClient(mockLogger, mockClient)),
|
||||
],
|
||||
child: app,
|
||||
),
|
||||
);
|
||||
|
||||
// wait for auth state
|
||||
await tester.pump();
|
||||
// wait for auth state
|
||||
await tester.pump();
|
||||
|
||||
// profile user name at the top
|
||||
expect(find.widgetWithText(ListTile, testUserName), findsOneWidget);
|
||||
// profile user name at the top
|
||||
expect(find.widgetWithText(ListTile, testUserName), findsOneWidget);
|
||||
|
||||
// wait for recent games
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
// wait for recent games
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
|
||||
await meetsTapTargetGuideline(tester);
|
||||
await meetsTapTargetGuideline(tester);
|
||||
|
||||
await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
|
||||
await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
|
||||
|
||||
if (debugDefaultTargetPlatformOverride == TargetPlatform.android) {
|
||||
await expectLater(tester, meetsGuideline(textContrastGuideline));
|
||||
}
|
||||
handle.dispose();
|
||||
}, variant: kPlatformVariant);
|
||||
if (debugDefaultTargetPlatformOverride == TargetPlatform.android) {
|
||||
await expectLater(tester, meetsGuideline(textContrastGuideline));
|
||||
}
|
||||
handle.dispose();
|
||||
},
|
||||
variant: kPlatformVariant,
|
||||
);
|
||||
|
||||
testWidgets('should see recent games', (WidgetTester tester) async {
|
||||
final app = await buildTestApp(
|
||||
tester,
|
||||
home: Consumer(builder: (context, ref, _) {
|
||||
return const ProfileScreen();
|
||||
}),
|
||||
);
|
||||
testWidgets(
|
||||
'should see recent games',
|
||||
(WidgetTester tester) async {
|
||||
final app = await buildTestApp(
|
||||
tester,
|
||||
home: Consumer(
|
||||
builder: (context, ref, _) {
|
||||
return const ProfileScreen();
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
authRepositoryProvider
|
||||
.overrideWithValue(FakeAuthRepository(testUser)),
|
||||
apiClientProvider
|
||||
.overrideWithValue(ApiClient(mockLogger, mockClient)),
|
||||
],
|
||||
child: app,
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
authRepositoryProvider
|
||||
.overrideWithValue(FakeAuthRepository(testUser)),
|
||||
apiClientProvider
|
||||
.overrideWithValue(ApiClient(mockLogger, mockClient)),
|
||||
],
|
||||
child: app,
|
||||
),
|
||||
);
|
||||
|
||||
// wait for auth state
|
||||
await tester.pump();
|
||||
// wait for auth state
|
||||
await tester.pump();
|
||||
|
||||
// profile user name at the top
|
||||
expect(find.widgetWithText(ListTile, testUserName), findsOneWidget);
|
||||
// profile user name at the top
|
||||
expect(find.widgetWithText(ListTile, testUserName), findsOneWidget);
|
||||
|
||||
// wait for recent games
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
// wait for recent games
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
|
||||
// opponent in recent games
|
||||
expect(find.widgetWithText(ListTile, 'maia1 (1397)'), findsOneWidget);
|
||||
expect(find.widgetWithText(ListTile, 'maia1 (1399)'), findsOneWidget);
|
||||
expect(find.widgetWithText(ListTile, 'maia1 (1410)'), findsOneWidget);
|
||||
}, variant: kPlatformVariant);
|
||||
// opponent in recent games
|
||||
expect(find.widgetWithText(ListTile, 'maia1 (1397)'), findsOneWidget);
|
||||
expect(find.widgetWithText(ListTile, 'maia1 (1399)'), findsOneWidget);
|
||||
expect(find.widgetWithText(ListTile, 'maia1 (1410)'), findsOneWidget);
|
||||
},
|
||||
variant: kPlatformVariant,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -149,4 +164,8 @@ final testUser = User(
|
||||
);
|
||||
|
||||
const _fakePerf = UserPerf(
|
||||
rating: 1500, ratingDeviation: 0, progression: 0, numberOfGames: 0);
|
||||
rating: 1500,
|
||||
ratingDeviation: 0,
|
||||
progression: 0,
|
||||
numberOfGames: 0,
|
||||
);
|
||||
|
||||
+13
-6
@@ -21,10 +21,13 @@ Future<http.Response> mockResponse(String body, int code) async =>
|
||||
.then((_) => http.Response(body, code));
|
||||
|
||||
Future<http.StreamedResponse> mockHttpStreamFromIterable(
|
||||
Iterable<String> events) async {
|
||||
Iterable<String> events,
|
||||
) async {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 20));
|
||||
return http.StreamedResponse(
|
||||
_streamFromFutures(events.map((e) => _withDelay(utf8.encode(e)))), 200);
|
||||
_streamFromFutures(events.map((e) => _withDelay(utf8.encode(e)))),
|
||||
200,
|
||||
);
|
||||
}
|
||||
|
||||
Future<http.StreamedResponse> mockHttpStream(Stream<String> stream) async =>
|
||||
@@ -54,8 +57,10 @@ Offset squareOffset(
|
||||
}) {
|
||||
final squareSize = boardRect.width / 8;
|
||||
final o = cg.Coord.fromSquareId(id).offset(orientation, squareSize);
|
||||
return Offset(o.dx + boardRect.left + squareSize / 2,
|
||||
o.dy + boardRect.top + squareSize / 2);
|
||||
return Offset(
|
||||
o.dx + boardRect.left + squareSize / 2,
|
||||
o.dy + boardRect.top + squareSize / 2,
|
||||
);
|
||||
}
|
||||
|
||||
// simplified version of class [App] in lib/src/app.dart
|
||||
@@ -119,6 +124,8 @@ Stream<T> _streamFromFutures<T>(Iterable<Future<T>> futures) async* {
|
||||
}
|
||||
}
|
||||
|
||||
Future<T> _withDelay<T>(T value,
|
||||
{Duration delay = const Duration(milliseconds: 10)}) =>
|
||||
Future<T> _withDelay<T>(
|
||||
T value, {
|
||||
Duration delay = const Duration(milliseconds: 10),
|
||||
}) =>
|
||||
Future<void>.delayed(delay).then((_) => value);
|
||||
|
||||
@@ -26,12 +26,22 @@ void main() {
|
||||
group('DurationExtensions.toDaysHoursMinutes()', () {
|
||||
test('all values nonzero, plural', () {
|
||||
testTimeStr(
|
||||
mockAppLocalizations, 2, 2, 2, '2 days, 2 hours and 2 minutes');
|
||||
mockAppLocalizations,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
'2 days, 2 hours and 2 minutes',
|
||||
);
|
||||
});
|
||||
|
||||
test('all values nonzero, plural', () {
|
||||
testTimeStr(
|
||||
mockAppLocalizations, 2, 2, 2, '2 days, 2 hours and 2 minutes');
|
||||
mockAppLocalizations,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
'2 days, 2 hours and 2 minutes',
|
||||
);
|
||||
});
|
||||
|
||||
test('all values nonzero, single', () {
|
||||
@@ -68,8 +78,13 @@ void main() {
|
||||
});
|
||||
}
|
||||
|
||||
void testTimeStr(MockAppLocalizations mockAppLocalizations, int days, int hours,
|
||||
int minutes, String expected) {
|
||||
void testTimeStr(
|
||||
MockAppLocalizations mockAppLocalizations,
|
||||
int days,
|
||||
int hours,
|
||||
int minutes,
|
||||
String expected,
|
||||
) {
|
||||
final timeStr = Duration(days: days, hours: hours, minutes: minutes)
|
||||
.toDaysHoursMinutes(mockAppLocalizations);
|
||||
expect(timeStr, expected);
|
||||
|
||||
Reference in New Issue
Block a user