1+ import 'dart:convert' ;
2+
13import 'package:fast_immutable_collections/fast_immutable_collections.dart' ;
24import 'package:flutter/widgets.dart' ;
35import 'package:flutter_riverpod/flutter_riverpod.dart' ;
6+ import 'package:freezed_annotation/freezed_annotation.dart' ;
47import 'package:lichess_mobile/l10n/l10n.dart' ;
8+ import 'package:lichess_mobile/src/binding.dart' ;
59import 'package:lichess_mobile/src/model/account/account_repository.dart' ;
610import 'package:lichess_mobile/src/model/auth/auth_controller.dart' ;
11+ import 'package:lichess_mobile/src/model/settings/preferences_storage.dart' ;
712
8- typedef AccountPrefState = ({
9- // game display
10- Zen zenMode,
11- PieceNotation pieceNotation,
12- ShowRatings showRatings,
13- // game behavior
14- BooleanPref premove,
15- AutoQueen autoQueen,
16- AutoThreefold autoThreefold,
17- Takeback takeback,
18- BooleanPref confirmResign,
19- SubmitMove submitMove,
20- // clock
21- Moretime moretime,
22- ClockTenths clockTenths,
23- BooleanPref clockSound,
24- // privacy
25- BooleanPref follow,
26- Challenge challenge,
27- Message message,
28- });
29-
30- /// A provider that tells if the user wants to see ratings in the app.
31- final showRatingsPrefProvider = FutureProvider <ShowRatings >((Ref ref) async {
32- final prefs = await ref.watch (accountPreferencesProvider.future);
33- return prefs? .showRatings ?? defaultAccountPreferences.showRatings;
34- });
13+ part 'account_preferences.freezed.dart' ;
14+ part 'account_preferences.g.dart' ;
3515
36- /// A provider that tells if the user wants clock sounds.
37- final clockSoundProvider = FutureProvider <bool >((Ref ref) async {
38- final prefs = await ref.watch (accountPreferencesProvider.future);
39- return prefs? .clockSound.value ?? defaultAccountPreferences.clockSound.value;
40- });
41-
42- /// A provider that gives the user's preferred piece notation.
43- final pieceNotationProvider = FutureProvider <PieceNotation >((Ref ref) async {
44- final prefs = await ref.watch (accountPreferencesProvider.future);
45- return prefs? .pieceNotation ?? defaultAccountPreferences.pieceNotation;
46- });
16+ /// Represents the account preferences for a user.
17+ ///
18+ /// If the user is authenticated, the preferences are fetched from the server. If the user is not authenticated, the preferences are fetched from local storage.
19+ /// Technically an anonymous user does not have account preferences, but we still store them locally for convenience, because some settings are useful even for anonymous users (e.g. piece notation, premoves, etc.).
20+ @Freezed (fromJson: true , toJson: true )
21+ sealed class AccountPrefState with _$AccountPrefState implements Serializable {
22+ const AccountPrefState ._();
23+
24+ const factory AccountPrefState ({
25+ // game display
26+ @JsonKey (unknownEnumValue: Zen .no) required Zen zenMode,
27+ @JsonKey (unknownEnumValue: PieceNotation .symbol) required PieceNotation pieceNotation,
28+ @JsonKey (unknownEnumValue: ShowRatings .yes) required ShowRatings showRatings,
29+ // game behavior
30+ required BooleanPref premove,
31+ @JsonKey (unknownEnumValue: AutoQueen .premove) required AutoQueen autoQueen,
32+ @JsonKey (unknownEnumValue: AutoThreefold .always) required AutoThreefold autoThreefold,
33+ @JsonKey (unknownEnumValue: Takeback .always) required Takeback takeback,
34+ required BooleanPref confirmResign,
35+ required SubmitMove submitMove,
36+ // clock
37+ @JsonKey (unknownEnumValue: Moretime .always) required Moretime moretime,
38+ @JsonKey (unknownEnumValue: ClockTenths .lessThan10s) required ClockTenths clockTenths,
39+ required BooleanPref clockSound,
40+ // privacy
41+ required BooleanPref follow,
42+ @JsonKey (unknownEnumValue: Challenge .registered) required Challenge challenge,
43+ @JsonKey (unknownEnumValue: Message .always) required Message message,
44+ }) = _AccountPrefState ;
45+
46+ factory AccountPrefState .fromJson (Map <String , dynamic > json) {
47+ try {
48+ return _$AccountPrefStateFromJson (json);
49+ } catch (_) {
50+ return defaultAccountPreferences;
51+ }
52+ }
53+ }
4754
48- final defaultAccountPreferences = (
55+ final defaultAccountPreferences = AccountPrefState (
4956 zenMode: Zen .no,
5057 pieceNotation: PieceNotation .symbol,
5158 showRatings: ShowRatings .yes,
@@ -63,8 +70,32 @@ final defaultAccountPreferences = (
6370 message: Message .always,
6471);
6572
73+ /// A provider that tells if the user wants to see ratings in the app.
74+ final showRatingsPrefProvider = FutureProvider <ShowRatings >((Ref ref) async {
75+ final prefs = await ref.watch (accountPreferencesProvider.future);
76+ return prefs.showRatings;
77+ });
78+
79+ /// A provider that tells if the user wants clock sounds.
80+ final clockSoundProvider = FutureProvider <bool >((Ref ref) async {
81+ final prefs = await ref.watch (accountPreferencesProvider.future);
82+ return prefs.clockSound.value;
83+ });
84+
85+ /// A provider that gives the user's preferred piece notation.
86+ final pieceNotationProvider = FutureProvider <PieceNotation >((Ref ref) async {
87+ final prefs = await ref.watch (accountPreferencesProvider.future);
88+ return prefs.pieceNotation;
89+ });
90+
91+ /// A provider that gives the user's preferred clock tenths display.
92+ final clockTenthsProvider = FutureProvider <ClockTenths >((Ref ref) async {
93+ final prefs = await ref.watch (accountPreferencesProvider.future);
94+ return prefs.clockTenths;
95+ });
96+
6697/// A provider that gives the account preferences for the current user.
67- final accountPreferencesProvider = AsyncNotifierProvider <AccountPreferences , AccountPrefState ? >(
98+ final accountPreferencesProvider = AsyncNotifierProvider <AccountPreferences , AccountPrefState >(
6899 AccountPreferences .new ,
69100 name: 'AccountPreferencesProvider' ,
70101);
@@ -73,45 +104,99 @@ final accountPreferencesProvider = AsyncNotifierProvider<AccountPreferences, Acc
73104///
74105/// The result is cached for the lifetime of the app, until refreshed.
75106/// If the server returns an error, default values are returned.
76- class AccountPreferences extends AsyncNotifier <AccountPrefState ?> {
107+ ///
108+ /// If the user is authenticated, the preferences are fetched from the server. If the user is not authenticated, the preferences are fetched from local storage.
109+ /// See also [AccountPrefState] for more details.
110+ class AccountPreferences extends AsyncNotifier <AccountPrefState > {
77111 @override
78- Future <AccountPrefState ? > build () async {
112+ Future <AccountPrefState > build () async {
79113 final authUser = ref.watch (authControllerProvider);
80114
81115 if (authUser == null ) {
82- return null ;
116+ return _fetchLocal () ;
83117 }
84118
85119 try {
86- return ref.read (accountRepositoryProvider).getPreferences ();
120+ return await ref.read (accountRepositoryProvider).getPreferences ();
87121 } catch (e) {
88122 debugPrint ('[AccountPreferences] Error getting account preferences: $e ' );
89123 return defaultAccountPreferences;
90124 }
91125 }
92126
93- Future <void > setZen (Zen value) => _setPref ('zen' , value);
94- Future <void > setPieceNotation (PieceNotation value) => _setPref ('pieceNotation' , value);
95- Future <void > setShowRatings (ShowRatings value) => _setPref ('ratings' , value);
96-
97- Future <void > setPremove (BooleanPref value) => _setPref ('premove' , value);
98- Future <void > setTakeback (Takeback value) => _setPref ('takeback' , value);
99- Future <void > setAutoQueen (AutoQueen value) => _setPref ('autoQueen' , value);
100- Future <void > setAutoThreefold (AutoThreefold value) => _setPref ('autoThreefold' , value);
101- Future <void > setMoretime (Moretime value) => _setPref ('moretime' , value);
102- Future <void > setClockTenths (ClockTenths value) => _setPref ('clockTenths' , value);
103- Future <void > setClockSound (BooleanPref value) => _setPref ('clockSound' , value);
104- Future <void > setConfirmResign (BooleanPref value) => _setPref ('confirmResign' , value);
105- Future <void > setSubmitMove (SubmitMove value) => _setPref ('submitMove' , value);
106- Future <void > setFollow (BooleanPref value) => _setPref ('follow' , value);
107- Future <void > setChallenge (Challenge value) => _setPref ('challenge' , value);
108- Future <void > setMessage (Message value) => _setPref ('message' , value);
109-
110- Future <void > _setPref <T >(String key, AccountPref <T > value) async {
127+ Future <void > setZen (Zen value) =>
128+ _setPref ('zen' , value, (prefs) => prefs.copyWith (zenMode: value));
129+ Future <void > setPieceNotation (PieceNotation value) =>
130+ _setPref ('pieceNotation' , value, (prefs) => prefs.copyWith (pieceNotation: value));
131+ Future <void > setShowRatings (ShowRatings value) =>
132+ _setPref ('ratings' , value, (prefs) => prefs.copyWith (showRatings: value));
133+
134+ Future <void > setPremove (BooleanPref value) =>
135+ _setPref ('premove' , value, (prefs) => prefs.copyWith (premove: value));
136+ Future <void > setTakeback (Takeback value) =>
137+ _setPref ('takeback' , value, (prefs) => prefs.copyWith (takeback: value));
138+ Future <void > setAutoQueen (AutoQueen value) =>
139+ _setPref ('autoQueen' , value, (prefs) => prefs.copyWith (autoQueen: value));
140+ Future <void > setAutoThreefold (AutoThreefold value) =>
141+ _setPref ('autoThreefold' , value, (prefs) => prefs.copyWith (autoThreefold: value));
142+ Future <void > setMoretime (Moretime value) =>
143+ _setPref ('moretime' , value, (prefs) => prefs.copyWith (moretime: value));
144+ Future <void > setClockTenths (ClockTenths value) =>
145+ _setPref ('clockTenths' , value, (prefs) => prefs.copyWith (clockTenths: value));
146+ Future <void > setClockSound (BooleanPref value) =>
147+ _setPref ('clockSound' , value, (prefs) => prefs.copyWith (clockSound: value));
148+ Future <void > setConfirmResign (BooleanPref value) =>
149+ _setPref ('confirmResign' , value, (prefs) => prefs.copyWith (confirmResign: value));
150+ Future <void > setSubmitMove (SubmitMove value) =>
151+ _setPref ('submitMove' , value, (prefs) => prefs.copyWith (submitMove: value));
152+ Future <void > setFollow (BooleanPref value) =>
153+ _setPref ('follow' , value, (prefs) => prefs.copyWith (follow: value));
154+ Future <void > setChallenge (Challenge value) =>
155+ _setPref ('challenge' , value, (prefs) => prefs.copyWith (challenge: value));
156+ Future <void > setMessage (Message value) =>
157+ _setPref ('message' , value, (prefs) => prefs.copyWith (message: value));
158+
159+ Future <void > _setPref <T >(
160+ String key,
161+ AccountPref <T > value,
162+ AccountPrefState Function (AccountPrefState prefs) updateLocal,
163+ ) async {
164+ final authUser = ref.read (authControllerProvider);
165+ if (authUser == null ) {
166+ await _saveLocal (updateLocal (state.value ?? _fetchLocal ()));
167+ return ;
168+ }
169+
111170 await Future <void >.delayed (const Duration (milliseconds: 200 ));
112171 await ref.read (accountRepositoryProvider).setPreference (key, value);
113172 ref.invalidateSelf ();
114173 }
174+
175+ Future <void > _saveLocal (AccountPrefState value) async {
176+ await LichessBinding .instance.sharedPreferences.setString (
177+ PrefCategory .account.storageKey,
178+ jsonEncode (value.toJson ()),
179+ );
180+
181+ if (! ref.mounted) return ;
182+
183+ state = AsyncValue .data (value);
184+ }
185+
186+ AccountPrefState _fetchLocal () {
187+ final stored = LichessBinding .instance.sharedPreferences.getString (
188+ PrefCategory .account.storageKey,
189+ );
190+ if (stored == null ) {
191+ return defaultAccountPreferences;
192+ }
193+ try {
194+ return AccountPrefState .fromJson (jsonDecode (stored) as Map <String , dynamic >);
195+ } catch (e) {
196+ debugPrint ('[AccountPreferences] Error reading local account preferences: $e ' );
197+ return defaultAccountPreferences;
198+ }
199+ }
115200}
116201
117202abstract class AccountPref <T > {
@@ -128,6 +213,10 @@ class BooleanPref implements AccountPref<bool> {
128213 @override
129214 String get toFormData => value ? '1' : '0' ;
130215
216+ factory BooleanPref .fromJson (bool json) => BooleanPref (json);
217+
218+ bool toJson () => value;
219+
131220 static BooleanPref fromInt (int value) {
132221 switch (value) {
133222 case 1 :
@@ -141,8 +230,11 @@ class BooleanPref implements AccountPref<bool> {
141230}
142231
143232enum Zen implements AccountPref <int > {
233+ @JsonValue (0 )
144234 no (0 ),
235+ @JsonValue (1 )
145236 yes (1 ),
237+ @JsonValue (2 )
146238 gameAuto (2 );
147239
148240 const Zen (this .value);
@@ -179,8 +271,11 @@ enum Zen implements AccountPref<int> {
179271}
180272
181273enum ShowRatings implements AccountPref <int > {
274+ @JsonValue (0 )
182275 no (0 ),
276+ @JsonValue (1 )
183277 yes (1 ),
278+ @JsonValue (2 )
184279 exceptInGame (2 );
185280
186281 const ShowRatings (this .value);
@@ -217,7 +312,9 @@ enum ShowRatings implements AccountPref<int> {
217312}
218313
219314enum PieceNotation implements AccountPref <int > {
315+ @JsonValue (0 )
220316 symbol (0 ),
317+ @JsonValue (1 )
221318 letter (1 );
222319
223320 const PieceNotation (this .value);
@@ -250,8 +347,11 @@ enum PieceNotation implements AccountPref<int> {
250347}
251348
252349enum AutoQueen implements AccountPref <int > {
350+ @JsonValue (1 )
253351 never (1 ),
352+ @JsonValue (2 )
254353 premove (2 ),
354+ @JsonValue (3 )
255355 always (3 );
256356
257357 const AutoQueen (this .value);
@@ -288,8 +388,11 @@ enum AutoQueen implements AccountPref<int> {
288388}
289389
290390enum AutoThreefold implements AccountPref <int > {
391+ @JsonValue (1 )
291392 never (1 ),
393+ @JsonValue (2 )
292394 time (2 ),
395+ @JsonValue (3 )
293396 always (3 );
294397
295398 const AutoThreefold (this .value);
@@ -326,8 +429,11 @@ enum AutoThreefold implements AccountPref<int> {
326429}
327430
328431enum Takeback implements AccountPref <int > {
432+ @JsonValue (1 )
329433 never (1 ),
434+ @JsonValue (2 )
330435 casual (2 ),
436+ @JsonValue (3 )
331437 always (3 );
332438
333439 const Takeback (this .value);
@@ -364,8 +470,11 @@ enum Takeback implements AccountPref<int> {
364470}
365471
366472enum Moretime implements AccountPref <int > {
473+ @JsonValue (1 )
367474 never (1 ),
475+ @JsonValue (2 )
368476 casual (2 ),
477+ @JsonValue (3 )
369478 always (3 );
370479
371480 const Moretime (this .value);
@@ -402,8 +511,11 @@ enum Moretime implements AccountPref<int> {
402511}
403512
404513enum ClockTenths implements AccountPref <int > {
514+ @JsonValue (0 )
405515 never (0 ),
516+ @JsonValue (1 )
406517 lessThan10s (1 ),
518+ @JsonValue (2 )
407519 always (2 );
408520
409521 const ClockTenths (this .value);
@@ -440,10 +552,15 @@ enum ClockTenths implements AccountPref<int> {
440552}
441553
442554enum Challenge implements AccountPref <int > {
555+ @JsonValue (1 )
443556 never (1 ),
557+ @JsonValue (2 )
444558 rating (2 ),
559+ @JsonValue (3 )
445560 friends (3 ),
561+ @JsonValue (4 )
446562 registered (4 ),
563+ @JsonValue (5 )
447564 always (5 );
448565
449566 const Challenge (this .value);
@@ -488,8 +605,11 @@ enum Challenge implements AccountPref<int> {
488605}
489606
490607enum Message implements AccountPref <int > {
608+ @JsonValue (1 )
491609 never (1 ),
610+ @JsonValue (2 )
492611 friends (2 ),
612+ @JsonValue (3 )
493613 always (3 );
494614
495615 const Message (this .value);
@@ -546,6 +666,10 @@ class SubmitMove implements AccountPref<int> {
546666
547667 factory SubmitMove .fromInt (int value) =>
548668 SubmitMove (SubmitMoveChoice .values.where ((choice) => _bitPresent (value, choice.value)));
669+
670+ factory SubmitMove .fromJson (int json) => SubmitMove .fromInt (json);
671+
672+ int toJson () => value;
549673}
550674
551675enum SubmitMoveChoice {
0 commit comments