Riverpod 3 (#2381)

This commit is contained in:
Vincent Velociter
2025-11-24 10:44:29 +01:00
committed by GitHub
parent abfb523cd3
commit 696817c887
185 changed files with 2110 additions and 1644 deletions
+1
View File
@@ -16,6 +16,7 @@ analyzer:
strict-raw-types: true
exclude:
- "**/*.g.dart"
- "**/*.freezed.dart"
- lib/src/styles/lichess_icons.dart
- lib/src/styles/social_icons.dart
- lib/firebase_options.dart
-8
View File
@@ -13,11 +13,3 @@ targets:
options:
from_json: false
to_json: false
riverpod_generator:
generate_for:
- lib/src/localizations.dart
- lib/src/model/**/*.dart
- lib/src/network/*.dart
- lib/src/db/*.dart
- lib/src/quick_actions.dart
- lib/src/**/*_providers.dart
-7
View File
@@ -129,9 +129,6 @@ PODS:
- Flutter
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- PromisesObjC (2.4.0)
- PromisesSwift (2.4.0):
- PromisesObjC (= 2.4.0)
@@ -171,7 +168,6 @@ DEPENDENCIES:
- multistockfish_variant (from `.symlinks/plugins/multistockfish_variant/ios`)
- objective_c (from `.symlinks/plugins/objective_c/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- quick_actions_ios (from `.symlinks/plugins/quick_actions_ios/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
@@ -235,8 +231,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/objective_c/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
quick_actions_ios:
:path: ".symlinks/plugins/quick_actions_ios/ios"
share_plus:
@@ -284,7 +278,6 @@ SPEC CHECKSUMS:
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
objective_c: 89e720c30d716b036faf9c9684022048eee1eee2
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
quick_actions_ios: 500fcc11711d9f646739093395c4ae8eec25f779
+8 -11
View File
@@ -5,9 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
part 'database.g.dart';
const kLichessDatabaseName = 'lichess_mobile.db';
@@ -21,14 +19,14 @@ const kStorageAnonId = '**anonymous**';
final _logger = Logger('Database');
@Riverpod(keepAlive: true)
Future<Database> database(Ref ref) async {
/// A provider for the app [Database].
final databaseProvider = FutureProvider<Database>((Ref ref) async {
if (Platform.isLinux) {
databaseFactory = databaseFactoryFfi;
}
final dbPath = await _databasePath;
return openAppDatabase(databaseFactory, dbPath);
}
}, name: 'DatabaseProvider');
/// Returns the database path including filename.
Future<String> get _databasePath async {
@@ -41,11 +39,10 @@ Future<String> get _databasePath async {
}
/// Returns the sqlite version as an integer.
@Riverpod(keepAlive: true)
Future<int?> sqliteVersion(Ref ref) async {
final sqliteVersionProvider = FutureProvider<int?>((Ref ref) async {
final db = await ref.read(databaseProvider.future);
return _getDatabaseVersion(db);
}
}, name: 'SqliteVersionProvider');
Future<int?> _getDatabaseVersion(Database db) async {
try {
@@ -57,13 +54,13 @@ Future<int?> _getDatabaseVersion(Database db) async {
}
}
@Riverpod(keepAlive: true)
Future<int> getDbSizeInBytes(Ref ref) async {
/// A provider that returns the size of the database file in bytes.
final getDbSizeInBytesProvider = FutureProvider<int>((Ref ref) async {
final dbPath = join(await getDatabasesPath(), kLichessDatabaseName);
final dbFile = File(dbPath);
return dbFile.length();
}
}, name: 'GetDbSizeInBytesProvider');
/// Opens the app database.
Future<Database> openAppDatabase(DatabaseFactory dbFactory, String path) {
+3 -6
View File
@@ -3,22 +3,19 @@ import 'dart:io';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:path/path.dart' as p;
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:sqflite/sqflite.dart';
part 'openings_database.g.dart';
// The dataset is from https://github.com/lichess-org/chess-openings
// It can be updated by running the script at scripts/update_openings_db.py
const _kDatabaseVersion = 3;
const _kDatabaseName = 'chess_openings$_kDatabaseVersion.db';
@Riverpod(keepAlive: true)
Future<Database> openingsDatabase(Ref ref) async {
/// A provider for the openings database.
final openingsDatabaseProvider = FutureProvider<Database>((Ref ref) async {
final dbPath = p.join(await getDatabasesPath(), _kDatabaseName);
return _openDb(dbPath);
}
}, name: 'OpeningsDatabaseProvider');
Future<Database> _openDb(String path) async {
final exists = await databaseExists(path);
+8 -5
View File
@@ -1,14 +1,17 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lichess_mobile/l10n/l10n.dart';
import 'package:lichess_mobile/src/model/settings/general_preferences.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'localizations.g.dart';
typedef ActiveLocalizations = ({Locale locale, AppLocalizations strings});
@Riverpod(keepAlive: true)
class Localizations extends _$Localizations {
/// A provider for [Localizations].
final localizationsProvider = NotifierProvider<Localizations, ActiveLocalizations>(
Localizations.new,
name: 'LocalizationsProvider',
);
class Localizations extends Notifier<ActiveLocalizations> {
@override
ActiveLocalizations build() {
final generalPrefs = ref.watch(generalPreferencesProvider);
+11 -12
View File
@@ -27,26 +27,25 @@ void setupLogging() {
}
}
class ProviderLogger extends ProviderObserver {
final class ProviderLogger extends ProviderObserver {
final _logger = Logger('Provider');
@override
void didAddProvider(ProviderBase<Object?> provider, Object? value, ProviderContainer container) {
_logger.info('${provider.name ?? provider.runtimeType} initialized', value);
void didAddProvider(ProviderObserverContext context, Object? value) {
_logger.info('${context.provider.name ?? context.provider.runtimeType} initialized', value);
}
@override
void didDisposeProvider(ProviderBase<Object?> provider, ProviderContainer container) {
_logger.info('${provider.name ?? provider.runtimeType} disposed');
void didDisposeProvider(ProviderObserverContext context) {
_logger.info('${context.provider.name ?? context.provider.runtimeType} disposed');
}
@override
void providerDidFail(
ProviderBase<Object?> provider,
Object error,
StackTrace stackTrace,
ProviderContainer container,
) {
_logger.severe('${provider.name ?? provider.runtimeType} error', error, stackTrace);
void providerDidFail(ProviderObserverContext context, Object error, StackTrace stackTrace) {
_logger.severe(
'${context.provider.name ?? context.provider.runtimeType} error',
error,
stackTrace,
);
}
}
+21 -25
View File
@@ -4,9 +4,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lichess_mobile/l10n/l10n.dart';
import 'package:lichess_mobile/src/model/account/account_repository.dart';
import 'package:lichess_mobile/src/model/auth/auth_session.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'account_preferences.g.dart';
typedef AccountPrefState = ({
// game display
@@ -30,28 +27,22 @@ typedef AccountPrefState = ({
});
/// A provider that tells if the user wants to see ratings in the app.
@Riverpod(keepAlive: true)
Future<ShowRatings> showRatingsPref(Ref ref) async {
return ref.watch(
accountPreferencesProvider.selectAsync((state) => state?.showRatings ?? ShowRatings.yes),
);
}
final showRatingsPrefProvider = FutureProvider<ShowRatings>((Ref ref) async {
final prefs = await ref.watch(accountPreferencesProvider.future);
return prefs?.showRatings ?? defaultAccountPreferences.showRatings;
});
@Riverpod(keepAlive: true)
Future<bool> clockSound(Ref ref) async {
return ref.watch(
accountPreferencesProvider.selectAsync((state) => state?.clockSound.value ?? true),
);
}
/// 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;
});
@Riverpod(keepAlive: true)
Future<PieceNotation> pieceNotation(Ref ref) async {
return ref.watch(
accountPreferencesProvider.selectAsync(
(state) => state?.pieceNotation ?? defaultAccountPreferences.pieceNotation,
),
);
}
/// 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;
});
final defaultAccountPreferences = (
zenMode: Zen.no,
@@ -70,12 +61,17 @@ final defaultAccountPreferences = (
message: Message.always,
);
/// A provider that gives the account preferences for the current user.
final accountPreferencesProvider = AsyncNotifierProvider<AccountPreferences, AccountPrefState?>(
AccountPreferences.new,
name: 'AccountPreferencesProvider',
);
/// Get the account preferences for the current user.
///
/// The result is cached for the lifetime of the app, until refreshed.
/// If the server returns an error, default values are returned.
@Riverpod(keepAlive: true)
class AccountPreferences extends _$AccountPreferences {
class AccountPreferences extends AsyncNotifier<AccountPrefState?> {
@override
Future<AccountPrefState?> build() async {
final session = ref.watch(authSessionProvider);
@@ -17,10 +17,12 @@ final accountProvider = FutureProvider.autoDispose<User?>((Ref ref) {
}, name: 'AccountProvider');
/// A provider that fetches the current user's kid mode status.
final kidModeProvider = FutureProvider.autoDispose<bool>((ref) {
return ref.watch(accountProvider.selectAsync((user) => user?.kid ?? false));
final kidModeProvider = FutureProvider.autoDispose<bool>((ref) async {
final account = await ref.watch(accountProvider.future);
return account?.kid ?? false;
}, name: 'KidModeProvider');
/// A provider for the [AccountRepository].
final accountRepositoryProvider = Provider<AccountRepository>((ref) {
final client = ref.read(lichessClientProvider);
final aggregator = ref.read(aggregatorProvider);
+4 -7
View File
@@ -14,18 +14,15 @@ import 'package:lichess_mobile/src/model/user/user.dart' show TemporaryBan, User
import 'package:lichess_mobile/src/tab_scaffold.dart' show currentNavigatorKeyProvider;
import 'package:lichess_mobile/src/view/play/playban.dart';
import 'package:lichess_mobile/src/widgets/platform_alert_dialog.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'account_service.g.dart';
@Riverpod(keepAlive: true)
AccountService accountService(Ref ref) {
/// A provider for [AccountService].
final accountServiceProvider = Provider<AccountService>((Ref ref) {
final service = AccountService(ref);
ref.onDispose(() {
service.dispose();
});
return service;
}
}, name: 'AccountServiceProvider');
class AccountService {
AccountService(this._ref);
@@ -48,7 +45,7 @@ class AccountService {
final prefs = LichessBinding.instance.sharedPreferences;
_accountProviderSubscription = _ref.listen(accountProvider, (_, account) {
final playban = account.valueOrNull?.playban;
final playban = account.value?.playban;
final storedDate = prefs.getString(_storageKey);
final lastPlaybanNotificationDate = storedDate != null ? DateTime.parse(storedDate) : null;
+3 -6
View File
@@ -2,18 +2,15 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lichess_mobile/src/network/http.dart';
import 'package:lichess_mobile/src/utils/lichess_assets.dart';
import 'package:lichess_mobile/src/widgets/emoji_picker/emoji_picker_models.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'flair_provider.g.dart';
@Riverpod(keepAlive: true)
Future<EmojiData> flairList(Ref ref) async {
/// A provider that fetches the list of available flairs.
final flairListProvider = FutureProvider<EmojiData>((Ref ref) async {
final list = await ref
.read(defaultClientProvider)
.read(Uri.parse(lichessAssetUrl('flair/list.txt')));
final data = _makeEmojiData(list);
return EmojiData.fromJson(data);
}
}, name: 'FlairListProvider');
const categories = [
['smileys', 'Smileys'],
+7 -3
View File
@@ -1,15 +1,19 @@
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/model/account/home_widgets.dart';
import 'package:lichess_mobile/src/model/settings/preferences_storage.dart';
import 'package:lichess_mobile/src/model/user/user.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'home_preferences.freezed.dart';
part 'home_preferences.g.dart';
@Riverpod(keepAlive: true)
class HomePreferences extends _$HomePreferences with SessionPreferencesStorage<HomePrefs> {
final homePreferencesProvider = NotifierProvider<HomePreferences, HomePrefs>(
HomePreferences.new,
name: 'HomePreferencesProvider',
);
class HomePreferences extends Notifier<HomePrefs> with SessionPreferencesStorage<HomePrefs> {
@override
@protected
PrefCategory get prefCategory => PrefCategory.home;
@@ -4,6 +4,7 @@ import 'package:collection/collection.dart';
import 'package:dartchess/dartchess.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:intl/intl.dart';
import 'package:lichess_mobile/src/model/account/account_service.dart';
@@ -34,10 +35,8 @@ import 'package:lichess_mobile/src/network/http.dart';
import 'package:lichess_mobile/src/network/socket.dart';
import 'package:lichess_mobile/src/view/engine/engine_gauge.dart';
import 'package:lichess_mobile/src/widgets/pgn.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'analysis_controller.freezed.dart';
part 'analysis_controller.g.dart';
final _dateFormat = DateFormat('yyyy.MM.dd');
@@ -104,10 +103,20 @@ enum AnalysisGameResult {
};
}
@riverpod
class AnalysisController extends _$AnalysisController
/// A provider for [AnalysisController].
final analysisControllerProvider = AsyncNotifierProvider.autoDispose
.family<AnalysisController, AnalysisState, AnalysisOptions>(
AnalysisController.new,
name: 'AnalysisControllerProvider',
);
class AnalysisController extends AsyncNotifier<AnalysisState>
with EngineEvaluationMixin
implements PgnTreeNotifier {
AnalysisController(this.options);
final AnalysisOptions options;
static final Uri socketUri = Uri(path: '/analysis/socket/v5');
StreamSubscription<SocketEvent>? _socketSubscription;
@@ -143,7 +152,7 @@ class AnalysisController extends _$AnalysisController
GameRepository get _gameRepository => ref.read(gameRepositoryProvider);
@override
Future<AnalysisState> build(AnalysisOptions options) async {
Future<AnalysisState> build() async {
final serverAnalysisService = ref.watch(serverAnalysisServiceProvider);
ref.onDispose(() {
@@ -173,7 +182,7 @@ class AnalysisController extends _$AnalysisController
switch (options) {
case ArchivedGame(:final gameId):
{
archivedGame = await ref.read(archivedGameProvider(id: gameId).future);
archivedGame = await ref.read(archivedGameProvider(gameId).future);
_variant = archivedGame!.meta.variant;
if (!_variant.isReadSupported) {
throw UnsupportedVariantException(_variant, gameId);
@@ -1,13 +1,17 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/model/analysis/common_analysis_prefs.dart';
import 'package:lichess_mobile/src/model/settings/preferences_storage.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'analysis_preferences.freezed.dart';
part 'analysis_preferences.g.dart';
@Riverpod(keepAlive: true)
class AnalysisPreferences extends _$AnalysisPreferences with PreferencesStorage<AnalysisPrefs> {
final analysisPreferencesProvider = NotifierProvider<AnalysisPreferences, AnalysisPrefs>(
AnalysisPreferences.new,
name: 'AnalysisPreferencesProvider',
);
class AnalysisPreferences extends Notifier<AnalysisPrefs> with PreferencesStorage<AnalysisPrefs> {
@override
@protected
final prefCategory = PrefCategory.analysis;
+3 -6
View File
@@ -2,11 +2,8 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lichess_mobile/src/db/openings_database.dart';
import 'package:lichess_mobile/src/model/common/chess.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:sqflite/sqflite.dart';
part 'opening_service.g.dart';
const kOpeningAllowedVariants = ISetConst({
Variant.standard,
Variant.kingOfTheHill,
@@ -14,10 +11,10 @@ const kOpeningAllowedVariants = ISetConst({
Variant.crazyhouse,
});
@Riverpod(keepAlive: true)
OpeningService openingService(Ref ref) {
/// A provider for [OpeningService].
final openingServiceProvider = Provider<OpeningService>((Ref ref) {
return OpeningService(ref);
}
}, name: 'OpeningServiceProvider');
class OpeningService {
OpeningService(this._ref);
+18 -9
View File
@@ -4,6 +4,7 @@ import 'dart:math';
import 'package:dartchess/dartchess.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart';
import 'package:lichess_mobile/src/model/analysis/common_analysis_state.dart';
@@ -25,10 +26,8 @@ import 'package:lichess_mobile/src/model/game/game_repository_providers.dart';
import 'package:lichess_mobile/src/network/socket.dart';
import 'package:lichess_mobile/src/view/engine/engine_gauge.dart';
import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'retro_controller.freezed.dart';
part 'retro_controller.g.dart';
typedef RetroOptions = ({GameId id, Side initialSide});
@@ -74,8 +73,18 @@ const double _kCorrectMovePovDiffThreshold = -0.04;
/// Threshold for considering a move a mistake due to how it affected the evaluation.
const double _kEvalSwingThreshold = 0.1;
@riverpod
class RetroController extends _$RetroController with EngineEvaluationMixin {
/// A provider for [RetroController].
final retroControllerProvider = AsyncNotifierProvider.autoDispose
.family<RetroController, RetroState, RetroOptions>(
RetroController.new,
name: 'RetroControllerProvider',
);
class RetroController extends AsyncNotifier<RetroState> with EngineEvaluationMixin {
RetroController(this.options);
final RetroOptions options;
late Root _root;
late ExportedGame _game;
@@ -109,7 +118,7 @@ class RetroController extends _$RetroController with EngineEvaluationMixin {
Node get positionTree => _root;
@override
Future<RetroState> build(RetroOptions options) async {
Future<RetroState> build() async {
final serverAnalysisService = ref.watch(serverAnalysisServiceProvider);
ref.onDispose(() {
@@ -119,7 +128,7 @@ class RetroController extends _$RetroController with EngineEvaluationMixin {
socketClient = ref.watch(socketPoolProvider).open(AnalysisController.socketUri);
_game = await ref.read(archivedGameProvider(id: options.id).future);
_game = await ref.read(archivedGameProvider(options.id).future);
if (engineSupportedVariants.contains(_game.meta.variant) == false) {
throw Exception('Variant ${_game.meta.variant} is not supported for retro mode');
@@ -270,7 +279,7 @@ class RetroController extends _$RetroController with EngineEvaluationMixin {
}
void onPromotionSelection(Role? role) {
final state = this.state.valueOrNull;
final state = this.state.value;
if (state == null) return;
if (role == null) {
@@ -297,7 +306,7 @@ class RetroController extends _$RetroController with EngineEvaluationMixin {
}
void viewSolution() {
final currentMistake = state.valueOrNull?.currentMistake;
final currentMistake = state.value?.currentMistake;
if (currentMistake != null) {
onUserMove(currentMistake.serverMove);
state = AsyncValue.data(state.requireValue.copyWith(feedback: RetroFeedback.viewingSolution));
@@ -352,7 +361,7 @@ class RetroController extends _$RetroController with EngineEvaluationMixin {
/// Whether the user is navigating through the moves (as opposed to playing a move).
bool isNavigating = false,
}) {
final state = this.state.valueOrNull;
final state = this.state.value;
if (state == null) return;
final pathChange = state.currentPath != path;
@@ -15,16 +15,13 @@ import 'package:lichess_mobile/src/model/game/game_repository.dart';
import 'package:lichess_mobile/src/model/game/game_socket_events.dart';
import 'package:lichess_mobile/src/network/http.dart';
import 'package:lichess_mobile/src/network/socket.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'server_analysis_service.g.dart';
const Duration kMaxWaitForServerAnalysis = Duration(minutes: 1);
@Riverpod(keepAlive: true)
ServerAnalysisService serverAnalysisService(Ref ref) {
/// A provider for [ServerAnalysisService].
final serverAnalysisServiceProvider = Provider<ServerAnalysisService>((Ref ref) {
return ServerAnalysisService(ref);
}
}, name: 'ServerAnalysisServiceProvider');
class ServerAnalysisService {
ServerAnalysisService(this.ref);
@@ -177,8 +174,13 @@ class ServerAnalysisService {
}
}
@riverpod
class CurrentAnalysis extends _$CurrentAnalysis {
/// A provider that exposes the current game being analyzed by the server.
final currentAnalysisProvider = NotifierProvider.autoDispose<CurrentAnalysis, GameId?>(
CurrentAnalysis.new,
name: 'CurrentAnalysisProvider',
);
class CurrentAnalysis extends Notifier<GameId?> {
@override
GameId? build() {
final listenable = ref.watch(serverAnalysisServiceProvider).currentAnalysis;
+9 -6
View File
@@ -1,17 +1,20 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lichess_mobile/src/model/auth/auth_repository.dart';
import 'package:lichess_mobile/src/model/auth/auth_session.dart';
import 'package:lichess_mobile/src/model/notifications/notification_service.dart';
import 'package:lichess_mobile/src/network/http.dart';
import 'package:lichess_mobile/src/network/socket.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'auth_controller.g.dart';
/// A provider for [AuthController].
final authControllerProvider = AsyncNotifierProvider.autoDispose<AuthController, void>(
AuthController.new,
name: 'AuthControllerProvider',
);
@riverpod
class AuthController extends _$AuthController {
class AuthController extends AsyncNotifier<void> {
@override
AsyncValue<void> build() {
return const AsyncValue.data(null);
Future<void> build() {
return Future.value();
}
Future<void> signIn() async {
+3 -6
View File
@@ -7,17 +7,14 @@ import 'package:lichess_mobile/src/model/auth/bearer.dart';
import 'package:lichess_mobile/src/model/user/user.dart';
import 'package:lichess_mobile/src/network/http.dart';
import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'auth_repository.g.dart';
const redirectUri = 'org.lichess.mobile://login-callback';
const oauthScopes = ['web:mobile'];
@Riverpod(keepAlive: true)
FlutterAppAuth appAuth(Ref ref) {
/// A provider for [FlutterAppAuth].
final appAuthProvider = Provider<FlutterAppAuth>((Ref ref) {
return const FlutterAppAuth();
}
}, name: 'AppAuthProvider');
class AuthRepository {
AuthRepository(LichessClient client, FlutterAppAuth appAuth)
+12 -8
View File
@@ -3,13 +3,22 @@ import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/model/auth/session_storage.dart';
import 'package:lichess_mobile/src/model/common/preloaded_data.dart';
import 'package:lichess_mobile/src/model/user/user.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'auth_session.freezed.dart';
part 'auth_session.g.dart';
@Riverpod(keepAlive: true)
class AuthSession extends _$AuthSession {
/// A provider for [AuthSession].
final authSessionProvider = NotifierProvider<AuthSession, AuthSessionState?>(
AuthSession.new,
name: 'AuthSessionProvider',
);
/// A provider that indicates whether the user is logged in.
final isLoggedInProvider = Provider.autoDispose<bool>((Ref ref) {
return ref.watch(authSessionProvider.select((authSession) => authSession != null));
}, name: 'IsLoggedInProvider');
class AuthSession extends Notifier<AuthSessionState?> {
@override
AuthSessionState? build() {
return ref.read(preloadedDataProvider).requireValue.userSession;
@@ -35,8 +44,3 @@ sealed class AuthSessionState with _$AuthSessionState {
factory AuthSessionState.fromJson(Map<String, dynamic> json) => _$AuthSessionStateFromJson(json);
}
@riverpod
bool isLoggedIn(Ref ref) {
return ref.watch(authSessionProvider.select((authSession) => authSession != null));
}
+3 -6
View File
@@ -4,16 +4,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lichess_mobile/src/constants.dart';
import 'package:lichess_mobile/src/db/secure_storage.dart';
import 'package:lichess_mobile/src/model/auth/auth_session.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'session_storage.g.dart';
const kSessionStorageKey = '$kLichessHost.userSession';
@Riverpod(keepAlive: true)
SessionStorage sessionStorage(Ref ref) {
/// A provider for [SessionStorage].
final sessionStorageProvider = Provider<SessionStorage>((Ref ref) {
return const SessionStorage();
}
}, name: 'SessionStorageProvider');
class SessionStorage {
const SessionStorage();
@@ -1,16 +1,25 @@
import 'package:chessground/chessground.dart';
import 'package:dartchess/dartchess.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'board_editor_controller.freezed.dart';
part 'board_editor_controller.g.dart';
@riverpod
class BoardEditorController extends _$BoardEditorController {
/// A provider for [BoardEditorController].
final boardEditorControllerProvider = NotifierProvider.autoDispose
.family<BoardEditorController, BoardEditorState, String?>(
BoardEditorController.new,
name: 'BoardEditorControllerProvider',
);
class BoardEditorController extends Notifier<BoardEditorState> {
BoardEditorController(this.initialFen);
final String? initialFen;
@override
BoardEditorState build(String? initialFen) {
BoardEditorState build() {
final setup = Setup.parseFen(initialFen ?? kInitialFEN);
final position = Chess.fromSetup(setup, ignoreImpossibleCheck: true);
final pieces = readFen(initialFen ?? kInitialFEN).lock;
@@ -4,6 +4,7 @@ import 'package:dartchess/dartchess.dart';
import 'package:deep_pick/deep_pick.dart';
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/src/model/analysis/analysis_controller.dart';
import 'package:lichess_mobile/src/model/analysis/common_analysis_state.dart';
@@ -24,15 +25,25 @@ import 'package:lichess_mobile/src/utils/json.dart';
import 'package:lichess_mobile/src/utils/rate_limit.dart';
import 'package:lichess_mobile/src/view/engine/engine_gauge.dart';
import 'package:lichess_mobile/src/widgets/pgn.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'broadcast_analysis_controller.freezed.dart';
part 'broadcast_analysis_controller.g.dart';
@riverpod
class BroadcastAnalysisController extends _$BroadcastAnalysisController
typedef BroadcastAnalysisControllerParams = ({BroadcastRoundId roundId, BroadcastGameId gameId});
/// A provider for [BroadcastAnalysisController].
final broadcastAnalysisControllerProvider = AsyncNotifierProvider.autoDispose
.family<BroadcastAnalysisController, BroadcastAnalysisState, BroadcastAnalysisControllerParams>(
BroadcastAnalysisController.new,
name: 'BroadcastAnalysisControllerProvider',
);
class BroadcastAnalysisController extends AsyncNotifier<BroadcastAnalysisState>
with EngineEvaluationMixin
implements PgnTreeNotifier {
BroadcastAnalysisController(this.params);
final BroadcastAnalysisControllerParams params;
static Uri broadcastSocketUri(BroadcastRoundId broadcastRoundId) =>
Uri(path: 'study/$broadcastRoundId/socket/v6');
@@ -75,7 +86,7 @@ class BroadcastAnalysisController extends _$BroadcastAnalysisController
Root get positionTree => _root;
@override
Future<BroadcastAnalysisState> build(BroadcastRoundId roundId, BroadcastGameId gameId) async {
Future<BroadcastAnalysisState> build() async {
ref.onDispose(() {
_key = null;
_subscription?.cancel();
@@ -88,14 +99,14 @@ class BroadcastAnalysisController extends _$BroadcastAnalysisController
_socketClient = ref
.watch(socketPoolProvider)
.open(BroadcastAnalysisController.broadcastSocketUri(roundId));
.open(BroadcastAnalysisController.broadcastSocketUri(params.roundId));
_subscription = _socketClient.stream.listen(_handleSocketEvent);
await _socketClient.firstConnection;
_socketOpenSubscription = _socketClient.connectedStream.listen((_) {
if (state.valueOrNull?.isNewOrOngoing == true) {
if (state.value?.isNewOrOngoing == true) {
_syncDebouncer(() {
_reloadPgn();
});
@@ -104,7 +115,7 @@ class BroadcastAnalysisController extends _$BroadcastAnalysisController
_appLifecycleListener = AppLifecycleListener(
onResume: () {
if (state.valueOrNull?.isNewOrOngoing == true) {
if (state.value?.isNewOrOngoing == true) {
_syncDebouncer(() {
_reloadPgn();
});
@@ -112,7 +123,9 @@ class BroadcastAnalysisController extends _$BroadcastAnalysisController
},
);
final pgn = await ref.read(broadcastRepositoryProvider).getGamePgn(roundId, gameId);
final pgn = await ref
.read(broadcastRepositoryProvider)
.getGamePgn(params.roundId, params.gameId);
final game = PgnGame.parsePgn(pgn);
final pgnHeaders = IMap(game.headers);
@@ -128,7 +141,7 @@ class BroadcastAnalysisController extends _$BroadcastAnalysisController
final prefs = ref.read(broadcastPreferencesProvider);
final broadcastState = BroadcastAnalysisState(
id: gameId,
id: params.gameId,
currentPath: currentPath,
broadcastPath: currentPath,
isOnMainline: _root.isOnMainline(currentPath),
@@ -163,7 +176,9 @@ class BroadcastAnalysisController extends _$BroadcastAnalysisController
if (!state.hasValue) return;
final key = _key;
final pgn = await ref.read(broadcastRepositoryProvider).getGamePgn(roundId, gameId);
final pgn = await ref
.read(broadcastRepositoryProvider)
.getGamePgn(params.roundId, params.gameId);
// check provider is still mounted
if (key == _key) {
@@ -235,7 +250,7 @@ class BroadcastAnalysisController extends _$BroadcastAnalysisController
final broadcastGameId = pick(event.data, 'p', 'chapterId').asBroadcastGameIdOrThrow();
// We check if the event is for this game
if (broadcastGameId != gameId) return;
if (broadcastGameId != params.gameId) return;
// The path of the last and current move of the broadcasted game
// Its value is "!" if the path is identical to one of the node that was received
@@ -276,7 +291,7 @@ class BroadcastAnalysisController extends _$BroadcastAnalysisController
final broadcastGameId = pick(event.data, 'chapterId').asBroadcastGameIdOrThrow();
// We check if the event is for this game
if (broadcastGameId != gameId) return;
if (broadcastGameId != params.gameId) return;
final pgnHeadersEntries = pick(
event.data,
@@ -1,14 +1,19 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/model/analysis/common_analysis_prefs.dart';
import 'package:lichess_mobile/src/model/settings/preferences_storage.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'broadcast_preferences.freezed.dart';
part 'broadcast_preferences.g.dart';
@Riverpod(keepAlive: true)
class BroadcastPreferences extends _$BroadcastPreferences with PreferencesStorage<BroadcastPrefs> {
final broadcastPreferencesProvider = NotifierProvider<BroadcastPreferences, BroadcastPrefs>(
BroadcastPreferences.new,
name: 'BroadcastPreferencesProvider',
);
class BroadcastPreferences extends Notifier<BroadcastPrefs>
with PreferencesStorage<BroadcastPrefs> {
@override
@protected
PrefCategory get prefCategory => PrefCategory.broadcast;
@@ -3,13 +3,15 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lichess_mobile/src/model/broadcast/broadcast.dart';
import 'package:lichess_mobile/src/model/broadcast/broadcast_repository.dart';
import 'package:lichess_mobile/src/model/common/id.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'broadcast_providers.g.dart';
/// A provider that fetches a paginated list of broadcasts.
@riverpod
class BroadcastsPaginator extends _$BroadcastsPaginator {
final broadcastsPaginatorProvider =
AsyncNotifierProvider.autoDispose<BroadcastsPaginator, BroadcastList>(
BroadcastsPaginator.new,
name: 'BroadcastsPaginatorProvider',
);
class BroadcastsPaginator extends AsyncNotifier<BroadcastList> {
@override
Future<BroadcastList> build() {
return ref.read(broadcastRepositoryProvider).getBroadcasts();
@@ -35,10 +37,19 @@ class BroadcastsPaginator extends _$BroadcastsPaginator {
}
/// A provider that fetches a paginated list of broadcasts matching the [searchTerm].
@riverpod
class BroadcastsSearchPaginator extends _$BroadcastsSearchPaginator {
final broadcastsSearchPaginatorProvider = AsyncNotifierProvider.autoDispose
.family<BroadcastsSearchPaginator, BroadcastSearchList, String>(
BroadcastsSearchPaginator.new,
name: 'BroadcastsSearchPaginatorProvider',
);
class BroadcastsSearchPaginator extends AsyncNotifier<BroadcastSearchList> {
BroadcastsSearchPaginator(this.searchTerm);
final String searchTerm;
@override
Future<BroadcastSearchList> build(String searchTerm) {
Future<BroadcastSearchList> build() {
return ref.read(broadcastRepositoryProvider).searchBroadcasts(searchTerm: searchTerm);
}
@@ -60,37 +71,36 @@ class BroadcastsSearchPaginator extends _$BroadcastsSearchPaginator {
}
}
@riverpod
Future<BroadcastTournament> broadcastTournament(
Ref ref,
BroadcastTournamentId broadcastTournamentId,
) {
return ref.read(broadcastRepositoryProvider).getTournament(broadcastTournamentId);
}
final broadcastTournamentProvider = FutureProvider.autoDispose
.family<BroadcastTournament, BroadcastTournamentId>((
Ref ref,
BroadcastTournamentId broadcastTournamentId,
) {
return ref.read(broadcastRepositoryProvider).getTournament(broadcastTournamentId);
}, name: 'BroadcastTournamentProvider');
@riverpod
Future<BroadcastRoundResponse> broadcastRound(Ref ref, BroadcastRoundId roundId) {
return ref.read(broadcastRepositoryProvider).getRound(roundId);
}
final broadcastRoundProvider = FutureProvider.autoDispose
.family<BroadcastRoundResponse, BroadcastRoundId>((Ref ref, BroadcastRoundId roundId) {
return ref.read(broadcastRepositoryProvider).getRound(roundId);
}, name: 'BroadcastRoundProvider');
@riverpod
Future<IList<BroadcastPlayerWithOverallResult>> broadcastPlayers(
Ref ref,
BroadcastTournamentId tournamentId,
) {
return ref.read(broadcastRepositoryProvider).getPlayers(tournamentId);
}
final broadcastPlayersProvider = FutureProvider.autoDispose
.family<IList<BroadcastPlayerWithOverallResult>, BroadcastTournamentId>((
Ref ref,
BroadcastTournamentId tournamentId,
) {
return ref.read(broadcastRepositoryProvider).getPlayers(tournamentId);
}, name: 'BroadcastPlayersProvider');
@riverpod
Future<BroadcastPlayerWithGameResults> broadcastPlayer(
Ref ref,
BroadcastTournamentId broadcastTournamentId,
String playerId,
) {
return ref.read(broadcastRepositoryProvider).getPlayerResults(broadcastTournamentId, playerId);
}
final broadcastPlayerProvider = FutureProvider.autoDispose
.family<BroadcastPlayerWithGameResults, (BroadcastTournamentId, String)>((
Ref ref,
(BroadcastTournamentId, String) params,
) {
return ref.read(broadcastRepositoryProvider).getPlayerResults(params.$1, params.$2);
}, name: 'BroadcastPlayerProvider');
@riverpod
Future<IList<BroadcastTeamMatch>> broadcastTeamMatches(Ref ref, BroadcastRoundId roundId) {
return ref.read(broadcastRepositoryProvider).getTeamMatches(roundId);
}
final broadcastTeamMatchesProvider = FutureProvider.autoDispose
.family<IList<BroadcastTeamMatch>, BroadcastRoundId>((Ref ref, BroadcastRoundId roundId) {
return ref.read(broadcastRepositoryProvider).getTeamMatches(roundId);
}, name: 'BroadcastTeamMatchesProvider');
@@ -4,6 +4,7 @@ import 'package:dartchess/dartchess.dart';
import 'package:deep_pick/deep_pick.dart';
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/src/model/broadcast/broadcast.dart';
import 'package:lichess_mobile/src/model/broadcast/broadcast_preferences.dart';
@@ -15,13 +16,21 @@ import 'package:lichess_mobile/src/model/common/socket.dart';
import 'package:lichess_mobile/src/network/socket.dart';
import 'package:lichess_mobile/src/utils/json.dart';
import 'package:lichess_mobile/src/utils/rate_limit.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'broadcast_round_controller.freezed.dart';
part 'broadcast_round_controller.g.dart';
@riverpod
class BroadcastRoundController extends _$BroadcastRoundController {
/// A provider for [BroadcastRoundController].
final broadcastRoundControllerProvider = AsyncNotifierProvider.autoDispose
.family<BroadcastRoundController, BroadcastRoundState, BroadcastRoundId>(
BroadcastRoundController.new,
name: 'BroadcastRoundControllerProvider',
);
class BroadcastRoundController extends AsyncNotifier<BroadcastRoundState> {
BroadcastRoundController(this.broadcastRoundId);
final BroadcastRoundId broadcastRoundId;
static Uri broadcastSocketUri(BroadcastRoundId broadcastRoundId) =>
Uri(path: 'study/$broadcastRoundId/socket/v6');
@@ -37,7 +46,7 @@ class BroadcastRoundController extends _$BroadcastRoundController {
Object? _key = Object();
@override
Future<BroadcastRoundState> build(BroadcastRoundId broadcastRoundId) async {
Future<BroadcastRoundState> build() async {
ref.onDispose(() {
_key = null;
_subscription?.cancel();
@@ -56,7 +65,7 @@ class BroadcastRoundController extends _$BroadcastRoundController {
await _socketClient.firstConnection;
_socketOpenSubscription = _socketClient.connectedStream.listen((_) {
if (state.valueOrNull?.round.status == RoundStatus.live) {
if (state.value?.round.status == RoundStatus.live) {
_syncRoundDebouncer(() {
_syncRound();
});
@@ -65,7 +74,7 @@ class BroadcastRoundController extends _$BroadcastRoundController {
_appLifecycleListener = AppLifecycleListener(
onResume: () {
if (state.valueOrNull?.round.status == RoundStatus.live) {
if (state.value?.round.status == RoundStatus.live) {
_syncRoundDebouncer(() {
_syncRound();
});
@@ -254,7 +263,7 @@ class BroadcastRoundController extends _$BroadcastRoundController {
}
void addObservedGame(BroadcastGameId gameId) {
if (state.valueOrNull?.games.containsKey(gameId) != true) return;
if (state.value?.games.containsKey(gameId) != true) return;
state = AsyncData(
state.requireValue.copyWith(observedGames: state.requireValue.observedGames.add(gameId)),
@@ -1,3 +1,4 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/model/challenge/challenge.dart';
import 'package:lichess_mobile/src/model/common/chess.dart';
@@ -6,13 +7,16 @@ import 'package:lichess_mobile/src/model/common/speed.dart';
import 'package:lichess_mobile/src/model/common/time_increment.dart';
import 'package:lichess_mobile/src/model/settings/preferences_storage.dart';
import 'package:lichess_mobile/src/model/user/user.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'challenge_preferences.freezed.dart';
part 'challenge_preferences.g.dart';
@Riverpod(keepAlive: true)
class ChallengePreferences extends _$ChallengePreferences
final challengePreferencesProvider = NotifierProvider<ChallengePreferences, ChallengePrefs>(
ChallengePreferences.new,
name: 'ChallengePreferencesProvider',
);
class ChallengePreferences extends Notifier<ChallengePrefs>
with SessionPreferencesStorage<ChallengePrefs> {
@override
@protected
@@ -7,14 +7,11 @@ import 'package:lichess_mobile/src/model/challenge/challenge.dart';
import 'package:lichess_mobile/src/model/common/id.dart';
import 'package:lichess_mobile/src/network/aggregator.dart';
import 'package:lichess_mobile/src/network/http.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'challenge_repository.g.dart';
@Riverpod(keepAlive: true)
ChallengeRepository challengeRepository(Ref ref) {
/// A provider for [ChallengeRepository].
final challengeRepositoryProvider = Provider<ChallengeRepository>((Ref ref) {
return ChallengeRepository(ref.read(lichessClientProvider), ref.read(aggregatorProvider));
}
}, name: 'ChallengeRepositoryProvider');
typedef ChallengesList = ({IList<Challenge> inward, IList<Challenge> outward});
@@ -20,17 +20,14 @@ import 'package:lichess_mobile/src/view/game/game_screen_providers.dart';
import 'package:lichess_mobile/src/view/user/challenge_requests_screen.dart';
import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart';
import 'package:lichess_mobile/src/widgets/feedback.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:stream_transform/stream_transform.dart';
part 'challenge_service.g.dart';
@Riverpod(keepAlive: true)
ChallengeService challengeService(Ref ref) {
/// A provider for [ChallengeService].
final challengeServiceProvider = Provider<ChallengeService>((Ref ref) {
final service = ChallengeService(ref);
ref.onDispose(() => service.dispose());
return service;
}
}, name: 'ChallengeServiceProvider');
/// A service that listens to challenge events and shows notifications.
class ChallengeService {
+6 -4
View File
@@ -1,16 +1,18 @@
import 'dart:async';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lichess_mobile/src/model/auth/auth_session.dart';
import 'package:lichess_mobile/src/model/challenge/challenge.dart';
import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart';
import 'package:lichess_mobile/src/model/challenge/challenge_service.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'challenges.g.dart';
final challengesProvider = AsyncNotifierProvider.autoDispose<Challenges, ChallengesList>(
Challenges.new,
name: 'ChallengesProvider',
);
@riverpod
class Challenges extends _$Challenges {
class Challenges extends AsyncNotifier<ChallengesList> {
StreamSubscription<ChallengesList>? _subscription;
@override
+29 -19
View File
@@ -16,11 +16,9 @@ import 'package:lichess_mobile/src/model/tournament/tournament_controller.dart';
import 'package:lichess_mobile/src/model/user/user.dart';
import 'package:lichess_mobile/src/network/http.dart';
import 'package:lichess_mobile/src/network/socket.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:sqflite/sqflite.dart';
part 'chat_controller.freezed.dart';
part 'chat_controller.g.dart';
const _tableName = 'chat_read_messages';
String _storeKey(StringId id) => 'chat.$id';
@@ -79,21 +77,33 @@ abstract class StudyChatOptions extends ChatOptions with _$StudyChatOptions {
}
/// A provider that gets the chat unread messages
@riverpod
Future<int> chatUnread(Ref ref, ChatOptions options) async {
return ref.watch(chatControllerProvider(options).selectAsync((s) => s.unreadMessages));
}
final chatUnreadProvider = FutureProvider.autoDispose.family<int, ChatOptions>((
Ref ref,
ChatOptions options,
) async {
return (await ref.watch(chatControllerProvider(options).future)).unreadMessages;
}, name: 'ChatUnreadProvider');
const IList<ChatMessage> _kEmptyMessages = IListConst([]);
@riverpod
class ChatController extends _$ChatController {
/// A provider for [ChatController].
final chatControllerProvider = AsyncNotifierProvider.autoDispose
.family<ChatController, ChatState, ChatOptions>(
ChatController.new,
name: 'ChatControllerProvider',
);
class ChatController extends AsyncNotifier<ChatState> {
ChatController(this.options);
final ChatOptions options;
StreamSubscription<SocketEvent>? _subscription;
LightUser? get _me => ref.read(authSessionProvider)?.user;
@override
Future<ChatState> build(ChatOptions options) async {
Future<ChatState> build() async {
_subscription?.cancel();
_subscription = socketGlobalStream.listen(_handleSocketEvent);
@@ -101,16 +111,16 @@ class ChatController extends _$ChatController {
_subscription?.cancel();
});
final initialMessages = await switch (options) {
GameChatOptions(:final id) => ref.watch(
gameControllerProvider(id).selectAsync((s) => s.game.chat?.lines),
),
TournamentChatOptions(:final id) => ref.watch(
tournamentControllerProvider(id).selectAsync((s) => s.tournament.chat?.lines),
),
StudyChatOptions(:final id) => ref.watch(
studyControllerProvider(id).selectAsync((s) => s.study.chat?.lines),
),
final initialMessages = switch (options) {
GameChatOptions(:final id) => (await ref.watch(
gameControllerProvider(id).future,
)).game.chat?.lines,
TournamentChatOptions(:final id) => (await ref.watch(
tournamentControllerProvider(id).future,
)).tournament.chat?.lines,
StudyChatOptions(:final id) => (await ref.watch(
studyControllerProvider(id).future,
)).study.chat?.lines,
};
final filteredMessages = _selectMessages(initialMessages ?? _kEmptyMessages);
@@ -1,13 +1,12 @@
import 'package:dartchess/dartchess.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/model/clock/chess_clock.dart';
import 'package:lichess_mobile/src/model/common/service/sound_service.dart';
import 'package:lichess_mobile/src/model/common/time_increment.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'clock_tool_controller.freezed.dart';
part 'clock_tool_controller.g.dart';
enum ClockSide {
top,
@@ -18,8 +17,13 @@ enum ClockSide {
Side get chessClockSide => this == ClockSide.top ? Side.black : Side.white;
}
@riverpod
class ClockToolController extends _$ClockToolController {
/// A provider for [ClockToolController].
final clockToolControllerProvider = NotifierProvider.autoDispose<ClockToolController, ClockState>(
ClockToolController.new,
name: 'ClockToolControllerProvider',
);
class ClockToolController extends Notifier<ClockState> {
late ChessClock _clock;
final Map<ClockSide, bool> _hasPlayedLowTimeSound = {
ClockSide.top: false,
+3 -6
View File
@@ -12,9 +12,6 @@ import 'package:lichess_mobile/src/utils/system.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path_provider/path_provider.dart'
show getApplicationDocumentsDirectory, getApplicationSupportDirectory;
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'preloaded_data.g.dart';
typedef PreloadedData = ({
PackageInfo packageInfo,
@@ -26,8 +23,8 @@ typedef PreloadedData = ({
Directory? appSupportDirectory,
});
@Riverpod(keepAlive: true)
Future<PreloadedData> preloadedData(Ref ref) async {
/// A provider that preloads various data needed throughout the app.
final preloadedDataProvider = FutureProvider<PreloadedData>((Ref ref) async {
final sessionStorage = ref.watch(sessionStorageProvider);
final pInfo = await PackageInfo.fromPlatform();
@@ -74,4 +71,4 @@ Future<PreloadedData> preloadedData(Ref ref) async {
appDocumentsDirectory: appDocumentsDirectory,
appSupportDirectory: appSupportDirectory,
);
}
}, name: 'PreloadedDataProvider');
@@ -2,15 +2,12 @@ import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lichess_mobile/src/model/common/service/sound_service.dart';
import 'package:lichess_mobile/src/model/settings/board_preferences.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'move_feedback.g.dart';
@Riverpod(keepAlive: true)
MoveFeedbackService moveFeedbackService(Ref ref) {
/// A provider for [MoveFeedbackService].
final moveFeedbackServiceProvider = Provider<MoveFeedbackService>((Ref ref) {
final soundService = ref.watch(soundServiceProvider);
return MoveFeedbackService(soundService, ref);
}
}, name: 'MoveFeedbackServiceProvider');
class MoveFeedbackService {
MoveFeedbackService(this._soundService, this._ref);
@@ -7,11 +7,8 @@ import 'package:lichess_mobile/src/binding.dart';
import 'package:lichess_mobile/src/model/settings/general_preferences.dart';
import 'package:lichess_mobile/src/model/settings/preferences_storage.dart';
import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:sound_effect/sound_effect.dart';
part 'sound_service.g.dart';
/// Maximum number of concurrent sounds that can be played.
const _kMaxConcurrentStreams = 2;
@@ -22,12 +19,12 @@ final _logger = Logger('SoundService');
// Must match name of files in assets/sounds/standard
enum Sound { move, capture, lowTime, dong, error, confirmation, puzzleStormEnd, clock, berserk }
@Riverpod(keepAlive: true)
SoundService soundService(Ref ref) {
/// A provider for [SoundService].
final soundServiceProvider = Provider<SoundService>((Ref ref) {
final service = SoundService(ref);
ref.onDispose(() => service.release());
return service;
}
}, name: 'SoundServiceProvider');
final _extension = defaultTargetPlatform == TargetPlatform.iOS ? 'aifc' : 'mp3';
@@ -2,18 +2,22 @@ import 'dart:async';
import 'dart:math';
import 'package:dartchess/dartchess.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/model/common/game.dart';
import 'package:lichess_mobile/src/model/coordinate_training/coordinate_training_preferences.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'coordinate_training_controller.freezed.dart';
part 'coordinate_training_controller.g.dart';
enum Guess { correct, incorrect }
@riverpod
class CoordinateTrainingController extends _$CoordinateTrainingController {
final coordinateTrainingControllerProvider =
NotifierProvider.autoDispose<CoordinateTrainingController, CoordinateTrainingState>(
CoordinateTrainingController.new,
name: 'CoordinateTrainingControllerProvider',
);
class CoordinateTrainingController extends Notifier<CoordinateTrainingState> {
final _random = Random(DateTime.now().millisecondsSinceEpoch);
final _stopwatch = Stopwatch();
@@ -1,15 +1,20 @@
import 'package:flutter/material.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/model/common/game.dart';
import 'package:lichess_mobile/src/model/settings/preferences_storage.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'coordinate_training_preferences.freezed.dart';
part 'coordinate_training_preferences.g.dart';
@Riverpod(keepAlive: true)
class CoordinateTrainingPreferences extends _$CoordinateTrainingPreferences
final coordinateTrainingPreferencesProvider =
NotifierProvider<CoordinateTrainingPreferences, CoordinateTrainingPrefs>(
CoordinateTrainingPreferences.new,
name: 'CoordinateTrainingPreferencesProvider',
);
class CoordinateTrainingPreferences extends Notifier<CoordinateTrainingPrefs>
with PreferencesStorage<CoordinateTrainingPrefs> {
@override
@protected
@@ -7,34 +7,32 @@ import 'package:lichess_mobile/src/db/database.dart';
import 'package:lichess_mobile/src/model/auth/auth_session.dart';
import 'package:lichess_mobile/src/model/common/id.dart';
import 'package:lichess_mobile/src/model/correspondence/offline_correspondence_game.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:sqflite/sqflite.dart';
part 'correspondence_game_storage.g.dart';
@Riverpod(keepAlive: true)
Future<CorrespondenceGameStorage> correspondenceGameStorage(Ref ref) async {
final db = await ref.watch(databaseProvider.future);
return CorrespondenceGameStorage(db, ref);
}
@riverpod
Future<IList<(DateTime, OfflineCorrespondenceGame)>> offlineOngoingCorrespondenceGames(
/// A provider for [CorrespondenceGameStorage].
final correspondenceGameStorageProvider = FutureProvider<CorrespondenceGameStorage>((
Ref ref,
) async {
final session = ref.watch(authSessionProvider);
// cannot use ref.watch because it would create a circular dependency
// as we invalidate this provider in the storage save and delete methods
final storage = await ref.read(correspondenceGameStorageProvider.future);
final data = await storage.fetchOngoingGames(session?.user.id);
return data.sort((a, b) {
final aIsMyTurn = a.$2.isMyTurn;
final bIsMyTurn = b.$2.isMyTurn;
if (aIsMyTurn && !bIsMyTurn) return -1;
if (!aIsMyTurn && bIsMyTurn) return 1;
return b.$1.compareTo(a.$1);
});
}
final db = await ref.watch(databaseProvider.future);
return CorrespondenceGameStorage(db, ref);
}, name: 'CorrespondenceGameStorageProvider');
/// Fetches all ongoing offline correspondence games, sorted by whose turn it is and last modified time.
final offlineOngoingCorrespondenceGamesProvider =
FutureProvider.autoDispose<IList<(DateTime, OfflineCorrespondenceGame)>>((Ref ref) async {
final session = ref.watch(authSessionProvider);
// cannot use ref.watch because it would create a circular dependency
// as we invalidate this provider in the storage save and delete methods
final storage = await ref.read(correspondenceGameStorageProvider.future);
final data = await storage.fetchOngoingGames(session?.user.id);
return data.sort((a, b) {
final aIsMyTurn = a.$2.isMyTurn;
final bIsMyTurn = b.$2.isMyTurn;
if (aIsMyTurn && !bIsMyTurn) return -1;
if (!aIsMyTurn && bIsMyTurn) return 1;
return b.$1.compareTo(a.$1);
});
}, name: 'OfflineOngoingCorrespondenceGamesProvider');
const kCorrespondenceStorageTable = 'correspondence_game';
@@ -24,16 +24,13 @@ import 'package:lichess_mobile/src/tab_scaffold.dart' show currentNavigatorKeyPr
import 'package:lichess_mobile/src/view/game/game_screen.dart';
import 'package:lichess_mobile/src/view/game/game_screen_providers.dart';
import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'correspondence_service.g.dart';
@Riverpod(keepAlive: true)
CorrespondenceService correspondenceService(Ref ref) {
/// A provider for [CorrespondenceService].
final correspondenceServiceProvider = Provider<CorrespondenceService>((Ref ref) {
final service = CorrespondenceService(Logger('CorrespondenceService'), ref: ref);
ref.onDispose(() => service.dispose());
return service;
}
}, name: 'CorrespondenceServiceProvider');
/// Service that manages correspondence games.
class CorrespondenceService {
@@ -1,19 +1,24 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/model/engine/evaluation_service.dart';
import 'package:lichess_mobile/src/model/settings/preferences_storage.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'evaluation_preferences.freezed.dart';
part 'evaluation_preferences.g.dart';
final engineEvaluationPreferencesProvider =
NotifierProvider<EngineEvaluationPreferences, EngineEvaluationPrefState>(
EngineEvaluationPreferences.new,
name: 'EngineEvaluationPreferencesProvider',
);
/// Preferences for engine evaluation.
///
/// Same preferences are used in various screens where engine evaluation is used:
/// - Analysis screen
/// - Study screen
/// - Broadcast game screen
@Riverpod(keepAlive: true)
class EngineEvaluationPreferences extends _$EngineEvaluationPreferences
class EngineEvaluationPreferences extends Notifier<EngineEvaluationPrefState>
with PreferencesStorage<EngineEvaluationPrefState> {
@override
@protected
+10 -7
View File
@@ -25,11 +25,9 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart';
import 'package:lichess_mobile/src/widgets/platform_alert_dialog.dart';
import 'package:logging/logging.dart';
import 'package:multistockfish/multistockfish.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:stream_transform/stream_transform.dart';
part 'evaluation_service.freezed.dart';
part 'evaluation_service.g.dart';
final _logger = Logger('EvaluationService');
@@ -47,8 +45,8 @@ typedef ShouldEmitEvalFilter = bool Function(Work work);
typedef NNUEFiles = ({File bigNet, File smallNet});
@Riverpod(keepAlive: true)
EvaluationService evaluationService(Ref ref) {
/// A provider for [EvaluationService].
final evaluationServiceProvider = Provider<EvaluationService>((Ref ref) {
final maxMemory = ref.read(preloadedDataProvider).requireValue.engineMaxMemoryInMb;
final service = EvaluationService(ref, maxMemory: maxMemory);
@@ -57,7 +55,7 @@ EvaluationService evaluationService(Ref ref) {
});
return service;
}
}, name: 'EvaluationServiceProvider');
/// A service to evaluate chess positions using an engine.
class EvaluationService {
@@ -387,8 +385,13 @@ typedef EngineEvaluationState = ({
});
/// A provider that holds the state of the engine and the current evaluation.
@riverpod
class EngineEvaluation extends _$EngineEvaluation {
final engineEvaluationProvider =
NotifierProvider.autoDispose<EngineEvaluation, EngineEvaluationState>(
EngineEvaluation.new,
name: 'EngineEvaluationProvider',
);
class EngineEvaluation extends Notifier<EngineEvaluationState> {
@override
EngineEvaluationState build() {
final listenable = ref.watch(evaluationServiceProvider).state;
@@ -1,17 +1,22 @@
import 'package:dartchess/dartchess.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/model/common/speed.dart';
import 'package:lichess_mobile/src/model/explorer/opening_explorer.dart';
import 'package:lichess_mobile/src/model/settings/preferences_storage.dart';
import 'package:lichess_mobile/src/model/user/user.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'opening_explorer_preferences.freezed.dart';
part 'opening_explorer_preferences.g.dart';
@Riverpod(keepAlive: true)
class OpeningExplorerPreferences extends _$OpeningExplorerPreferences
final openingExplorerPreferencesProvider =
NotifierProvider<OpeningExplorerPreferences, OpeningExplorerPrefs>(
OpeningExplorerPreferences.new,
name: 'OpeningExplorerPreferencesProvider',
);
class OpeningExplorerPreferences extends Notifier<OpeningExplorerPrefs>
with SessionPreferencesStorage<OpeningExplorerPrefs> {
@override
@protected
@@ -10,16 +10,21 @@ import 'package:lichess_mobile/src/model/explorer/opening_explorer.dart';
import 'package:lichess_mobile/src/model/explorer/opening_explorer_preferences.dart';
import 'package:lichess_mobile/src/network/http.dart';
import 'package:lichess_mobile/src/utils/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'opening_explorer_repository.g.dart';
final openingExplorerProvider = AsyncNotifierProvider.autoDispose
.family<OpeningExplorer, ({OpeningExplorerEntry entry, bool isIndexing})?, String>(
OpeningExplorer.new,
name: 'OpeningExplorerProvider',
);
class OpeningExplorer extends AsyncNotifier<({OpeningExplorerEntry entry, bool isIndexing})?> {
OpeningExplorer(this.fen);
final String fen;
@riverpod
class OpeningExplorer extends _$OpeningExplorer {
StreamSubscription<OpeningExplorerEntry>? _openingExplorerSubscription;
@override
Future<({OpeningExplorerEntry entry, bool isIndexing})?> build({required String fen}) async {
Future<({OpeningExplorerEntry entry, bool isIndexing})?> build() async {
ref.onDispose(() {
_openingExplorerSubscription?.cancel();
});
@@ -67,10 +72,10 @@ class OpeningExplorer extends _$OpeningExplorer {
}
}
@Riverpod(keepAlive: true)
OpeningExplorerRepository openingExplorerRepository(Ref ref) {
/// A provider for [OpeningExplorerRepository].
final openingExplorerRepositoryProvider = Provider<OpeningExplorerRepository>((Ref ref) {
return OpeningExplorerRepository(ref.read(defaultClientProvider));
}
}, name: 'OpeningExplorerRepositoryProvider');
class OpeningExplorerRepository {
const OpeningExplorerRepository(this.client);
@@ -1,26 +1,24 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart';
import 'package:lichess_mobile/src/constants.dart';
import 'package:lichess_mobile/src/model/explorer/tablebase.dart';
import 'package:lichess_mobile/src/network/http.dart';
import 'package:lichess_mobile/src/utils/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'tablebase_repository.g.dart';
/// A provider for fetching tablebase entries.
final tablebaseProvider = FutureProvider.autoDispose.family<TablebaseEntry?, String>((
ref,
fen,
) async {
await ref.debounce(const Duration(milliseconds: 300));
@riverpod
class Tablebase extends _$Tablebase {
@override
Future<TablebaseEntry?> build({required String fen}) async {
await ref.debounce(const Duration(milliseconds: 300));
final client = ref.read(defaultClientProvider);
final client = ref.read(defaultClientProvider);
final tablebaseEntry = await TablebaseRepository(client).getTablebaseEntry(fen);
return tablebaseEntry;
}
}
final tablebaseEntry = await TablebaseRepository(client).getTablebaseEntry(fen);
return tablebaseEntry;
}, name: 'TablebaseProvider');
class TablebaseRepository {
const TablebaseRepository(this.client);
+8 -4
View File
@@ -2,21 +2,25 @@ import 'dart:async';
import 'package:collection/collection.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/model/auth/auth_session.dart';
import 'package:lichess_mobile/src/model/common/id.dart';
import 'package:lichess_mobile/src/model/game/exported_game.dart';
import 'package:lichess_mobile/src/model/game/game_repository.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'game_bookmarks.freezed.dart';
part 'game_bookmarks.g.dart';
const _nbPerPage = 20;
/// A provider that paginates the game bookmarks for the current app user.
@riverpod
class GameBookmarksPaginator extends _$GameBookmarksPaginator {
final gameBookmarksPaginatorProvider =
AsyncNotifierProvider.autoDispose<GameBookmarksPaginator, GameBookmarksPaginatorState>(
GameBookmarksPaginator.new,
name: 'GameBookmarksPaginatorProvider',
);
class GameBookmarksPaginator extends AsyncNotifier<GameBookmarksPaginatorState> {
final _list = <LightExportedGameWithPov>[];
GameRepository get _gameRepository => ref.read(gameRepositoryProvider);
+18 -9
View File
@@ -6,6 +6,7 @@ import 'package:deep_pick/deep_pick.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/binding.dart';
import 'package:lichess_mobile/src/model/account/account_preferences.dart';
@@ -33,13 +34,21 @@ import 'package:lichess_mobile/src/model/settings/board_preferences.dart';
import 'package:lichess_mobile/src/network/socket.dart';
import 'package:lichess_mobile/src/utils/rate_limit.dart';
import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'game_controller.freezed.dart';
part 'game_controller.g.dart';
@riverpod
class GameController extends _$GameController {
/// A provider for [GameController].
final gameControllerProvider = AsyncNotifierProvider.autoDispose
.family<GameController, GameState, GameFullId>(
GameController.new,
name: 'GameControllerProvider',
);
class GameController extends AsyncNotifier<GameState> {
GameController(this.gameFullId);
final GameFullId gameFullId;
final _logger = Logger('GameController');
StreamSubscription<SocketEvent>? _socketSubscription;
@@ -67,7 +76,7 @@ class GameController extends _$GameController {
GameRepository get _gameRepository => ref.read(gameRepositoryProvider);
@override
Future<GameState> build(GameFullId gameFullId) {
Future<GameState> build() {
_socketClient = _openSocket();
_onFullReload = () {
@@ -374,7 +383,7 @@ class GameController extends _$GameController {
/// Play a sound when the clock is about to run out
Future<void> onClockEmergency(Side activeSide) async {
if (activeSide != state.valueOrNull?.game.youAre) return;
if (activeSide != state.value?.game.youAre) return;
final shouldPlay = await ref.read(clockSoundProvider.future);
if (shouldPlay) {
ref.read(soundServiceProvider).play(Sound.lowTime);
@@ -394,7 +403,7 @@ class GameController extends _$GameController {
}
void berserk() {
if (state.valueOrNull?.canBerserk == true && state.valueOrNull?.hasBerserked == false) {
if (state.value?.canBerserk == true && state.value?.hasBerserked == false) {
_socketClient.send('berserk', null);
}
}
@@ -702,8 +711,8 @@ class GameController extends _$GameController {
playedSide == curState.game.youAre?.opposite &&
curState.premove != null) {
scheduleMicrotask(() {
final postMovePremove = state.valueOrNull?.premove;
final postMovePosition = state.valueOrNull?.game.lastPosition;
final postMovePremove = state.value?.premove;
final postMovePosition = state.value?.game.lastPosition;
if (postMovePremove != null && postMovePosition?.isLegal(postMovePremove) == true) {
userMove(postMovePremove, isPremove: true);
}
+14 -5
View File
@@ -1,18 +1,27 @@
import 'package:dartchess/dartchess.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.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/model/common/perf.dart';
import 'package:lichess_mobile/src/model/user/user.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'game_filter.freezed.dart';
part 'game_filter.g.dart';
@riverpod
class GameFilter extends _$GameFilter {
/// A provider for [GameFilter].
final gameFilterProvider = NotifierProvider.autoDispose
.family<GameFilter, GameFilterState, GameFilterState?>(
GameFilter.new,
name: 'GameFilterProvider',
);
class GameFilter extends Notifier<GameFilterState> {
GameFilter(this.filter);
final GameFilterState? filter;
@override
GameFilterState build({GameFilterState? filter}) {
GameFilterState build() {
return filter ?? const GameFilterState();
}
+41 -40
View File
@@ -18,10 +18,8 @@ import 'package:lichess_mobile/src/model/user/user.dart';
import 'package:lichess_mobile/src/model/user/user_repository_providers.dart';
import 'package:lichess_mobile/src/network/connectivity.dart';
import 'package:lichess_mobile/src/utils/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'game_history.freezed.dart';
part 'game_history.g.dart';
const kNumberOfRecentGames = 10;
@@ -32,9 +30,10 @@ const _nbPerPage = 20;
/// If the user is logged in, the recent games are fetched from the server.
/// If the user is not logged in, or there is no connectivity, the recent games
/// stored locally are fetched instead.
@riverpod
Future<IList<LightExportedGameWithPov>> myRecentGames(Ref ref) async {
final online = await ref.watch(connectivityChangesProvider.selectAsync((c) => c.isOnline));
final myRecentGamesProvider = FutureProvider.autoDispose<IList<LightExportedGameWithPov>>((
Ref ref,
) async {
final online = (await ref.watch(connectivityChangesProvider.future)).isOnline;
final session = ref.watch(authSessionProvider);
if (session != null && online) {
return ref
@@ -52,38 +51,51 @@ Future<IList<LightExportedGameWithPov>> myRecentGames(Ref ref) async {
.toIList(),
);
}
}
}, name: 'MyRecentGamesProvider');
/// A provider that fetches the recent games from the server for a given user.
@riverpod
Future<IList<LightExportedGameWithPov>> userRecentGames(Ref ref, {required UserId userId}) {
return ref
.read(gameRepositoryProvider)
.getUserGames(userId, withBookmarked: true, max: kNumberOfRecentGames);
}
final userRecentGamesProvider = FutureProvider.autoDispose
.family<IList<LightExportedGameWithPov>, UserId>((Ref ref, UserId userId) {
return ref
.read(gameRepositoryProvider)
.getUserGames(userId, withBookmarked: true, max: kNumberOfRecentGames);
}, name: 'UserRecentGamesProvider');
/// A provider that fetches the total number of games played by given user, or the current app user if no user is provided.
///
/// If the user is logged in, the number of games is fetched from the server.
/// If the user is not logged in, or there is no connectivity, the number of games
/// stored locally are fetched instead.
@riverpod
Future<int> userNumberOfGames(Ref ref, LightUser? user) async {
final userNumberOfGamesProvider = FutureProvider.autoDispose.family<int, LightUser?>((
Ref ref,
LightUser? user,
) async {
final session = ref.watch(authSessionProvider);
final online = await ref.watch(connectivityChangesProvider.selectAsync((c) => c.isOnline));
final online = (await ref.watch(connectivityChangesProvider.future)).isOnline;
return user != null
? ref.watch(userProvider(id: user.id).selectAsync((u) => u.count?.all ?? 0))
? (await ref.watch(userProvider(user.id).future)).count?.all ?? 0
: session != null && online
? ref.watch(accountProvider.selectAsync((u) => u?.count?.all ?? 0))
? (await ref.watch(accountProvider.future))?.count?.all ?? 0
: (await ref.watch(gameStorageProvider.future)).count(userId: user?.id);
}
}, name: 'UserNumberOfGamesProvider');
typedef UserGameHistoryNotifierParams = ({UserId? userId, GameFilterState filter});
/// A provider that paginates the game history for a given user, or the current app user if no user is provided.
///
/// The game history is fetched from the server if the user is logged in and app is online.
/// Otherwise, the game history is fetched from the local storage.
@riverpod
class UserGameHistory extends _$UserGameHistory {
final userGameHistoryProvider = AsyncNotifierProvider.autoDispose
.family<UserGameHistoryNotifier, UserGameHistoryState, UserGameHistoryNotifierParams>(
UserGameHistoryNotifier.new,
name: 'UserGameHistoryProvider',
);
class UserGameHistoryNotifier extends AsyncNotifier<UserGameHistoryState> {
UserGameHistoryNotifier(this.params);
final UserGameHistoryNotifierParams params;
final _list = <LightExportedGameWithPov>[];
StreamSubscription<(GameId, bool)>? _bookmarkChangesSubscription;
@@ -91,18 +103,7 @@ class UserGameHistory extends _$UserGameHistory {
GameRepository get _gameRepository => ref.read(gameRepositoryProvider);
@override
Future<UserGameHistoryState> build(
UserId? userId, {
/// Whether the history is requested in an online context. Applicable only
/// when [userId] is null.
///
/// If this is true, the provider will attempt to fetch the games from the
/// server. If this is false, the provider will fetch the games from the
/// local storage.
required bool isOnline,
GameFilterState filter = const GameFilterState(),
}) async {
Future<UserGameHistoryState> build() async {
_bookmarkChangesSubscription?.cancel();
_bookmarkChangesSubscription = ref.read(accountServiceProvider).bookmarkChanges.listen((data) {
final (id, bookmarked) = data;
@@ -119,19 +120,19 @@ class UserGameHistory extends _$UserGameHistory {
final session = ref.watch(authSessionProvider);
final prefs = ref.watch(gameHistoryPreferencesProvider);
final online = await ref.watch(connectivityChangesProvider.selectAsync((c) => c.isOnline));
final online = (await ref.watch(connectivityChangesProvider.future)).isOnline;
final storage = await ref.watch(gameStorageProvider.future);
final id = userId ?? session?.user.id;
final id = params.userId ?? session?.user.id;
final recentGames = id != null && online
? _gameRepository.getUserGames(
id,
filter: filter,
filter: params.filter,
withBookmarked: true,
withMoves: prefs.displayMode == GameHistoryDisplayMode.detail,
)
: storage
.page(userId: id, filter: filter)
.page(userId: id, filter: params.filter)
.then(
(value) => value
// we can assume that `youAre` is not null either for logged
@@ -147,8 +148,8 @@ class UserGameHistory extends _$UserGameHistory {
isLoading: false,
hasMore: true,
hasError: false,
online: isOnline,
filter: filter,
online: online,
filter: params.filter,
session: session,
);
}
@@ -161,9 +162,9 @@ class UserGameHistory extends _$UserGameHistory {
final currentVal = state.requireValue;
state = AsyncData(currentVal.copyWith(isLoading: true));
try {
final value = await (userId != null
final value = await (params.userId != null
? _gameRepository.getUserGames(
userId!,
params.userId!,
max: _nbPerPage,
until: _list.last.game.createdAt,
filter: currentVal.filter,
+8 -3
View File
@@ -1,13 +1,18 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/model/settings/preferences_storage.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'game_preferences.freezed.dart';
part 'game_preferences.g.dart';
/// Provider for [GamePreferences].
final gamePreferencesProvider = NotifierProvider<GamePreferences, GamePrefs>(
GamePreferences.new,
name: 'GamePreferencesProvider',
);
/// Local game preferences, defined client-side only.
@Riverpod(keepAlive: true)
class GamePreferences extends _$GamePreferences with PreferencesStorage<GamePrefs> {
class GamePreferences extends Notifier<GamePrefs> with PreferencesStorage<GamePrefs> {
@override
@protected
final prefCategory = PrefCategory.game;
@@ -5,13 +5,12 @@ import 'package:lichess_mobile/src/model/common/id.dart';
import 'package:lichess_mobile/src/model/game/exported_game.dart';
import 'package:lichess_mobile/src/model/game/game_repository.dart';
import 'package:lichess_mobile/src/model/game/game_storage.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'game_repository_providers.g.dart';
/// Fetches a game from the server or from the local storage if not available online.
@riverpod
Future<ExportedGame> archivedGame(Ref ref, {required GameId id}) async {
final archivedGameProvider = FutureProvider.autoDispose.family<ExportedGame, GameId>((
Ref ref,
GameId id,
) async {
ExportedGame game;
try {
final isLoggedIn = ref.watch(isLoggedInProvider);
@@ -26,9 +25,11 @@ Future<ExportedGame> archivedGame(Ref ref, {required GameId id}) async {
}
}
return game;
}
}, name: 'ArchivedGameProvider');
@riverpod
Future<IList<LightExportedGame>> gamesById(Ref ref, {required ISet<GameId> ids}) {
return ref.read(gameRepositoryProvider).getGamesByIds(ids);
}
final gamesByIdProvider = FutureProvider.autoDispose.family<IList<LightExportedGame>, ISet<GameId>>(
(Ref ref, ISet<GameId> ids) {
return ref.read(gameRepositoryProvider).getGamesByIds(ids);
},
name: 'GamesByIdProvider',
);
+3 -6
View File
@@ -9,15 +9,12 @@ import 'package:lichess_mobile/src/model/game/exported_game.dart';
import 'package:lichess_mobile/src/model/game/game_repository.dart';
import 'package:lichess_mobile/src/model/settings/board_preferences.dart';
import 'package:lichess_mobile/src/network/http.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:share_plus/share_plus.dart';
part 'game_share_service.g.dart';
@Riverpod(keepAlive: true)
GameShareService gameShareService(Ref ref) {
/// A provider for [GameShareService].
final gameShareServiceProvider = Provider<GameShareService>((Ref ref) {
return GameShareService(ref);
}
}, name: 'GameShareServiceProvider');
class GameShareService {
GameShareService(this._ref);
+3 -6
View File
@@ -6,16 +6,13 @@ import 'package:lichess_mobile/src/db/database.dart';
import 'package:lichess_mobile/src/model/common/id.dart';
import 'package:lichess_mobile/src/model/game/exported_game.dart';
import 'package:lichess_mobile/src/model/game/game_filter.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:sqflite/sqflite.dart';
part 'game_storage.g.dart';
@Riverpod(keepAlive: true)
Future<GameStorage> gameStorage(Ref ref) async {
/// A provider for [GameStorage].
final gameStorageProvider = FutureProvider<GameStorage>((Ref ref) async {
final db = await ref.watch(databaseProvider.future);
return GameStorage(db);
}
}, name: 'GameStorageProvider');
const kGameStorageTable = 'game';
+10 -7
View File
@@ -1,21 +1,25 @@
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/model/http_log/http_log_storage.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'http_log_paginator.freezed.dart';
part 'http_log_paginator.g.dart';
/// The number of HTTP logs to fetch per page.
const _pageSize = 20;
/// A provider for [HttpLogPaginator].
final httpLogPaginatorProvider = AsyncNotifierProvider.autoDispose<HttpLogPaginator, HttpLogState>(
HttpLogPaginator.new,
name: 'HttpLogPaginatorProvider',
);
/// A Riverpod controller for managing HTTP logs.
///
/// The `HttpLogController` class is responsible for fetching and managing
/// paginated HTTP log entries from the storage. It uses a throttler to limit
/// the rate of fetching new pages.
@riverpod
class HttpLogPaginator extends _$HttpLogPaginator {
class HttpLogPaginator extends AsyncNotifier<HttpLogState> {
@override
Future<HttpLogState> build() async {
final storage = await ref.read(httpLogStorageProvider.future);
@@ -68,9 +72,8 @@ sealed class HttpLogState with _$HttpLogState {
const factory HttpLogState({required IList<AsyncValue<HttpLog>> data}) = _HttpLogState;
bool get initialized => data.isNotEmpty;
List<HttpLogEntry> get logs =>
data.expand((e) => e.valueOrNull?.items ?? <HttpLogEntry>[]).toList();
int? get nextPage => data.lastOrNull?.valueOrNull?.next;
List<HttpLogEntry> get logs => data.expand((e) => e.value?.items ?? <HttpLogEntry>[]).toList();
int? get nextPage => data.lastOrNull?.value?.next;
bool get hasMore => initialized && nextPage != null;
bool get isLoading => data.lastOrNull?.isLoading == true;
bool get isDeleteButtonVisible => logs.isNotEmpty;
+2 -4
View File
@@ -3,18 +3,16 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/db/database.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:sqflite/sqflite.dart';
part 'http_log_storage.g.dart';
part 'http_log_storage.freezed.dart';
/// Provides an instance of [HttpLogStorage] using Riverpod.
@Riverpod(keepAlive: true)
Future<HttpLogStorage> httpLogStorage(Ref ref) async {
final httpLogStorageProvider = FutureProvider<HttpLogStorage>((Ref ref) async {
final db = await ref.watch(databaseProvider.future);
return HttpLogStorage(db);
}
}, name: 'HttpLogStorageProvider');
const kHttpLogStorageTable = 'http_log';
+2 -5
View File
@@ -14,9 +14,7 @@ import 'package:lichess_mobile/src/model/lobby/lobby_repository.dart';
import 'package:lichess_mobile/src/network/http.dart';
import 'package:lichess_mobile/src/network/socket.dart';
import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'create_game_service.g.dart';
part 'create_game_service.freezed.dart';
/// The user response of a challenge request.
@@ -47,14 +45,13 @@ sealed class ChallengeResponseDeclined
}
/// A provider for the [CreateGameService].
@riverpod
CreateGameService createGameService(Ref ref) {
final createGameServiceProvider = Provider.autoDispose<CreateGameService>((Ref ref) {
final service = CreateGameService(Logger('CreateGameService'), ref: ref);
ref.onDispose(() {
service.dispose();
});
return service;
}
}, name: 'CreateGameServiceProvider');
/// A service to create a new game from the lobby or from a challenge.
class CreateGameService {
+8 -3
View File
@@ -1,6 +1,7 @@
import 'dart:math' as math;
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/binding.dart';
import 'package:lichess_mobile/src/model/auth/auth_session.dart';
@@ -13,7 +14,6 @@ import 'package:lichess_mobile/src/model/game/playable_game.dart';
import 'package:lichess_mobile/src/model/lobby/game_setup_preferences.dart';
import 'package:lichess_mobile/src/model/settings/preferences_storage.dart';
import 'package:lichess_mobile/src/model/user/user.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'game_seek.freezed.dart';
part 'game_seek.g.dart';
@@ -139,8 +139,13 @@ sealed class RecentGameSeekPrefs with _$RecentGameSeekPrefs implements Serializa
}
}
@Riverpod(keepAlive: true)
class RecentGameSeek extends _$RecentGameSeek with PreferencesStorage<RecentGameSeekPrefs> {
final recentGameSeekProvider = NotifierProvider<RecentGameSeek, RecentGameSeekPrefs>(
RecentGameSeek.new,
name: 'RecentGameSeekProvider',
);
class RecentGameSeek extends Notifier<RecentGameSeekPrefs>
with PreferencesStorage<RecentGameSeekPrefs> {
@override
@protected
final prefCategory = PrefCategory.gameSeeks;
@@ -1,4 +1,5 @@
import 'dart:math' as math;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/model/common/chess.dart';
import 'package:lichess_mobile/src/model/common/perf.dart';
@@ -6,13 +7,16 @@ import 'package:lichess_mobile/src/model/common/speed.dart';
import 'package:lichess_mobile/src/model/common/time_increment.dart';
import 'package:lichess_mobile/src/model/settings/preferences_storage.dart';
import 'package:lichess_mobile/src/model/user/user.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'game_setup_preferences.freezed.dart';
part 'game_setup_preferences.g.dart';
@Riverpod(keepAlive: true)
class GameSetupPreferences extends _$GameSetupPreferences
final gameSetupPreferencesProvider = NotifierProvider<GameSetupPreferences, GameSetupPrefs>(
GameSetupPreferences.new,
name: 'GameSetupPreferencesProvider',
);
class GameSetupPreferences extends Notifier<GameSetupPrefs>
with SessionPreferencesStorage<GameSetupPrefs> {
@override
@protected
+8 -5
View File
@@ -1,15 +1,18 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lichess_mobile/src/model/common/socket.dart';
import 'package:lichess_mobile/src/network/socket.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'lobby_numbers.g.dart';
/// The [LobbyNumbers] provider is used to display the number of players and
/// games on lichess in real time.
@riverpod
class LobbyNumbers extends _$LobbyNumbers {
final lobbyNumbersProvider =
NotifierProvider.autoDispose<LobbyNumbers, ({int nbPlayers, int nbGames})?>(
LobbyNumbers.new,
name: 'LobbyNumbersProvider',
);
class LobbyNumbers extends Notifier<({int nbPlayers, int nbGames})?> {
StreamSubscription<SocketEvent>? _socketSubscription;
@override
+6 -7
View File
@@ -7,14 +7,13 @@ import 'package:lichess_mobile/src/model/common/perf.dart';
import 'package:lichess_mobile/src/model/lobby/correspondence_challenge.dart';
import 'package:lichess_mobile/src/model/lobby/game_seek.dart';
import 'package:lichess_mobile/src/network/http.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'lobby_repository.g.dart';
@riverpod
Future<IList<CorrespondenceChallenge>> correspondenceChallenges(Ref ref) {
return ref.withClient((client) => LobbyRepository(client).getCorrespondenceChallenges());
}
final correspondenceChallengesProvider = FutureProvider.autoDispose<IList<CorrespondenceChallenge>>(
(Ref ref) {
return ref.withClient((client) => LobbyRepository(client).getCorrespondenceChallenges());
},
name: 'CorrespondenceChallengesProvider',
);
class LobbyRepository {
LobbyRepository(this.client);
@@ -1,4 +1,5 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/model/auth/auth_session.dart';
import 'package:lichess_mobile/src/model/common/id.dart';
@@ -8,15 +9,21 @@ import 'package:lichess_mobile/src/model/message/message_repository.dart';
import 'package:lichess_mobile/src/model/user/user.dart';
import 'package:lichess_mobile/src/network/socket.dart';
import 'package:lichess_mobile/src/utils/rate_limit.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'conversation_controller.g.dart';
part 'conversation_controller.freezed.dart';
const kMessagesPerPage = 100;
@riverpod
class ConversationController extends _$ConversationController {
final conversationControllerProvider = AsyncNotifierProvider.autoDispose
.family<ConversationController, ConversationState, UserId>(
ConversationController.new,
name: 'ConversationControllerProvider',
);
class ConversationController extends AsyncNotifier<ConversationState> {
ConversationController(this.userId);
final UserId userId;
late SocketClient _client;
StreamSubscription<SocketEvent>? _socketSubscription;
Timer? _setReadTimer;
@@ -27,7 +34,7 @@ class ConversationController extends _$ConversationController {
LightUser? get _me => ref.read(authSessionProvider)?.user;
@override
Future<ConversationState> build(UserId userId) async {
Future<ConversationState> build() async {
_connectSocket();
ref.onDispose(() {
@@ -4,7 +4,6 @@ import 'package:lichess_mobile/src/model/common/id.dart';
import 'package:lichess_mobile/src/model/message/message.dart';
import 'package:lichess_mobile/src/network/aggregator.dart';
import 'package:lichess_mobile/src/network/http.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
/// A provider that gets the conversation data for the current user.
final contactsProvider = FutureProvider.autoDispose<Contacts>((ref) {
+3 -6
View File
@@ -9,16 +9,13 @@ import 'package:lichess_mobile/src/model/notifications/notifications.dart';
import 'package:lichess_mobile/src/model/user/user_repository.dart';
import 'package:lichess_mobile/src/tab_scaffold.dart';
import 'package:lichess_mobile/src/view/message/conversation_screen.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'message_service.g.dart';
@Riverpod(keepAlive: true)
MessageService messageService(Ref ref) {
/// A provider for [MessageService].
final messageServiceProvider = Provider<MessageService>((Ref ref) {
final service = MessageService(ref);
ref.onDispose(service.dispose);
return service;
}
}, name: 'MessageServiceProvider');
class MessageService {
MessageService(this.ref);
@@ -15,25 +15,22 @@ import 'package:lichess_mobile/src/network/connectivity.dart';
import 'package:lichess_mobile/src/network/http.dart';
import 'package:lichess_mobile/src/utils/badge_service.dart';
import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'notification_service.g.dart';
final _logger = Logger('NotificationService');
/// A provider instance of the [FlutterLocalNotificationsPlugin].
@Riverpod(keepAlive: true)
FlutterLocalNotificationsPlugin notificationDisplay(Ref _) => FlutterLocalNotificationsPlugin();
final notificationDisplayProvider = Provider<FlutterLocalNotificationsPlugin>(
(Ref _) => FlutterLocalNotificationsPlugin(),
);
/// A provider instance of the [NotificationService].
@Riverpod(keepAlive: true)
NotificationService notificationService(Ref ref) {
final notificationServiceProvider = Provider<NotificationService>((Ref ref) {
final service = NotificationService(ref);
ref.onDispose(() => service._dispose());
return service;
}
});
/// Received FCM message and whether it was from the background.
typedef ReceivedFcmMessage = ({FcmMessage message, bool fromBackground});
@@ -2,15 +2,19 @@ import 'dart:async';
import 'dart:math';
import 'package:dartchess/dartchess.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/model/common/time_increment.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'over_the_board_clock.freezed.dart';
part 'over_the_board_clock.g.dart';
@riverpod
class OverTheBoardClock extends _$OverTheBoardClock {
final overTheBoardClockProvider =
NotifierProvider.autoDispose<OverTheBoardClock, OverTheBoardClockState>(
OverTheBoardClock.new,
name: 'OverTheBoardClockProvider',
);
class OverTheBoardClock extends Notifier<OverTheBoardClockState> {
final Stopwatch _stopwatch = Stopwatch();
Timer? _updateTimer;
@@ -1,5 +1,6 @@
import 'package:dartchess/dartchess.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/model/common/chess.dart';
import 'package:lichess_mobile/src/model/common/chess960.dart';
@@ -11,13 +12,16 @@ import 'package:lichess_mobile/src/model/game/game.dart';
import 'package:lichess_mobile/src/model/game/game_status.dart';
import 'package:lichess_mobile/src/model/game/material_diff.dart';
import 'package:lichess_mobile/src/model/game/over_the_board_game.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'over_the_board_game_controller.freezed.dart';
part 'over_the_board_game_controller.g.dart';
@riverpod
class OverTheBoardGameController extends _$OverTheBoardGameController {
final overTheBoardGameControllerProvider =
NotifierProvider.autoDispose<OverTheBoardGameController, OverTheBoardGameState>(
OverTheBoardGameController.new,
name: 'OverTheBoardGameControllerProvider',
);
class OverTheBoardGameController extends Notifier<OverTheBoardGameState> {
@override
OverTheBoardGameState build() => OverTheBoardGameState.fromVariant(
Variant.standard,
+8 -4
View File
@@ -1,22 +1,26 @@
import 'dart:async';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/model/puzzle/puzzle.dart';
import 'package:lichess_mobile/src/model/puzzle/puzzle_providers.dart';
import 'package:lichess_mobile/src/model/puzzle/puzzle_repository.dart';
import 'package:lichess_mobile/src/network/http.dart';
import 'package:lichess_mobile/src/utils/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'puzzle_activity.freezed.dart';
part 'puzzle_activity.g.dart';
const _nbPerPage = 50;
const _maxPuzzles = 500;
@riverpod
class PuzzleActivity extends _$PuzzleActivity {
final puzzleActivityProvider =
AsyncNotifierProvider.autoDispose<PuzzleActivity, PuzzleActivityState>(
PuzzleActivity.new,
name: 'PuzzleActivityProvider',
);
class PuzzleActivity extends AsyncNotifier<PuzzleActivityState> {
final _list = <PuzzleHistoryEntry>[];
@override
@@ -9,17 +9,16 @@ import 'package:lichess_mobile/src/model/common/id.dart';
import 'package:lichess_mobile/src/model/puzzle/puzzle.dart';
import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart';
import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:sqflite/sqflite.dart';
part 'puzzle_batch_storage.freezed.dart';
part 'puzzle_batch_storage.g.dart';
@Riverpod(keepAlive: true)
Future<PuzzleBatchStorage> puzzleBatchStorage(Ref ref) async {
/// A provider for [PuzzleBatchStorage].
final puzzleBatchStorageProvider = FutureProvider<PuzzleBatchStorage>((Ref ref) async {
final database = await ref.watch(databaseProvider.future);
return PuzzleBatchStorage(database, ref);
}
}, name: 'PuzzleBatchStorageProvider');
const _anonUserKey = '**anon**';
const _tableName = 'puzzle_batchs';
+28 -10
View File
@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:collection/collection.dart' show IterableExtension;
import 'package:dartchess/dartchess.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/model/common/chess.dart';
import 'package:lichess_mobile/src/model/common/node.dart';
@@ -20,13 +21,20 @@ import 'package:lichess_mobile/src/model/puzzle/puzzle_service.dart';
import 'package:lichess_mobile/src/model/puzzle/puzzle_session.dart';
import 'package:lichess_mobile/src/network/http.dart';
import 'package:lichess_mobile/src/network/socket.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'puzzle_controller.freezed.dart';
part 'puzzle_controller.g.dart';
@riverpod
class PuzzleController extends _$PuzzleController with EngineEvaluationMixin {
final puzzleControllerProvider = NotifierProvider.autoDispose
.family<PuzzleController, PuzzleState, PuzzleContext>(
PuzzleController.new,
name: 'PuzzleControllerProvider',
);
class PuzzleController extends Notifier<PuzzleState> with EngineEvaluationMixin {
PuzzleController(this.initialContext);
final PuzzleContext initialContext;
static final Uri socketUri = Uri(path: '/analysis/socket/v5');
late Branch _gameTree;
@@ -62,7 +70,7 @@ class PuzzleController extends _$PuzzleController with EngineEvaluationMixin {
ref.read(puzzleServiceFactoryProvider)(queueLength: kPuzzleLocalQueueLength);
@override
PuzzleState build(PuzzleContext initialContext, {bool isPuzzleStreak = false}) {
PuzzleState build() {
initEngineEvaluation();
ref.onDispose(() {
@@ -165,7 +173,7 @@ class PuzzleController extends _$PuzzleController with EngineEvaluationMixin {
} else {
state = state.copyWith(feedback: PuzzleFeedback.bad);
_onFailOrWin(PuzzleResult.lose);
if (!isPuzzleStreak) {
if (initialContext.isPuzzleStreak != true) {
await Future<void>.delayed(const Duration(milliseconds: 500));
_setPath(state.currentPath.penultimate);
}
@@ -224,7 +232,7 @@ class PuzzleController extends _$PuzzleController with EngineEvaluationMixin {
}
void skipMove() {
if (isPuzzleStreak && state._nextSolutionMove != null) {
if (initialContext.isPuzzleStreak == true && state._nextSolutionMove != null) {
onUserMove(state._nextSolutionMove!);
}
}
@@ -271,7 +279,7 @@ class PuzzleController extends _$PuzzleController with EngineEvaluationMixin {
final soundService = ref.read(soundServiceProvider);
if (isPuzzleStreak) {
if (initialContext.isPuzzleStreak == true) {
// one fail and streak is over
if (result == PuzzleResult.lose) {
soundService.play(Sound.error);
@@ -307,13 +315,23 @@ class PuzzleController extends _$PuzzleController with EngineEvaluationMixin {
state = state.copyWith(nextContext: next);
ref
.read(puzzleSessionProvider(initialContext.userId, initialContext.angle).notifier)
.read(
puzzleSessionProvider((
userId: initialContext.userId,
angle: initialContext.angle,
)).notifier,
)
.addAttempt(state.puzzle.puzzle.id, win: result == PuzzleResult.win);
final rounds = next?.rounds;
if (rounds != null) {
ref
.read(puzzleSessionProvider(initialContext.userId, initialContext.angle).notifier)
.read(
puzzleSessionProvider((
userId: initialContext.userId,
angle: initialContext.angle,
)).notifier,
)
.setRatingDiffs(rounds);
}
+10 -9
View File
@@ -1,9 +1,6 @@
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lichess_mobile/src/model/puzzle/puzzle_providers.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'puzzle_opening.g.dart';
typedef PuzzleOpeningFamily = ({
String key,
@@ -15,8 +12,9 @@ typedef PuzzleOpeningFamily = ({
typedef PuzzleOpeningData = ({String key, String name, int count});
/// Returns a flattened list of openings with their respective counts.
@riverpod
Future<IList<PuzzleOpeningData>> flatOpeningsList(Ref ref) async {
final flatOpeningsListProvider = FutureProvider.autoDispose<IList<PuzzleOpeningData>>((
Ref ref,
) async {
final families = await ref.watch(puzzleOpeningsProvider.future);
return families
.map(
@@ -27,10 +25,13 @@ Future<IList<PuzzleOpeningData>> flatOpeningsList(Ref ref) async {
)
.expand((e) => e)
.toIList();
}
}, name: 'FlatOpeningsListProvider');
@riverpod
Future<String> puzzleOpeningName(Ref ref, String key) async {
/// Provides the name of a puzzle opening given its key.
final puzzleOpeningNameProvider = FutureProvider.autoDispose.family<String, String>((
Ref ref,
String key,
) async {
final openings = await ref.watch(flatOpeningsListProvider.future);
return openings.firstWhere((element) => element.key == key).name;
}
}, name: 'PuzzleOpeningNameProvider');
+7 -3
View File
@@ -1,15 +1,19 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/model/common/id.dart';
import 'package:lichess_mobile/src/model/puzzle/puzzle_difficulty.dart';
import 'package:lichess_mobile/src/model/settings/preferences_storage.dart';
import 'package:lichess_mobile/src/model/user/user.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'puzzle_preferences.freezed.dart';
part 'puzzle_preferences.g.dart';
@Riverpod(keepAlive: true)
class PuzzlePreferences extends _$PuzzlePreferences with SessionPreferencesStorage<PuzzlePrefs> {
final puzzlePreferencesProvider = NotifierProvider<PuzzlePreferences, PuzzlePrefs>(
PuzzlePreferences.new,
name: 'PuzzlePreferencesProvider',
);
class PuzzlePreferences extends Notifier<PuzzlePrefs> with SessionPreferencesStorage<PuzzlePrefs> {
@override
@protected
final prefCategory = PrefCategory.puzzle;
+53 -41
View File
@@ -1,5 +1,3 @@
import 'dart:async';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lichess_mobile/src/model/auth/auth_session.dart';
@@ -15,12 +13,12 @@ import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart';
import 'package:lichess_mobile/src/model/puzzle/storm.dart';
import 'package:lichess_mobile/src/network/http.dart';
import 'package:lichess_mobile/src/utils/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'puzzle_providers.g.dart';
@riverpod
Future<PuzzleContext?> nextPuzzle(Ref ref, PuzzleAngle angle) async {
/// Fetches the next puzzle for the given [PuzzleAngle].
final nextPuzzleProvider = FutureProvider.autoDispose.family<PuzzleContext?, PuzzleAngle>((
Ref ref,
PuzzleAngle angle,
) async {
final session = ref.watch(authSessionProvider);
final puzzleService = await ref.read(puzzleServiceFactoryProvider)(
queueLength: kPuzzleLocalQueueLength,
@@ -30,88 +28,102 @@ Future<PuzzleContext?> nextPuzzle(Ref ref, PuzzleAngle angle) async {
ref.cacheFor(const Duration(minutes: 1));
return puzzleService.nextPuzzle(userId: session?.user.id, angle: angle);
}
}, name: 'NextPuzzleProvider');
@riverpod
Future<PuzzleStormResponse> storm(Ref ref) {
/// Fetches a storm of puzzles.
final stormProvider = FutureProvider.autoDispose<PuzzleStormResponse>((Ref ref) {
return ref.withClient((client) => PuzzleRepository(client).storm());
}
}, name: 'StormProvider');
/// Fetches a puzzle from the local storage if available, otherwise fetches it from the server.
@riverpod
Future<Puzzle> puzzle(Ref ref, PuzzleId id) async {
final puzzleProvider = FutureProvider.autoDispose.family<Puzzle, PuzzleId>((
Ref ref,
PuzzleId id,
) async {
final puzzleStorage = await ref.watch(puzzleStorageProvider.future);
final puzzle = await puzzleStorage.fetch(puzzleId: id);
if (puzzle != null) return puzzle;
return ref.withClient((client) => PuzzleRepository(client).fetch(id));
}
}, name: 'PuzzleProvider');
@riverpod
Future<Puzzle> dailyPuzzle(Ref ref) {
/// Fetches the daily puzzle.
final dailyPuzzleProvider = FutureProvider.autoDispose<Puzzle>((Ref ref) {
return ref.withClientCacheFor(
(client) => PuzzleRepository(client).daily(),
const Duration(hours: 6),
);
}
}, name: 'DailyPuzzleProvider');
@riverpod
Future<IList<(PuzzleAngle, int)>> savedBatches(Ref ref) async {
/// Fetches all saved puzzle batches for the current user.
final savedBatchesProvider = FutureProvider.autoDispose<IList<(PuzzleAngle, int)>>((Ref ref) async {
final session = ref.watch(authSessionProvider);
final storage = await ref.watch(puzzleBatchStorageProvider.future);
return storage.fetchAll(userId: session?.user.id);
}
}, name: 'SavedBatchesProvider');
@riverpod
Future<IMap<PuzzleThemeKey, int>> savedThemeBatches(Ref ref) async {
/// Fetches saved puzzle theme batches for the current user.
final savedThemeBatchesProvider = FutureProvider.autoDispose<IMap<PuzzleThemeKey, int>>((
Ref ref,
) async {
final session = ref.watch(authSessionProvider);
final storage = await ref.watch(puzzleBatchStorageProvider.future);
return storage.fetchSavedThemes(userId: session?.user.id);
}
}, name: 'SavedThemeBatchesProvider');
@riverpod
Future<IMap<String, int>> savedOpeningBatches(Ref ref) async {
/// Fetches saved puzzle opening batches for the current user.
final savedOpeningBatchesProvider = FutureProvider.autoDispose<IMap<String, int>>((Ref ref) async {
final session = ref.watch(authSessionProvider);
final storage = await ref.watch(puzzleBatchStorageProvider.future);
return storage.fetchSavedOpenings(userId: session?.user.id);
}
}, name: 'SavedOpeningBatchesProvider');
@riverpod
Future<PuzzleDashboard?> puzzleDashboard(Ref ref, int days) async {
/// Fetches the puzzle dashboard for the current user for the given number of [days].
final puzzleDashboardProvider = FutureProvider.autoDispose.family<PuzzleDashboard?, int>((
Ref ref,
int days,
) {
final session = ref.watch(authSessionProvider);
if (session == null) return null;
return ref.withClientCacheFor(
(client) => PuzzleRepository(client).puzzleDashboard(days),
const Duration(hours: 3),
);
}
}, name: 'PuzzleDashboardProvider');
@riverpod
Future<IList<PuzzleHistoryEntry>?> puzzleRecentActivity(Ref ref) async {
/// Fetches recent puzzle activity for the current user.
final puzzleRecentActivityProvider = FutureProvider.autoDispose<IList<PuzzleHistoryEntry>?>((
Ref ref,
) {
final session = ref.watch(authSessionProvider);
if (session == null) return null;
return ref.withClientCacheFor(
(client) => PuzzleRepository(client).puzzleActivity(20),
const Duration(hours: 3),
);
}
}, name: 'PuzzleRecentActivityProvider');
@riverpod
Future<StormDashboard?> stormDashboard(Ref ref, UserId id) {
/// Fetches the storm dashboard for a given user [UserId].
final stormDashboardProvider = FutureProvider.autoDispose.family<StormDashboard?, UserId>((
Ref ref,
UserId id,
) {
return ref.withClient((client) => PuzzleRepository(client).stormDashboard(id));
}
}, name: 'StormDashboardProvider');
@riverpod
Future<IMap<PuzzleThemeKey, PuzzleThemeData>> puzzleThemes(Ref ref) {
/// Fetches available puzzle themes.
final puzzleThemesProvider = FutureProvider.autoDispose<IMap<PuzzleThemeKey, PuzzleThemeData>>((
Ref ref,
) {
return ref.withClientCacheFor(
(client) => PuzzleRepository(client).puzzleThemes(),
const Duration(days: 1),
);
}
}, name: 'PuzzleThemesProvider');
@riverpod
Future<IList<PuzzleOpeningFamily>> puzzleOpenings(Ref ref) {
/// Fetches available puzzle openings.
final puzzleOpeningsProvider = FutureProvider.autoDispose<IList<PuzzleOpeningFamily>>((Ref ref) {
return ref.withClientCacheFor(
(client) => PuzzleRepository(client).puzzleOpenings(),
const Duration(days: 1),
);
}
}, name: 'PuzzleOpeningsProvider');
+7 -8
View File
@@ -15,23 +15,21 @@ import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart';
import 'package:lichess_mobile/src/network/http.dart';
import 'package:logging/logging.dart';
import 'package:result_extensions/result_extensions.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'puzzle_service.freezed.dart';
part 'puzzle_service.g.dart';
/// Size of puzzle local cache
const kPuzzleLocalQueueLength = 50;
@Riverpod(keepAlive: true)
Future<PuzzleService> puzzleService(Ref ref) {
/// A provider for [PuzzleService].
final puzzleServiceProvider = FutureProvider<PuzzleService>((Ref ref) {
return ref.read(puzzleServiceFactoryProvider)(queueLength: kPuzzleLocalQueueLength);
}
}, name: 'PuzzleServiceProvider');
@Riverpod(keepAlive: true)
PuzzleServiceFactory puzzleServiceFactory(Ref ref) {
/// A provider for [PuzzleServiceFactory].
final puzzleServiceFactoryProvider = Provider<PuzzleServiceFactory>((Ref ref) {
return PuzzleServiceFactory(ref);
}
}, name: 'PuzzleServiceFactoryProvider');
class PuzzleServiceFactory {
PuzzleServiceFactory(this._ref);
@@ -63,6 +61,7 @@ sealed class PuzzleContext with _$PuzzleContext {
/// If true, the result won't be recorded on the server for this puzzle.
bool? casual,
bool? isPuzzleStreak,
}) = _PuzzleContext;
}
+19 -8
View File
@@ -2,32 +2,43 @@ import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/binding.dart';
import 'package:lichess_mobile/src/model/common/id.dart';
import 'package:lichess_mobile/src/model/puzzle/puzzle.dart';
import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart';
import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
part 'puzzle_session.freezed.dart';
part 'puzzle_session.g.dart';
@riverpod
class PuzzleSession extends _$PuzzleSession {
final puzzleSessionProvider =
NotifierProvider.family<PuzzleSession, PuzzleSessionData, PuzzleSessionParams>(
PuzzleSession.new,
name: 'PuzzleSessionProvider',
);
typedef PuzzleSessionParams = ({UserId? userId, PuzzleAngle angle});
class PuzzleSession extends Notifier<PuzzleSessionData> {
PuzzleSession(this.params);
final PuzzleSessionParams params;
static const maxAge = Duration(hours: 1);
static const maxSize = 150;
@override
PuzzleSessionData build(UserId? userId, PuzzleAngle angle) {
PuzzleSessionData build() {
final data = _stored;
if (data != null &&
data.angle == angle &&
data.angle == params.angle &&
data.lastUpdatedAt.isAfter(DateTime.now().subtract(maxAge))) {
return data;
}
return PuzzleSessionData.initial(angle: angle);
return PuzzleSessionData.initial(angle: params.angle);
}
Future<void> addAttempt(PuzzleId id, {required bool win}) async {
@@ -66,14 +77,14 @@ class PuzzleSession extends _$PuzzleSession {
PuzzleSessionData? get _stored {
final stored = _store.getString(_storageKey);
if (stored == null) {
return PuzzleSessionData.initial(angle: angle);
return PuzzleSessionData.initial(angle: params.angle);
}
return PuzzleSessionData.fromJson(jsonDecode(stored) as Map<String, dynamic>);
}
SharedPreferencesWithCache get _store => LichessBinding.instance.sharedPreferences;
String get _storageKey => 'puzzle_session.${userId ?? '**anon**'}';
String get _storageKey => 'puzzle_session.${params.userId ?? '**anon**'}';
}
@Freezed(fromJson: true, toJson: true)
+3 -6
View File
@@ -4,16 +4,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lichess_mobile/src/db/database.dart';
import 'package:lichess_mobile/src/model/common/id.dart';
import 'package:lichess_mobile/src/model/puzzle/puzzle.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:sqflite/sqflite.dart';
part 'puzzle_storage.g.dart';
@Riverpod(keepAlive: true)
Future<PuzzleStorage> puzzleStorage(Ref ref) async {
/// A provider for [PuzzleStorage].
final puzzleStorageProvider = FutureProvider<PuzzleStorage>((Ref ref) async {
final db = await ref.watch(databaseProvider.future);
return PuzzleStorage(db);
}
}, name: 'PuzzleStorageProvider');
const _tableName = 'puzzle';
+8 -3
View File
@@ -1,4 +1,5 @@
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/model/auth/auth_session.dart';
import 'package:lichess_mobile/src/model/common/id.dart';
@@ -10,7 +11,6 @@ import 'package:lichess_mobile/src/model/puzzle/streak_storage.dart';
import 'package:lichess_mobile/src/network/http.dart';
import 'package:lichess_mobile/src/tab_scaffold.dart' show currentNavigatorKeyProvider;
import 'package:lichess_mobile/src/widgets/feedback.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'puzzle_streak.freezed.dart';
part 'puzzle_streak.g.dart';
@@ -37,8 +37,13 @@ sealed class PuzzleStreak with _$PuzzleStreak {
/// [PuzzleStreak] with its current [Puzzle].
typedef StreakState = ({PuzzleStreak streak, Puzzle puzzle, Puzzle? nextPuzzle});
@riverpod
class PuzzleStreakController extends _$PuzzleStreakController {
final puzzleStreakControllerProvider =
AsyncNotifierProvider.autoDispose<PuzzleStreakController, StreakState>(
PuzzleStreakController.new,
name: 'PuzzleStreakControllerProvider',
);
class PuzzleStreakController extends AsyncNotifier<StreakState> {
@override
Future<StreakState> build() async {
final session = ref.watch(authSessionProvider);
+2 -5
View File
@@ -5,10 +5,8 @@ import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/l10n/l10n.dart';
import 'package:lichess_mobile/src/localizations.dart';
import 'package:lichess_mobile/src/styles/puzzle_icons.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'puzzle_theme.freezed.dart';
part 'puzzle_theme.g.dart';
@freezed
sealed class PuzzleThemeData with _$PuzzleThemeData {
@@ -412,8 +410,7 @@ final IMap<String, PuzzleThemeKey> puzzleThemeNameMap = IMap(PuzzleThemeKey.valu
typedef PuzzleThemeCategory = (String, List<PuzzleThemeKey>);
@Riverpod(keepAlive: true)
IList<PuzzleThemeCategory> puzzleThemeCategories(Ref ref) {
final puzzleThemeCategoriesProvider = Provider<IList<PuzzleThemeCategory>>((Ref ref) {
final l10n = ref.watch(localizationsProvider);
return IList([
@@ -512,7 +509,7 @@ IList<PuzzleThemeCategory> puzzleThemeCategories(Ref ref) {
[PuzzleThemeKey.master, PuzzleThemeKey.masterVsMaster, PuzzleThemeKey.superGM],
),
]);
}
}, name: 'PuzzleThemeCategoriesProvider');
class PuzzleThemeL10n {
const PuzzleThemeL10n({required this.name, required this.description});
+22 -10
View File
@@ -6,6 +6,7 @@ import 'package:async/async.dart';
import 'package:dartchess/dartchess.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/model/auth/auth_session.dart';
import 'package:lichess_mobile/src/model/common/chess.dart';
@@ -16,22 +17,33 @@ import 'package:lichess_mobile/src/model/puzzle/puzzle_repository.dart';
import 'package:lichess_mobile/src/model/puzzle/storm.dart';
import 'package:lichess_mobile/src/network/http.dart';
import 'package:result_extensions/result_extensions.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'storm_controller.freezed.dart';
part 'storm_controller.g.dart';
const malus = Duration(seconds: 10);
const moveDelay = Duration(milliseconds: 200);
const startTime = Duration(minutes: 3);
@riverpod
class StormController extends _$StormController {
typedef StormControllerParams = (IList<LitePuzzle> puzzles, DateTime timestamp);
final stormControllerProvider = NotifierProvider.autoDispose
.family<StormController, StormState, StormControllerParams>(
StormController.new,
name: 'StormControllerProvider',
);
class StormController extends Notifier<StormState> {
StormController(this.params);
final StormControllerParams params;
Timer? _firstMoveTimer;
IList<LitePuzzle> get _puzzles => params.$1;
@override
StormState build(IList<LitePuzzle> puzzles, DateTime timestamp) {
final pov = Chess.fromSetup(Setup.parseFen(puzzles.first.fen));
StormState build() {
final pov = Chess.fromSetup(Setup.parseFen(_puzzles.first.fen));
final clock = StormClock();
ref.onDispose(() {
@@ -43,7 +55,7 @@ class StormController extends _$StormController {
firstMovePlayed: false,
mode: StormMode.initial,
puzzleIndex: 0,
puzzle: puzzles.first,
puzzle: _puzzles.first,
moves: 0,
errors: 0,
history: const IList.empty(),
@@ -181,8 +193,8 @@ class StormController extends _$StormController {
state = state.copyWith(
puzzleIndex: newPuzzleIndex,
puzzle: puzzles[newPuzzleIndex],
position: Chess.fromSetup(Setup.parseFen(puzzles[newPuzzleIndex].fen)),
puzzle: _puzzles[newPuzzleIndex],
position: Chess.fromSetup(Setup.parseFen(_puzzles[newPuzzleIndex].fen)),
moveIndex: -1,
numSolved: result ? state.numSolved + 1 : state.numSolved,
lastSolvedTime: DateTime.now(),
@@ -269,7 +281,7 @@ class StormController extends _$StormController {
}
bool _isNextPuzzleAvailable() {
return state.puzzleIndex + 1 < puzzles.length;
return state.puzzleIndex + 1 < _puzzles.length;
}
}
+8 -10
View File
@@ -1,31 +1,29 @@
import 'dart:convert';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/binding.dart';
import 'package:lichess_mobile/src/model/auth/auth_session.dart';
import 'package:lichess_mobile/src/model/common/id.dart';
import 'package:lichess_mobile/src/model/puzzle/puzzle_streak.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
part 'streak_storage.g.dart';
@riverpod
StreakStorage streakStorage(Ref ref, UserId? userId) {
/// Provider for the streak storage for a given user.
final streakStorageProvider = Provider.autoDispose.family<StreakStorage, UserId?>((
Ref ref,
UserId? userId,
) {
return StreakStorage(ref, userId);
}
});
/// Fetches the current streak score from the local storage if available, returns null otherwise.
@riverpod
Future<int?> savedStreakScore(Ref ref) async {
final savedStreakScoreProvider = FutureProvider.autoDispose<int?>((Ref ref) async {
final session = ref.watch(authSessionProvider);
// cannot use ref.watch because it would create a circular dependency
// as we invalidate this provider in the storage saveActiveStreak and clearActiveStreak methods
final streakStorage = ref.read(streakStorageProvider(session?.user.id));
final streak = await streakStorage.loadActiveStreak();
return streak?.index;
}
});
/// Local storage for the current puzzle streak.
class StreakStorage {
+7 -5
View File
@@ -2,18 +2,20 @@ import 'dart:async';
import 'package:collection/collection.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lichess_mobile/src/model/common/id.dart';
import 'package:lichess_mobile/src/model/common/socket.dart';
import 'package:lichess_mobile/src/model/user/user.dart';
import 'package:lichess_mobile/src/network/socket.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'online_friends.g.dart';
typedef OnlineFriend = ({LightUser user, bool playing});
@riverpod
class OnlineFriends extends _$OnlineFriends {
final onlineFriendsProvider = AsyncNotifierProvider.autoDispose<OnlineFriends, IList<OnlineFriend>>(
OnlineFriends.new,
name: 'OnlineFriendsProvider',
);
class OnlineFriends extends AsyncNotifier<IList<OnlineFriend>> {
StreamSubscription<SocketEvent>? _socketSubscription;
StreamSubscription<void>? _socketOpenSubscription;
@@ -2,13 +2,13 @@ import 'package:chessground/chessground.dart';
import 'package:dartchess/dartchess.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/material.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/model/common/chess.dart';
import 'package:lichess_mobile/src/model/settings/preferences_storage.dart';
import 'package:lichess_mobile/src/styles/styles.dart';
import 'package:lichess_mobile/src/utils/color_palette.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'board_preferences.freezed.dart';
part 'board_preferences.g.dart';
@@ -16,8 +16,12 @@ part 'board_preferences.g.dart';
const kBoardDefaultBrightnessFilter = 1.0;
const kBoardDefaultHueFilter = 0.0;
@Riverpod(keepAlive: true)
class BoardPreferences extends _$BoardPreferences with PreferencesStorage<BoardPrefs> {
final boardPreferencesProvider = NotifierProvider<BoardPreferences, BoardPrefs>(
BoardPreferences.new,
name: 'BoardPreferencesProvider',
);
class BoardPreferences extends Notifier<BoardPrefs> with PreferencesStorage<BoardPrefs> {
@override
@protected
PrefCategory get prefCategory => PrefCategory.board;
+6 -4
View File
@@ -1,12 +1,14 @@
import 'package:flutter/foundation.dart' show Brightness;
import 'package:flutter/widgets.dart' show WidgetsBinding;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lichess_mobile/src/model/settings/general_preferences.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'brightness.g.dart';
final currentBrightnessProvider = NotifierProvider<CurrentBrightness, Brightness>(
CurrentBrightness.new,
name: 'CurrentBrightnessProvider',
);
@riverpod
class CurrentBrightness extends _$CurrentBrightness {
class CurrentBrightness extends Notifier<Brightness> {
@override
Brightness build() {
final themeMode = ref.watch(generalPreferencesProvider.select((state) => state.themeMode));
@@ -1,5 +1,6 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.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/model/settings/board_preferences.dart'
@@ -7,13 +8,17 @@ import 'package:lichess_mobile/src/model/settings/board_preferences.dart'
import 'package:lichess_mobile/src/model/settings/preferences_storage.dart';
import 'package:lichess_mobile/src/theme.dart';
import 'package:lichess_mobile/src/utils/json.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'general_preferences.freezed.dart';
part 'general_preferences.g.dart';
@Riverpod(keepAlive: true)
class GeneralPreferences extends _$GeneralPreferences with PreferencesStorage<GeneralPrefs> {
final generalPreferencesProvider = NotifierProvider<GeneralPreferencesNotifier, GeneralPrefs>(
GeneralPreferencesNotifier.new,
name: 'GeneralPreferencesProvider',
);
class GeneralPreferencesNotifier extends Notifier<GeneralPrefs>
with PreferencesStorage<GeneralPrefs> {
@override
@protected
final prefCategory = PrefCategory.general;
@@ -1,12 +1,17 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/model/settings/preferences_storage.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'over_the_board_preferences.freezed.dart';
part 'over_the_board_preferences.g.dart';
@Riverpod(keepAlive: true)
class OverTheBoardPreferences extends _$OverTheBoardPreferences
final overTheBoardPreferencesProvider =
NotifierProvider<OverTheBoardPreferencesNotifier, OverTheBoardPrefs>(
OverTheBoardPreferencesNotifier.new,
name: 'OverTheBoardPreferencesProvider',
);
class OverTheBoardPreferencesNotifier extends Notifier<OverTheBoardPrefs>
with PreferencesStorage<OverTheBoardPrefs> {
@override
@protected
+18 -10
View File
@@ -4,6 +4,7 @@ import 'package:chessground/chessground.dart';
import 'package:collection/collection.dart';
import 'package:dartchess/dartchess.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/model/analysis/common_analysis_state.dart';
import 'package:lichess_mobile/src/model/auth/auth_session.dart';
@@ -25,15 +26,22 @@ import 'package:lichess_mobile/src/network/socket.dart';
import 'package:lichess_mobile/src/utils/rate_limit.dart';
import 'package:lichess_mobile/src/view/engine/engine_gauge.dart';
import 'package:lichess_mobile/src/widgets/pgn.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'study_controller.freezed.dart';
part 'study_controller.g.dart';
@riverpod
class StudyController extends _$StudyController
final studyControllerProvider = AsyncNotifierProvider.autoDispose
.family<StudyController, StudyState, StudyId>(
StudyController.new,
name: 'StudyControllerProvider',
);
class StudyController extends AsyncNotifier<StudyState>
with EngineEvaluationMixin
implements PgnTreeNotifier {
StudyController(this.id);
final StudyId id;
late Root _root;
Timer? _opponentFirstMoveTimer;
@@ -69,7 +77,7 @@ class StudyController extends _$StudyController
StudyState get evaluationState => state.requireValue;
@override
Future<StudyState> build(StudyId id) async {
Future<StudyState> build() async {
ref.onDispose(() {
_opponentFirstMoveTimer?.cancel();
_sendMoveToSocketTimer?.cancel();
@@ -275,7 +283,7 @@ class StudyController extends _$StudyController
}
void onPromotionSelection(Role? role) {
final state = this.state.valueOrNull;
final state = this.state.value;
if (state == null) return;
if (role == null) {
@@ -300,7 +308,7 @@ class StudyController extends _$StudyController
}
void userNext() {
final state = this.state.valueOrNull;
final state = this.state.value;
if (state!.currentNode.children.isEmpty) return;
_setPath(
state.currentPath + _root.nodeAt(state.currentPath).children.first.id,
@@ -332,7 +340,7 @@ class StudyController extends _$StudyController
}
void toggleBoard() {
final state = this.state.valueOrNull;
final state = this.state.value;
if (state != null) {
this.state = AsyncValue.data(state.copyWith(pov: state.pov.opposite));
}
@@ -382,7 +390,7 @@ class StudyController extends _$StudyController
@override
void promoteVariation(UciPath path, bool toMainline) {
final state = this.state.valueOrNull;
final state = this.state.value;
if (state == null) return;
_root.promoteAt(path, toMainline: toMainline);
this.state = AsyncValue.data(
@@ -440,7 +448,7 @@ class StudyController extends _$StudyController
/// Whether the user is navigating through the moves (as opposed to playing a move).
bool isNavigating = false,
}) {
final state = this.state.valueOrNull;
final state = this.state.value;
if (state == null) return;
final pathChange = state.currentPath != path;
+18 -14
View File
@@ -1,22 +1,26 @@
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lichess_mobile/src/model/study/study.dart';
import 'package:lichess_mobile/src/model/study/study_filter.dart';
import 'package:lichess_mobile/src/model/study/study_repository.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'study_list_paginator.g.dart';
typedef StudyList = ({IList<StudyPageItem> studies, int? nextPage});
typedef StudyListNotifierParams = ({StudyCategory category, StudyListOrder order, String? search});
/// A provider that gets a list of studies from the paginated API.
final studyListPaginatorProvider = AsyncNotifierProvider.autoDispose
.family<StudyListPaginatorNotifier, StudyList, StudyListNotifierParams>(
StudyListPaginatorNotifier.new,
name: 'StudyListPaginatorProvider',
);
class StudyListPaginatorNotifier extends AsyncNotifier<StudyList> {
StudyListPaginatorNotifier(this.params);
final StudyListNotifierParams params;
/// Gets a list of studies from the paginated API.
@riverpod
class StudyListPaginator extends _$StudyListPaginator {
@override
Future<StudyList> build({
required StudyCategory category,
required StudyListOrder order,
String? search,
}) {
Future<StudyList> build() {
return _nextPage();
}
@@ -36,8 +40,8 @@ class StudyListPaginator extends _$StudyListPaginator {
final nextPage = state.value?.nextPage ?? 1;
final repo = ref.read(studyRepositoryProvider);
return search == null
? repo.getStudies(category: category, order: order, page: nextPage)
: repo.searchStudies(query: search!, page: nextPage);
return params.search == null
? repo.getStudies(category: params.category, order: params.order, page: nextPage)
: repo.searchStudies(query: params.search!, page: nextPage);
}
}
+7 -3
View File
@@ -1,13 +1,17 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/model/analysis/common_analysis_prefs.dart';
import 'package:lichess_mobile/src/model/settings/preferences_storage.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'study_preferences.freezed.dart';
part 'study_preferences.g.dart';
@Riverpod(keepAlive: true)
class StudyPreferences extends _$StudyPreferences with PreferencesStorage<StudyPrefs> {
final studyPreferencesProvider = NotifierProvider<StudyPreferencesNotifier, StudyPrefs>(
StudyPreferencesNotifier.new,
name: 'StudyPreferencesProvider',
);
class StudyPreferencesNotifier extends Notifier<StudyPrefs> with PreferencesStorage<StudyPrefs> {
@override
@protected
final prefCategory = PrefCategory.study;
+3 -6
View File
@@ -9,14 +9,11 @@ import 'package:lichess_mobile/src/model/study/study.dart';
import 'package:lichess_mobile/src/model/study/study_filter.dart';
import 'package:lichess_mobile/src/model/study/study_list_paginator.dart';
import 'package:lichess_mobile/src/network/http.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'study_repository.g.dart';
@Riverpod(keepAlive: true)
StudyRepository studyRepository(Ref ref) {
/// A provider for [StudyRepository].
final studyRepositoryProvider = Provider<StudyRepository>((Ref ref) {
return StudyRepository(ref, ref.read(lichessClientProvider));
}
}, name: 'StudyRepositoryProvider');
class StudyRepository {
StudyRepository(this.ref, this.client);
@@ -1,5 +1,6 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/model/chat/chat_controller.dart';
import 'package:lichess_mobile/src/model/common/id.dart';
@@ -9,15 +10,22 @@ import 'package:lichess_mobile/src/model/tournament/tournament_repository.dart';
import 'package:lichess_mobile/src/model/tv/tv_socket_events.dart';
import 'package:lichess_mobile/src/network/socket.dart';
import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'tournament_controller.freezed.dart';
part 'tournament_controller.g.dart';
final _logger = Logger('TournamentController');
@riverpod
class TournamentController extends _$TournamentController {
final tournamentControllerProvider = AsyncNotifierProvider.autoDispose
.family<TournamentController, TournamentState, TournamentId>(
TournamentController.new,
name: 'TournamentControllerProvider',
);
class TournamentController extends AsyncNotifier<TournamentState> {
TournamentController(this.id);
final TournamentId id;
StreamSubscription<SocketEvent>? _socketSubscription;
SocketClient? _socketClient;
@@ -34,7 +42,7 @@ class TournamentController extends _$TournamentController {
SocketPool get _socketPool => ref.read(socketPoolProvider);
@override
Future<TournamentState> build(TournamentId id) async {
Future<TournamentState> build() async {
ref.onDispose(() {
_socketSubscription?.cancel();
_pauseDelayTimer?.cancel();
@@ -105,7 +113,7 @@ class TournamentController extends _$TournamentController {
}
void jumpToMyPage() {
if (state.valueOrNull?.tournament.me != null) {
if (state.value?.tournament.me != null) {
_loadPage(_pageOf(state.requireValue.tournament.me!.rank));
}
}
@@ -200,7 +208,7 @@ class TournamentController extends _$TournamentController {
}
void joinOrPause() {
final state = this.state.valueOrNull;
final state = this.state.value;
if (state == null) {
return;
}
@@ -1,5 +1,3 @@
import 'dart:async';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lichess_mobile/src/model/auth/auth_session.dart';
@@ -7,26 +5,22 @@ import 'package:lichess_mobile/src/model/common/id.dart';
import 'package:lichess_mobile/src/model/tournament/tournament.dart';
import 'package:lichess_mobile/src/model/tournament/tournament_repository.dart';
import 'package:lichess_mobile/src/network/http.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'tournament_providers.g.dart';
@riverpod
Future<IList<LightTournament>> featuredTournaments(Ref ref) {
final featuredTournamentsProvider = FutureProvider.autoDispose<IList<LightTournament>>((Ref ref) {
// logged in users get personalized featured tournaments
ref.watch(authSessionProvider);
return ref.read(tournamentRepositoryProvider).featured();
}
}, name: 'FeaturedTournamentsProvider');
@riverpod
Future<TournamentLists> tournaments(Ref ref) {
final tournamentsProvider = FutureProvider.autoDispose<TournamentLists>((Ref ref) {
return ref.read(tournamentRepositoryProvider).getTournaments();
}
}, name: 'TournamentsProvider');
@riverpod
Future<TournamentPlayer> tournamentPlayer(Ref ref, TournamentId tournamentId, UserId userId) {
return ref.withClientCacheFor(
(client) => ref.read(tournamentRepositoryProvider).getTournamentPlayer(tournamentId, userId),
const Duration(seconds: 10),
);
}
final tournamentPlayerProvider = FutureProvider.autoDispose
.family<TournamentPlayer, (TournamentId, UserId)>((Ref ref, (TournamentId, UserId) params) {
return ref.withClientCacheFor(
(client) =>
ref.read(tournamentRepositoryProvider).getTournamentPlayer(params.$1, params.$2),
const Duration(seconds: 10),
);
}, name: 'TournamentPlayerProvider');
@@ -8,14 +8,11 @@ import 'package:lichess_mobile/src/model/common/id.dart';
import 'package:lichess_mobile/src/model/tournament/tournament.dart';
import 'package:lichess_mobile/src/network/aggregator.dart';
import 'package:lichess_mobile/src/network/http.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'tournament_repository.g.dart';
@Riverpod(keepAlive: true)
TournamentRepository tournamentRepository(Ref ref) {
/// A provider for [TournamentRepository].
final tournamentRepositoryProvider = Provider<TournamentRepository>((Ref ref) {
return TournamentRepository(ref.read(lichessClientProvider), ref.read(aggregatorProvider), ref);
}
}, name: 'TournamentRepositoryProvider');
class TournamentRepository {
TournamentRepository(this.client, this.aggregator, Ref ref) : _ref = ref;
+8 -5
View File
@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:dartchess/dartchess.dart';
import 'package:deep_pick/deep_pick.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lichess_mobile/src/model/common/socket.dart';
import 'package:lichess_mobile/src/model/tv/featured_player.dart';
import 'package:lichess_mobile/src/model/tv/tv_channel.dart';
@@ -10,14 +11,16 @@ import 'package:lichess_mobile/src/model/tv/tv_game.dart';
import 'package:lichess_mobile/src/model/tv/tv_repository.dart';
import 'package:lichess_mobile/src/model/tv/tv_socket_events.dart';
import 'package:lichess_mobile/src/network/socket.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'live_tv_channels.g.dart';
typedef LiveTvChannelsState = IMap<TvChannel, TvGameSnapshot>;
@riverpod
class LiveTvChannels extends _$LiveTvChannels {
final liveTvChannelsProvider =
AsyncNotifierProvider.autoDispose<LiveTvChannels, LiveTvChannelsState>(
LiveTvChannels.new,
name: 'LiveTvChannelsProvider',
);
class LiveTvChannels extends AsyncNotifier<LiveTvChannelsState> {
StreamSubscription<SocketEvent>? _socketSubscription;
StreamSubscription<void>? _socketReadySubscription;
+33 -20
View File
@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:dartchess/dartchess.dart';
import 'package:deep_pick/deep_pick.dart';
import 'package:flutter/foundation.dart' show VoidCallback;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/model/common/chess.dart';
import 'package:lichess_mobile/src/model/common/id.dart';
@@ -18,24 +19,36 @@ import 'package:lichess_mobile/src/model/tv/tv_repository.dart';
import 'package:lichess_mobile/src/model/tv/tv_socket_events.dart';
import 'package:lichess_mobile/src/model/user/user_repository.dart';
import 'package:lichess_mobile/src/network/socket.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'tv_controller.freezed.dart';
part 'tv_controller.g.dart';
@riverpod
class TvController extends _$TvController {
typedef TvControllerParams = ({
TvChannel? channel,
(GameId id, Side orientation)? initialGame,
UserId? userId,
});
final tvControllerProvider = AsyncNotifierProvider.autoDispose
.family<TvController, TvState, TvControllerParams>(
TvController.new,
name: 'TvControllerProvider',
);
class TvController extends AsyncNotifier<TvState> {
TvController(this.params);
final TvControllerParams params;
StreamSubscription<SocketEvent>? _socketSubscription;
VoidCallback? _onReload;
@override
Future<TvState> build(
TvChannel? channel, {
(GameId id, Side orientation)? initialGame,
UserId? userId,
}) {
assert(channel != null || userId != null, 'Either a channel or a userId must be provided');
Future<TvState> build() {
assert(
params.channel != null || params.userId != null,
'Either a channel or a userId must be provided',
);
_onReload = ref.invalidateSelf;
@@ -44,7 +57,7 @@ class TvController extends _$TvController {
_onReload = null;
});
return _connectWebsocket(initialGame);
return _connectWebsocket(params.initialGame);
}
SoundService get _soundService => ref.read(soundServiceProvider);
@@ -65,18 +78,18 @@ class TvController extends _$TvController {
if (game != null) {
id = game.$1;
orientation = game.$2;
} else if (channel != null) {
} else if (params.channel != null) {
final channels = await ref.read(tvRepositoryProvider).channels();
final channelGame = channels[channel!]!;
final channelGame = channels[params.channel!]!;
id = channelGame.id;
orientation = channelGame.side ?? Side.white;
} else if (userId != null) {
final game = await ref.read(userRepositoryProvider).getCurrentGame(userId!);
} else if (params.userId != null) {
final game = await ref.read(userRepositoryProvider).getCurrentGame(params.userId!);
id = game.id;
orientation = game.playerSideOf(userId!) ?? Side.white;
orientation = game.playerSideOf(params.userId!) ?? Side.white;
} else {
id = state.valueOrNull?.game.id ?? initialGame!.$1;
orientation = state.valueOrNull?.orientation ?? initialGame!.$2;
id = state.value?.game.id ?? params.initialGame!.$1;
orientation = state.value?.orientation ?? params.initialGame!.$2;
}
final socketClient = ref
@@ -84,7 +97,7 @@ class TvController extends _$TvController {
.open(
Uri(
path: '/watch/$id/${orientation.name}/v6',
queryParameters: userId != null ? {'userTv': userId.toString()} : null,
queryParameters: params.userId != null ? {'userTv': params.userId.toString()} : null,
),
forceReconnect: true,
onEventGapFailure: () {
@@ -245,7 +258,7 @@ class TvController extends _$TvController {
case 'tvSelect':
final json = event.data as Map<String, dynamic>;
final eventChannel = pick(json, 'channel').asTvChannelOrNull();
if (eventChannel != null && eventChannel == channel) {
if (eventChannel != null && eventChannel == params.channel) {
final data = TvSelectEvent.fromJson(json);
_moveToNextGame((data.id, data.orientation));
}
@@ -1,12 +1,17 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/model/settings/preferences_storage.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'game_history_preferences.freezed.dart';
part 'game_history_preferences.g.dart';
@Riverpod(keepAlive: true)
class GameHistoryPreferences extends _$GameHistoryPreferences
final gameHistoryPreferencesProvider =
NotifierProvider<GameHistoryPreferencesNotifier, GameHistoryPrefs>(
GameHistoryPreferencesNotifier.new,
name: 'GameHistoryPreferencesProvider',
);
class GameHistoryPreferencesNotifier extends Notifier<GameHistoryPrefs>
with PreferencesStorage<GameHistoryPrefs> {
@override
@protected
+7 -3
View File
@@ -1,17 +1,21 @@
import 'dart:convert';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/binding.dart';
import 'package:lichess_mobile/src/model/auth/auth_session.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
part 'search_history.g.dart';
part 'search_history.freezed.dart';
@riverpod
class SearchHistory extends _$SearchHistory {
final searchHistoryProvider = NotifierProvider<SearchHistory, SearchHistoryState>(
SearchHistory.new,
name: 'SearchHistoryProvider',
);
class SearchHistory extends Notifier<SearchHistoryState> {
static const maxHistory = 10;
String _storageKey(AuthSessionState? session) =>
@@ -7,57 +7,53 @@ import 'package:lichess_mobile/src/model/user/streamer.dart';
import 'package:lichess_mobile/src/model/user/user.dart';
import 'package:lichess_mobile/src/model/user/user_repository.dart';
import 'package:lichess_mobile/src/network/http.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'user_repository_providers.g.dart';
const _kAutoCompleteDebounceTimer = Duration(milliseconds: 300);
@riverpod
Future<User> user(Ref ref, {required UserId id}) {
final userProvider = FutureProvider.autoDispose.family<User, UserId>((Ref ref, UserId id) {
return ref.read(userRepositoryProvider).getUser(id, withCanChallenge: true);
}
}, name: 'UserProvider');
@riverpod
Future<UserPerfStats> userPerfStats(Ref ref, {required UserId id, required Perf perf}) {
return ref.read(userRepositoryProvider).getPerfStats(id, perf);
}
final userPerfStatsProvider = FutureProvider.autoDispose.family<UserPerfStats, (UserId, Perf)>((
Ref ref,
(UserId, Perf) params,
) {
return ref.read(userRepositoryProvider).getPerfStats(params.$1, params.$2);
}, name: 'UserPerfStatsProvider');
@riverpod
Future<IList<Streamer>> liveStreamers(Ref ref) {
final liveStreamersProvider = FutureProvider.autoDispose<IList<Streamer>>((Ref ref) {
return ref.withAggregatorCacheFor(
(client, aggregator) => UserRepository(client, aggregator).getLiveStreamers(),
const Duration(minutes: 1),
);
}
}, name: 'LiveStreamersProvider');
@riverpod
Future<Top1Leaderboard> top1(Ref ref) {
final top1Provider = FutureProvider.autoDispose<Top1Leaderboard>((Ref ref) {
return ref.withAggregatorCacheFor(
(client, aggregator) => UserRepository(client, aggregator).getTop1(),
const Duration(hours: 12),
);
}
}, name: 'Top1Provider');
@riverpod
Future<Leaderboard> leaderboard(Ref ref) {
final leaderboardProvider = FutureProvider.autoDispose<Leaderboard>((Ref ref) {
return ref.withAggregatorCacheFor(
(client, aggregator) => UserRepository(client, aggregator).getLeaderboard(),
const Duration(hours: 2),
);
}
}, name: 'LeaderboardProvider');
@riverpod
Future<IList<User>> onlineBots(Ref ref) {
final onlineBotsProvider = FutureProvider.autoDispose<IList<User>>((Ref ref) {
return ref.withAggregatorCacheFor(
(client, aggregator) =>
UserRepository(client, aggregator).getOnlineBots().then((bots) => bots.toIList()),
const Duration(hours: 5),
);
}
}, name: 'OnlineBotsProvider');
@riverpod
Future<IList<LightUser>> autoCompleteUser(Ref ref, String term) async {
final autoCompleteUserProvider = FutureProvider.autoDispose.family<IList<LightUser>, String>((
Ref ref,
String term,
) async {
// debounce calls as user might be typing
var didDispose = false;
ref.onDispose(() => didDispose = true);
@@ -67,17 +63,23 @@ Future<IList<LightUser>> autoCompleteUser(Ref ref, String term) async {
}
return ref.read(userRepositoryProvider).autocompleteUser(term);
}
}, name: 'AutoCompleteUserProvider');
@riverpod
Future<IList<UserRatingHistoryPerf>> userRatingHistory(Ref ref, {required UserId id}) {
return ref.withAggregatorCacheFor(
(client, aggregator) => UserRepository(client, aggregator).getRatingHistory(id),
const Duration(minutes: 1),
);
}
final userRatingHistoryProvider = FutureProvider.autoDispose
.family<IList<UserRatingHistoryPerf>, UserId>((Ref ref, UserId id) {
return ref.withAggregatorCacheFor(
(client, aggregator) => UserRepository(client, aggregator).getRatingHistory(id),
const Duration(minutes: 1),
);
}, name: 'UserRatingHistoryProvider');
@riverpod
Future<Crosstable> crosstable(Ref ref, UserId userId1, UserId userId2, {bool matchup = true}) {
return ref.read(userRepositoryProvider).getCrosstable(userId1, userId2, matchup: matchup);
}
typedef CrosstableProviderParams = ({UserId userId1, UserId userId2, bool matchup});
final crosstableProvider = FutureProvider.autoDispose.family<Crosstable, CrosstableProviderParams>((
Ref ref,
CrosstableProviderParams params,
) {
return ref
.read(userRepositoryProvider)
.getCrosstable(params.userId1, params.userId2, matchup: params.matchup);
}, name: 'CrosstableProvider');
+9 -8
View File
@@ -8,23 +8,24 @@ import 'package:lichess_mobile/src/constants.dart';
import 'package:lichess_mobile/src/network/http.dart';
import 'package:lichess_mobile/src/utils/rate_limit.dart';
import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'connectivity.g.dart';
final _logger = Logger('Connectivity');
/// A provider that exposes a [Connectivity] instance.
@Riverpod(keepAlive: true)
Connectivity connectivityPlugin(Ref _) => Connectivity();
final connectivityPluginProvider = Provider<Connectivity>((Ref _) => Connectivity());
/// This provider is used to check the device's connectivity status, reacting to
/// changes in connectivity and app lifecycle events.
///
/// - Uses the [Connectivity] plugin to listen to connectivity changes
/// - Uses [AppLifecycleListener] to check connectivity on app resume
@Riverpod(keepAlive: true)
class ConnectivityChanges extends _$ConnectivityChanges {
final connectivityChangesProvider =
AsyncNotifierProvider<ConnectivityChangesNotifier, ConnectivityStatus>(
ConnectivityChangesNotifier.new,
name: 'ConnectivityChangesProvider',
);
class ConnectivityChangesNotifier extends AsyncNotifier<ConnectivityStatus> {
StreamSubscription<List<ConnectivityResult>>? _connectivitySubscription;
AppLifecycleListener? _appLifecycleListener;
@@ -83,7 +84,7 @@ class ConnectivityChanges extends _$ConnectivityChanges {
if (newIsOnline != wasOnline) {
_logger.info('Connectivity status: $result, isOnline: $isOnline');
state = AsyncValue.data((isOnline: newIsOnline, appState: state.valueOrNull?.appState));
state = AsyncValue.data((isOnline: newIsOnline, appState: state.value?.appState));
}
}
+8 -15
View File
@@ -30,9 +30,6 @@ import 'package:lichess_mobile/src/model/user/user.dart';
import 'package:lichess_mobile/src/network/aggregator.dart';
import 'package:logging/logging.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'http.g.dart';
final _logger = Logger('HttpClient');
@@ -82,8 +79,7 @@ class HttpClientFactory {
/// The global [HttpClientFactory] provider.
///
/// Http clients created by this factory log all requests and responses to the database.
@Riverpod(keepAlive: true)
HttpClientFactory httpClientFactory(Ref ref) {
final httpClientFactoryProvider = Provider<HttpClientFactory>((Ref ref) {
return HttpClientFactory(
wrapper: (client) => _RegisterCallbackClient(
client,
@@ -123,28 +119,26 @@ HttpClientFactory httpClientFactory(Ref ref) {
},
),
);
}
});
/// The default http client.
///
/// This client is used for all requests that don't go to the lichess server, for
/// example, requests to lichess CDN, or other APIs.
/// Only one instance of this client is created and kept alive for the whole app.
@Riverpod(keepAlive: true)
Client defaultClient(Ref ref) {
final defaultClientProvider = Provider<Client>((Ref ref) {
final client = _RegisterCallbackClient(
ref.read(httpClientFactoryProvider)(),
onRequest: (request) => _logger.info('${request.method} ${request.url}'),
);
ref.onDispose(() => client.close());
return client;
}
});
/// The http client configured to make requests to the lichess API.
///
/// Only one instance of this client is created and kept alive for the whole app.
@Riverpod(keepAlive: true)
LichessClient lichessClient(Ref ref) {
final lichessClientProvider = Provider<LichessClient>((Ref ref) {
final client = LichessClient(
// Retry just once, after 500ms, on 429 Too Many Requests.
RetryClient(
@@ -157,13 +151,12 @@ LichessClient lichessClient(Ref ref) {
);
ref.onDispose(() => client.close());
return client;
}
});
Duration _defaultDelay(int retryCount) =>
const Duration(milliseconds: 900) * math.pow(1.5, retryCount);
@Riverpod(keepAlive: true)
String userAgent(Ref ref) {
final userAgentProvider = Provider<String>((Ref ref) {
final session = ref.watch(authSessionProvider);
return makeUserAgent(
@@ -172,7 +165,7 @@ String userAgent(Ref ref) {
ref.read(preloadedDataProvider).requireValue.sri,
session?.user,
);
}
});
/// Creates a user-agent string with the app version, build number, and device info and possibly the user ID if a user is logged in.
String makeUserAgent(PackageInfo info, BaseDeviceInfo deviceInfo, String sri, LightUser? user) {
+17 -11
View File
@@ -17,12 +17,9 @@ import 'package:lichess_mobile/src/model/common/socket.dart';
import 'package:lichess_mobile/src/network/http.dart';
import 'package:logging/logging.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:web_socket_channel/io.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
part 'socket.g.dart';
const kDefaultSocketRoute = '/socket/v5';
const _kDefaultConnectTimeout = Duration(seconds: 10);
@@ -624,8 +621,8 @@ class SocketPool {
}
}
@Riverpod(keepAlive: true)
SocketPool socketPool(Ref ref) {
/// The global socket pool provider.
final socketPoolProvider = Provider<SocketPool>((Ref ref) {
final pool = SocketPool(ref);
Timer? closeInBackgroundTimer;
@@ -654,17 +651,26 @@ SocketPool socketPool(Ref ref) {
});
return pool;
}
}, name: 'SocketPoolProvider');
typedef SocketPingState = ({Duration averageLag, int rating});
/// A provider that exposes the average lag and ping rating for a given socket route.
final socketPingProvider = NotifierProvider.autoDispose
.family<SocketPingNotifier, SocketPingState, Uri?>(
SocketPingNotifier.new,
name: 'SocketPingProvider',
);
/// Average lag and ping rating computed from WebSocket ping/pong protocol.
///
/// If [route] is provided, it will return the average lag for that route only, and if any other route
/// is active, it will return [Duration.zero], meaning the socket is not connected.
/// If no route is provided, it will return the average lag for the current active route.
@riverpod
class SocketPing extends _$SocketPing {
class SocketPingNotifier extends Notifier<SocketPingState> {
SocketPingNotifier(this.route);
final Uri? route;
@override
SocketPingState build({Uri? route}) {
final pool = ref.watch(socketPoolProvider);
@@ -708,10 +714,10 @@ class SocketPing extends _$SocketPing {
}
}
@Riverpod(keepAlive: true)
WebSocketChannelFactory webSocketChannelFactory(Ref ref) {
/// A provider for the [WebSocketChannelFactory].
final webSocketChannelFactoryProvider = Provider<WebSocketChannelFactory>((Ref ref) {
return const WebSocketChannelFactory();
}
});
/// A factory to create a [WebSocketChannel].
///
+3 -6
View File
@@ -15,19 +15,16 @@ import 'package:lichess_mobile/src/view/game/game_screen.dart';
import 'package:lichess_mobile/src/view/game/game_screen_providers.dart';
import 'package:lichess_mobile/src/view/puzzle/puzzle_screen.dart';
import 'package:quick_actions/quick_actions.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'quick_actions.g.dart';
@Riverpod(keepAlive: true)
QuickActionService quickActionService(Ref ref) {
/// Provider for the [QuickActionService].
final quickActionServiceProvider = Provider<QuickActionService>((Ref ref) {
final service = QuickActionService(ref);
ref.listen<RecentGameSeekPrefs>(recentGameSeekProvider, (previous, next) {
if (previous?.seeks == next.seeks) return;
service.setQuickActions(next.seeks);
});
return service;
}
});
class QuickActionService {
QuickActionService(this.ref);
+1
View File
@@ -4,6 +4,7 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod/legacy.dart';
import 'package:lichess_mobile/l10n/l10n.dart';
import 'package:lichess_mobile/src/constants.dart';
import 'package:lichess_mobile/src/utils/l10n_context.dart';

Some files were not shown because too many files have changed in this diff Show More