Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
264 changes: 215 additions & 49 deletions lib/src/model/account/account_preferences.dart
Original file line number Diff line number Diff line change
@@ -1,51 +1,85 @@
import 'dart:convert';

import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/l10n/l10n.dart';
import 'package:lichess_mobile/src/binding.dart';
import 'package:lichess_mobile/src/model/account/account_repository.dart';
import 'package:lichess_mobile/src/model/auth/auth_controller.dart';

typedef AccountPrefState = ({
// game display
Zen zenMode,
PieceNotation pieceNotation,
ShowRatings showRatings,
// game behavior
BooleanPref premove,
AutoQueen autoQueen,
AutoThreefold autoThreefold,
Takeback takeback,
BooleanPref confirmResign,
SubmitMove submitMove,
// clock
Moretime moretime,
ClockTenths clockTenths,
BooleanPref clockSound,
// privacy
BooleanPref follow,
Challenge challenge,
Message message,
});
import 'package:lichess_mobile/src/model/settings/preferences_storage.dart';

part 'account_preferences.freezed.dart';
part 'account_preferences.g.dart';

@Freezed(fromJson: true, toJson: true)
sealed class AccountPrefState with _$AccountPrefState implements Serializable {
const AccountPrefState._();

const factory AccountPrefState({
// game display
@JsonKey(name: 'zen', fromJson: _zenFromJson, toJson: _zenToJson) required Zen zenMode,
@JsonKey(fromJson: _pieceNotationFromJson, toJson: _pieceNotationToJson)
required PieceNotation pieceNotation,
@JsonKey(name: 'ratings', fromJson: _showRatingsFromJson, toJson: _showRatingsToJson)
required ShowRatings showRatings,
// game behavior
@JsonKey(fromJson: _premoveFromJson, toJson: _booleanPrefToJson) required BooleanPref premove,
@JsonKey(fromJson: _autoQueenFromJson, toJson: _autoQueenToJson) required AutoQueen autoQueen,
@JsonKey(fromJson: _autoThreefoldFromJson, toJson: _autoThreefoldToJson)
required AutoThreefold autoThreefold,
@JsonKey(fromJson: _takebackFromJson, toJson: _takebackToJson) required Takeback takeback,
@JsonKey(fromJson: _confirmResignFromJson, toJson: _booleanPrefToJson)
required BooleanPref confirmResign,
@JsonKey(fromJson: _submitMoveFromJson, toJson: _submitMoveToJson)
required SubmitMove submitMove,
// clock
@JsonKey(fromJson: _moretimeFromJson, toJson: _moretimeToJson) required Moretime moretime,
@JsonKey(fromJson: _clockTenthsFromJson, toJson: _clockTenthsToJson)
required ClockTenths clockTenths,
@JsonKey(fromJson: _clockSoundFromJson, toJson: _booleanPrefToJson)
required BooleanPref clockSound,
// privacy
@JsonKey(fromJson: _followFromJson, toJson: _booleanPrefToJson) required BooleanPref follow,
@JsonKey(fromJson: _challengeFromJson, toJson: _challengeToJson) required Challenge challenge,
@JsonKey(fromJson: _messageFromJson, toJson: _messageToJson) required Message message,
}) = _AccountPrefState;

factory AccountPrefState.fromJson(Map<String, dynamic> json) {
try {
return _$AccountPrefStateFromJson(json);
} catch (_) {
return defaultAccountPreferences;
}
}
Comment on lines +49 to +55
}

/// A provider that tells if the user wants to see ratings in the app.
final showRatingsPrefProvider = FutureProvider<ShowRatings>((Ref ref) async {
final prefs = await ref.watch(accountPreferencesProvider.future);
return prefs?.showRatings ?? defaultAccountPreferences.showRatings;
return prefs.showRatings;
});

/// A provider that tells if the user wants clock sounds.
final clockSoundProvider = FutureProvider<bool>((Ref ref) async {
final prefs = await ref.watch(accountPreferencesProvider.future);
return prefs?.clockSound.value ?? defaultAccountPreferences.clockSound.value;
return prefs.clockSound.value;
});

/// A provider that gives the user's preferred piece notation.
final pieceNotationProvider = FutureProvider<PieceNotation>((Ref ref) async {
final prefs = await ref.watch(accountPreferencesProvider.future);
return prefs?.pieceNotation ?? defaultAccountPreferences.pieceNotation;
return prefs.pieceNotation;
});

final defaultAccountPreferences = (
/// A provider that gives the user's preferred clock tenths display.
final clockTenthsProvider = FutureProvider<ClockTenths>((Ref ref) async {
final prefs = await ref.watch(accountPreferencesProvider.future);
return prefs.clockTenths;
});

final defaultAccountPreferences = AccountPrefState(
zenMode: Zen.no,
pieceNotation: PieceNotation.symbol,
showRatings: ShowRatings.yes,
Expand All @@ -64,7 +98,7 @@ final defaultAccountPreferences = (
);

/// A provider that gives the account preferences for the current user.
final accountPreferencesProvider = AsyncNotifierProvider<AccountPreferences, AccountPrefState?>(
final accountPreferencesProvider = AsyncNotifierProvider<AccountPreferences, AccountPrefState>(
AccountPreferences.new,
name: 'AccountPreferencesProvider',
);
Expand All @@ -73,45 +107,177 @@ final accountPreferencesProvider = AsyncNotifierProvider<AccountPreferences, Acc
///
/// The result is cached for the lifetime of the app, until refreshed.
/// If the server returns an error, default values are returned.
class AccountPreferences extends AsyncNotifier<AccountPrefState?> {
class AccountPreferences extends AsyncNotifier<AccountPrefState> {
@override
Future<AccountPrefState?> build() async {
Future<AccountPrefState> build() async {
final authUser = ref.watch(authControllerProvider);

if (authUser == null) {
return null;
return _fetchLocal();
}

try {
return ref.read(accountRepositoryProvider).getPreferences();
return await ref.read(accountRepositoryProvider).getPreferences();
} catch (e) {
debugPrint('[AccountPreferences] Error getting account preferences: $e');
return defaultAccountPreferences;
}
}

Future<void> setZen(Zen value) => _setPref('zen', value);
Future<void> setPieceNotation(PieceNotation value) => _setPref('pieceNotation', value);
Future<void> setShowRatings(ShowRatings value) => _setPref('ratings', value);

Future<void> setPremove(BooleanPref value) => _setPref('premove', value);
Future<void> setTakeback(Takeback value) => _setPref('takeback', value);
Future<void> setAutoQueen(AutoQueen value) => _setPref('autoQueen', value);
Future<void> setAutoThreefold(AutoThreefold value) => _setPref('autoThreefold', value);
Future<void> setMoretime(Moretime value) => _setPref('moretime', value);
Future<void> setClockTenths(ClockTenths value) => _setPref('clockTenths', value);
Future<void> setClockSound(BooleanPref value) => _setPref('clockSound', value);
Future<void> setConfirmResign(BooleanPref value) => _setPref('confirmResign', value);
Future<void> setSubmitMove(SubmitMove value) => _setPref('submitMove', value);
Future<void> setFollow(BooleanPref value) => _setPref('follow', value);
Future<void> setChallenge(Challenge value) => _setPref('challenge', value);
Future<void> setMessage(Message value) => _setPref('message', value);

Future<void> _setPref<T>(String key, AccountPref<T> value) async {
Future<void> setZen(Zen value) =>
_setPref('zen', value, (prefs) => prefs.copyWith(zenMode: value));
Future<void> setPieceNotation(PieceNotation value) =>
_setPref('pieceNotation', value, (prefs) => prefs.copyWith(pieceNotation: value));
Future<void> setShowRatings(ShowRatings value) =>
_setPref('ratings', value, (prefs) => prefs.copyWith(showRatings: value));

Future<void> setPremove(BooleanPref value) =>
_setPref('premove', value, (prefs) => prefs.copyWith(premove: value));
Future<void> setTakeback(Takeback value) =>
_setPref('takeback', value, (prefs) => prefs.copyWith(takeback: value));
Future<void> setAutoQueen(AutoQueen value) =>
_setPref('autoQueen', value, (prefs) => prefs.copyWith(autoQueen: value));
Future<void> setAutoThreefold(AutoThreefold value) =>
_setPref('autoThreefold', value, (prefs) => prefs.copyWith(autoThreefold: value));
Future<void> setMoretime(Moretime value) =>
_setPref('moretime', value, (prefs) => prefs.copyWith(moretime: value));
Future<void> setClockTenths(ClockTenths value) =>
_setPref('clockTenths', value, (prefs) => prefs.copyWith(clockTenths: value));
Future<void> setClockSound(BooleanPref value) =>
_setPref('clockSound', value, (prefs) => prefs.copyWith(clockSound: value));
Future<void> setConfirmResign(BooleanPref value) =>
_setPref('confirmResign', value, (prefs) => prefs.copyWith(confirmResign: value));
Future<void> setSubmitMove(SubmitMove value) =>
_setPref('submitMove', value, (prefs) => prefs.copyWith(submitMove: value));
Future<void> setFollow(BooleanPref value) =>
_setPref('follow', value, (prefs) => prefs.copyWith(follow: value));
Future<void> setChallenge(Challenge value) =>
_setPref('challenge', value, (prefs) => prefs.copyWith(challenge: value));
Future<void> setMessage(Message value) =>
_setPref('message', value, (prefs) => prefs.copyWith(message: value));

Future<void> _setPref<T>(
String key,
AccountPref<T> value,
AccountPrefState Function(AccountPrefState prefs) updateLocal,
) async {
final authUser = ref.read(authControllerProvider);
if (authUser == null) {
await _saveLocal(updateLocal(state.value ?? _fetchLocal()));
return;
}
Comment on lines +164 to +168

await Future<void>.delayed(const Duration(milliseconds: 200));
await ref.read(accountRepositoryProvider).setPreference(key, value);
ref.invalidateSelf();
}

Future<void> _saveLocal(AccountPrefState value) async {
await LichessBinding.instance.sharedPreferences.setString(
PrefCategory.account.storageKey,
jsonEncode(value.toJson()),
);

if (!ref.mounted) return;

state = AsyncValue.data(value);
}

AccountPrefState _fetchLocal() {
final stored = LichessBinding.instance.sharedPreferences.getString(
PrefCategory.account.storageKey,
);
if (stored == null) {
return defaultAccountPreferences;
}
try {
return AccountPrefState.fromJson(jsonDecode(stored) as Map<String, dynamic>);
} catch (e) {
debugPrint('[AccountPreferences] Error reading local account preferences: $e');
return defaultAccountPreferences;
}
}
}

Zen _zenFromJson(Object? value) =>
Zen.fromInt(_intValue(value, defaultAccountPreferences.zenMode.value));

int _zenToJson(Zen value) => value.value;

PieceNotation _pieceNotationFromJson(Object? value) =>
PieceNotation.fromInt(_intValue(value, defaultAccountPreferences.pieceNotation.value));

int _pieceNotationToJson(PieceNotation value) => value.value;

ShowRatings _showRatingsFromJson(Object? value) =>
ShowRatings.fromInt(_intValue(value, defaultAccountPreferences.showRatings.value));

int _showRatingsToJson(ShowRatings value) => value.value;

BooleanPref _premoveFromJson(Object? value) =>
BooleanPref(_boolValue(value, defaultAccountPreferences.premove.value));

AutoQueen _autoQueenFromJson(Object? value) =>
AutoQueen.fromInt(_intValue(value, defaultAccountPreferences.autoQueen.value));

int _autoQueenToJson(AutoQueen value) => value.value;

AutoThreefold _autoThreefoldFromJson(Object? value) =>
AutoThreefold.fromInt(_intValue(value, defaultAccountPreferences.autoThreefold.value));

int _autoThreefoldToJson(AutoThreefold value) => value.value;

Takeback _takebackFromJson(Object? value) =>
Takeback.fromInt(_intValue(value, defaultAccountPreferences.takeback.value));

int _takebackToJson(Takeback value) => value.value;

BooleanPref _confirmResignFromJson(Object? value) =>
BooleanPref(_boolValue(value, defaultAccountPreferences.confirmResign.value));

SubmitMove _submitMoveFromJson(Object? value) =>
SubmitMove.fromInt(_intValue(value, defaultAccountPreferences.submitMove.value));

int _submitMoveToJson(SubmitMove value) => value.value;

Moretime _moretimeFromJson(Object? value) =>
Moretime.fromInt(_intValue(value, defaultAccountPreferences.moretime.value));

int _moretimeToJson(Moretime value) => value.value;

ClockTenths _clockTenthsFromJson(Object? value) =>
ClockTenths.fromInt(_intValue(value, defaultAccountPreferences.clockTenths.value));

int _clockTenthsToJson(ClockTenths value) => value.value;

BooleanPref _clockSoundFromJson(Object? value) =>
BooleanPref(_boolValue(value, defaultAccountPreferences.clockSound.value));

BooleanPref _followFromJson(Object? value) =>
BooleanPref(_boolValue(value, defaultAccountPreferences.follow.value));

Challenge _challengeFromJson(Object? value) =>
Challenge.fromInt(_intValue(value, defaultAccountPreferences.challenge.value));

int _challengeToJson(Challenge value) => value.value;

Message _messageFromJson(Object? value) =>
Message.fromInt(_intValue(value, defaultAccountPreferences.message.value));

int _messageToJson(Message value) => value.value;

bool _booleanPrefToJson(BooleanPref value) => value.value;

int _intValue(Object? value, int fallback) {
return value is num ? value.toInt() : fallback;
}

bool _boolValue(Object? value, bool fallback) {
return switch (value) {
final bool b => b,
final num n => n != 0,
_ => fallback,
};
}

abstract class AccountPref<T> {
Expand Down
2 changes: 1 addition & 1 deletion lib/src/model/account/account_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ class AccountRepository {
}

AccountPrefState _accountPreferencesFromPick(RequiredPick pick) {
return (
return AccountPrefState(
zenMode: Zen.fromInt(pick('zen').asIntOrThrow()),
pieceNotation: PieceNotation.fromInt(pick('pieceNotation').asIntOrThrow()),
showRatings: ShowRatings.fromInt(pick('ratings').asIntOrThrow()),
Expand Down
39 changes: 37 additions & 2 deletions lib/src/model/game/game_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,33 @@ final gameControllerProvider = AsyncNotifierProvider.autoDispose
name: 'GameControllerProvider',
);

PlayableGame _withAccountPrefsFallback(PlayableGame game, AccountPrefState prefs) {
if (game.prefs != null) {
return game;
}

return game.copyWith(
prefs: ServerGamePrefs(
showRatings: prefs.showRatings == ShowRatings.yes,
enablePremove: prefs.premove.value,
autoQueen: prefs.autoQueen,
confirmResign: prefs.confirmResign.value,
submitMove: _submitMoveEnabledForSpeed(prefs.submitMove, game.meta.speed),
zenMode: prefs.zenMode,
),
);
}

bool _submitMoveEnabledForSpeed(SubmitMove submitMove, Speed speed) {
return switch (speed) {
Speed.correspondence => submitMove.choices.contains(SubmitMoveChoice.correspondence),
Speed.classical => submitMove.choices.contains(SubmitMoveChoice.classical),
Speed.rapid => submitMove.choices.contains(SubmitMoveChoice.rapid),
Speed.blitz => submitMove.choices.contains(SubmitMoveChoice.blitz),
Speed.ultraBullet || Speed.bullet => false,
};
}

class GameController extends AsyncNotifier<GameState> with ChatMixin<GameState> {
GameController(this.gameFullId);

Expand Down Expand Up @@ -123,7 +150,12 @@ class GameController extends AsyncNotifier<GameState> with ChatMixin<GameState>
final fullEvent = GameFullEvent.fromJson(rawFullEvent.data as Map<String, dynamic>);
_socketClient.version = fullEvent.socketEventVersion;

final game = fullEvent.game;
final game = fullEvent.game.prefs != null
? fullEvent.game
: _withAccountPrefsFallback(
fullEvent.game,
await ref.read(accountPreferencesProvider.future),
);

// Play "dong" sound when this is a new game and we're playing it (not spectating)
final isMyGame = game.youAre != null;
Expand Down Expand Up @@ -642,7 +674,10 @@ class GameController extends AsyncNotifier<GameState> with ChatMixin<GameState>

final curState = state.requireValue;

final newGame = fullEvent.game;
final newGame = _withAccountPrefsFallback(
fullEvent.game,
ref.read(accountPreferencesProvider).value ?? defaultAccountPreferences,
);
final isOpponentOnGame =
newGame.playerOf(newGame.youAre?.opposite ?? Side.white).onGame ?? false;

Expand Down
1 change: 1 addition & 0 deletions lib/src/model/settings/preferences_storage.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ enum PrefCategory {
broadcast('preferences.broadcast'),
engineEvaluation('preferences.engineEvaluation'),
offlineComputerGame('preferences.offlineComputerGame'),
account('preferences.account'),
log('preferences.log'),
clockTool('preferences.clockTool');

Expand Down
Loading