From 5db848128b544015777f8422791263379ac3f892 Mon Sep 17 00:00:00 2001 From: ZTL-UwU Date: Sat, 24 Feb 2024 20:45:13 +0800 Subject: [PATCH 01/34] WIP on user full game list --- lib/src/model/account/account_repository.dart | 4 +- lib/src/model/game/game_repository.dart | 9 +- lib/src/view/game/game_list_tile.dart | 104 ++++++++++++ lib/src/view/home/home_tab_screen.dart | 2 +- lib/src/view/user/full_games_screen.dart | 156 ++++++++++++++++++ lib/src/view/user/recent_games.dart | 114 ++----------- test/model/game/game_repository_test.dart | 2 +- 7 files changed, 288 insertions(+), 103 deletions(-) create mode 100644 lib/src/view/user/full_games_screen.dart diff --git a/lib/src/model/account/account_repository.dart b/lib/src/model/account/account_repository.dart index 4943e72a46..751721ca63 100644 --- a/lib/src/model/account/account_repository.dart +++ b/lib/src/model/account/account_repository.dart @@ -50,11 +50,13 @@ Future> accountActivity(AccountActivityRef ref) async { @riverpod Future> accountRecentGames( AccountRecentGamesRef ref, + int gameCount, ) async { final session = ref.watch(authSessionProvider); if (session == null) return IList(); return ref.withClientCacheFor( - (client) => GameRepository(client).getRecentGames(session.user.id), + (client) => + GameRepository(client).getRecentGames(session.user.id, gameCount), const Duration(hours: 1), ); } diff --git a/lib/src/model/game/game_repository.dart b/lib/src/model/game/game_repository.dart index 9e9bd21e9f..a02a784c83 100644 --- a/lib/src/model/game/game_repository.dart +++ b/lib/src/model/game/game_repository.dart @@ -32,10 +32,15 @@ class GameRepository { } } - Future> getRecentGames(UserId userId) { + Future> getRecentGames( + UserId userId, + int gameCount, + ) { return client.readNdJsonList( Uri.parse( - '$kLichessHost/api/games/user/$userId?max=10&moves=false&lastFen=true&accuracy=true&opening=true', + gameCount == -1 + ? '$kLichessHost/api/games/user/$userId?moves=false&lastFen=true&accuracy=true&opening=true' + : '$kLichessHost/api/games/user/$userId?max=$gameCount&moves=false&lastFen=true&accuracy=true&opening=true', ), headers: {'Accept': 'application/x-ndjson'}, mapper: LightArchivedGame.fromServerJson, diff --git a/lib/src/view/game/game_list_tile.dart b/lib/src/view/game/game_list_tile.dart index 3bfff13fc7..87749804f9 100644 --- a/lib/src/view/game/game_list_tile.dart +++ b/lib/src/view/game/game_list_tile.dart @@ -5,20 +5,26 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; import 'package:lichess_mobile/src/model/game/game_share_service.dart'; +import 'package:lichess_mobile/src/model/game/game_status.dart'; import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/chessground_compat.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; +import 'package:lichess_mobile/src/view/game/archived_game_screen.dart'; +import 'package:lichess_mobile/src/view/game/standalone_game_screen.dart'; import 'package:lichess_mobile/src/view/game/status_l10n.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/board_thumbnail.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/user_full_name.dart'; +import 'package:timeago/timeago.dart' as timeago; /// A list tile that shows game info. class GameListTile extends StatelessWidget { @@ -365,3 +371,101 @@ class _ContextMenu extends ConsumerWidget { ); } } + +class ExtendedGameListTile extends StatelessWidget { + const ExtendedGameListTile({required this.game, this.userId}); + + final LightArchivedGame game; + final UserId? userId; + + Widget getResultIcon(LightArchivedGame game, Side mySide) { + if (game.status == GameStatus.aborted || + game.status == GameStatus.noStart) { + return const Icon( + CupertinoIcons.xmark_square_fill, + color: LichessColors.grey, + ); + } else { + return game.winner == null + ? const Icon( + CupertinoIcons.equal_square_fill, + color: LichessColors.brag, + ) + : game.winner == mySide + ? const Icon( + CupertinoIcons.plus_square_fill, + color: LichessColors.good, + ) + : const Icon( + CupertinoIcons.minus_square_fill, + color: LichessColors.red, + ); + } + } + + @override + Widget build(BuildContext context) { + final mySide = game.white.user?.id == userId ? Side.white : Side.black; + final me = game.white.user?.id == userId ? game.white : game.black; + final opponent = game.white.user?.id == userId ? game.black : game.white; + + return GameListTile( + game: game, + mySide: userId == game.white.user?.id ? Side.white : Side.black, + onTap: game.variant.isSupported + ? () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => game.fullId != null + ? StandaloneGameScreen( + params: InitialStandaloneGameParams( + id: game.fullId!, + ), + ) + : ArchivedGameScreen( + gameData: game, + orientation: userId == game.white.user?.id + ? Side.white + : Side.black, + ), + ); + } + : null, + icon: game.perf.icon, + playerTitle: UserFullNameWidget.player( + user: opponent.user, + aiLevel: opponent.aiLevel, + rating: opponent.rating, + ), + subtitle: Text( + timeago.format(game.lastMoveAt), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (me.analysis != null) ...[ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + CupertinoIcons.chart_bar_alt_fill, + color: textShade(context, 0.5), + ), + Text( + me.analysis!.accuracy.toString(), + style: TextStyle( + fontSize: 10, + color: textShade(context, Styles.subtitleOpacity), + ), + ), + ], + ), + const SizedBox(width: 5), + ], + getResultIcon(game, mySide), + ], + ), + ); + } +} diff --git a/lib/src/view/home/home_tab_screen.dart b/lib/src/view/home/home_tab_screen.dart index 89acb7e05d..d6a3dc096c 100644 --- a/lib/src/view/home/home_tab_screen.dart +++ b/lib/src/view/home/home_tab_screen.dart @@ -163,7 +163,7 @@ class _HomeScreenState extends ConsumerState { Future _refreshData() { return Future.wait([ - ref.refresh(accountRecentGamesProvider.future), + ref.refresh(accountRecentGamesProvider(10).future), ref.refresh(ongoingGamesProvider.future), ]); } diff --git a/lib/src/view/user/full_games_screen.dart b/lib/src/view/user/full_games_screen.dart new file mode 100644 index 0000000000..360a7beba8 --- /dev/null +++ b/lib/src/view/user/full_games_screen.dart @@ -0,0 +1,156 @@ +import 'package:collection/collection.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/account/account_repository.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/game/archived_game.dart'; +import 'package:lichess_mobile/src/model/game/game_repository.dart'; +import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/view/game/game_list_tile.dart'; +import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'full_games_screen.g.dart'; + +/// Create a Screen with Top 10 players for each Lichess Variant +class FullGameScreen extends StatelessWidget { + const FullGameScreen({this.user, super.key}); + final LightUser? user; + + @override + Widget build(BuildContext context) { + return PlatformWidget(androidBuilder: _buildAndroid, iosBuilder: _buildIos); + } + + Widget _buildIos(BuildContext context) { + return CupertinoPageScaffold( + navigationBar: const CupertinoNavigationBar( + previousPageTitle: 'Home', + middle: Text('Full Game History'), + ), + child: _Body(user: user), + ); + } + + Widget _buildAndroid(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Full Game History'), + ), + body: _Body(user: user), + ); + } +} + +@riverpod +Future> _userFullGames( + _UserFullGamesRef ref, { + required UserId userId, +}) { + return ref.withClientCacheFor( + (client) => GameRepository(client).getRecentGames(userId, -1), + // cache is important because the associated widget is in a [ListView] and + // the provider may be instanciated multiple times in a short period of time + // (e.g. when scrolling) + // TODO: consider debouncing the request instead of caching it, or make the + // request in the parent widget and pass the result to the child + const Duration(minutes: 1), + ); +} + +class _Body extends ConsumerWidget { + const _Body({this.user}); + final LightUser? user; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final fullGames = user != null + ? ref.watch(_userFullGamesProvider(userId: user!.id)) + : ref.watch(accountRecentGamesProvider(-1)); + + final userId = user?.id ?? ref.watch(authSessionProvider)?.user.id; + + return fullGames.when( + data: (data) { + return _GameList(gameListData: data, userId: userId); + }, + error: (error, stackTrace) { + debugPrint( + 'SEVERE: [RecentGames] could not recent games; $error\n$stackTrace', + ); + return Padding( + padding: Styles.bodySectionPadding, + child: const Text('Could not load recent games.'), + ); + }, + loading: () => const Center(child: CircularProgressIndicator.adaptive()), + ); + } +} + +class _GameList extends StatefulWidget { + const _GameList({required this.gameListData, this.userId}); + final IList gameListData; + final UserId? userId; + + @override + _GameListState createState() => _GameListState(); +} + +class _GameListState extends State<_GameList> { + ScrollController controller = ScrollController(); + + late IList> gameListData; + late List displayGames; + late UserId? userId; + int count = 0; + + @override + void initState() { + super.initState(); + userId = widget.userId; + gameListData = widget.gameListData.slices(10).toIList(); + displayGames = gameListData[0]; + controller = ScrollController()..addListener(_scrollListener); + } + + @override + void dispose() { + controller.removeListener(_scrollListener); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Scrollbar( + child: ListView.builder( + controller: controller, + itemBuilder: (context, index) { + return ExtendedGameListTile( + game: displayGames[index], + userId: userId, + ); + }, + itemCount: displayGames.length, + ), + ), + ); + } + + void _scrollListener() { + if (controller.position.extentAfter < 500) { + setState(() { + if (count < gameListData.length - 1) { + count++; + displayGames.addAll(gameListData[count]); + } + }); + } + } +} diff --git a/lib/src/view/user/recent_games.dart b/lib/src/view/user/recent_games.dart index 6cc65d9c94..b9135cfbb2 100644 --- a/lib/src/view/user/recent_games.dart +++ b/lib/src/view/user/recent_games.dart @@ -1,4 +1,3 @@ -import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -9,20 +8,16 @@ import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; import 'package:lichess_mobile/src/model/game/game_repository.dart'; -import 'package:lichess_mobile/src/model/game/game_status.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; -import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; -import 'package:lichess_mobile/src/view/game/archived_game_screen.dart'; import 'package:lichess_mobile/src/view/game/game_list_tile.dart'; -import 'package:lichess_mobile/src/view/game/standalone_game_screen.dart'; +import 'package:lichess_mobile/src/view/user/full_games_screen.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; -import 'package:lichess_mobile/src/widgets/user_full_name.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:timeago/timeago.dart' as timeago; part 'recent_games.g.dart'; @@ -32,7 +27,7 @@ Future> _userRecentGames( required UserId userId, }) { return ref.withClientCacheFor( - (client) => GameRepository(client).getRecentGames(userId), + (client) => GameRepository(client).getRecentGames(userId, 10), // cache is important because the associated widget is in a [ListView] and // the provider may be instanciated multiple times in a short period of time // (e.g. when scrolling) @@ -51,35 +46,10 @@ class RecentGames extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final recentGames = user != null ? ref.watch(_userRecentGamesProvider(userId: user!.id)) - : ref.watch(accountRecentGamesProvider); + : ref.watch(accountRecentGamesProvider(10)); final userId = user?.id ?? ref.watch(authSessionProvider)?.user.id; - Widget getResultIcon(LightArchivedGame game, Side mySide) { - if (game.status == GameStatus.aborted || - game.status == GameStatus.noStart) { - return const Icon( - CupertinoIcons.xmark_square_fill, - color: LichessColors.grey, - ); - } else { - return game.winner == null - ? const Icon( - CupertinoIcons.equal_square_fill, - color: LichessColors.brag, - ) - : game.winner == mySide - ? const Icon( - CupertinoIcons.plus_square_fill, - color: LichessColors.good, - ) - : const Icon( - CupertinoIcons.minus_square_fill, - color: LichessColors.red, - ); - } - } - return recentGames.when( data: (data) { if (data.isEmpty) { @@ -88,71 +58,19 @@ class RecentGames extends ConsumerWidget { return ListSection( header: Text(context.l10n.recentGames), hasLeading: true, + headerTrailing: NoPaddingTextButton( + onPressed: () { + pushPlatformRoute( + context, + builder: (context) => FullGameScreen(user: user), + ); + }, + child: Text( + context.l10n.more, + ), + ), children: data.map((game) { - final mySide = - game.white.user?.id == userId ? Side.white : Side.black; - final me = game.white.user?.id == userId ? game.white : game.black; - final opponent = - game.white.user?.id == userId ? game.black : game.white; - - return GameListTile( - game: game, - mySide: userId == game.white.user?.id ? Side.white : Side.black, - onTap: game.variant.isSupported - ? () { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => game.fullId != null - ? StandaloneGameScreen( - params: InitialStandaloneGameParams( - id: game.fullId!, - ), - ) - : ArchivedGameScreen( - gameData: game, - orientation: userId == game.white.user?.id - ? Side.white - : Side.black, - ), - ); - } - : null, - icon: game.perf.icon, - playerTitle: UserFullNameWidget.player( - user: opponent.user, - aiLevel: opponent.aiLevel, - rating: opponent.rating, - ), - subtitle: Text( - timeago.format(game.lastMoveAt), - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (me.analysis != null) ...[ - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - CupertinoIcons.chart_bar_alt_fill, - color: textShade(context, 0.5), - ), - Text( - me.analysis!.accuracy.toString(), - style: TextStyle( - fontSize: 10, - color: textShade(context, Styles.subtitleOpacity), - ), - ), - ], - ), - const SizedBox(width: 5), - ], - getResultIcon(game, mySide), - ], - ), - ); + return ExtendedGameListTile(game: game, userId: userId); }).toList(), ); }, diff --git a/test/model/game/game_repository_test.dart b/test/model/game/game_repository_test.dart index 060b4e6ae8..5fe9298650 100644 --- a/test/model/game/game_repository_test.dart +++ b/test/model/game/game_repository_test.dart @@ -26,7 +26,7 @@ void main() { final repo = GameRepository(mockClient); - final result = await repo.getRecentGames(const UserId('testUser')); + final result = await repo.getRecentGames(const UserId('testUser'), 10); expect(result, isA>()); expect(result.length, 3); expect(result[0].id, const GameId('Huk88k3D')); From 7c94aae4b5234627894b55c6a5fb25fa5b534245 Mon Sep 17 00:00:00 2001 From: ZTL-UwU Date: Sat, 24 Feb 2024 22:29:19 +0800 Subject: [PATCH 02/34] Add bookmark button --- lib/src/view/user/full_games_screen.dart | 104 +++++++++++++++++++++-- 1 file changed, 99 insertions(+), 5 deletions(-) diff --git a/lib/src/view/user/full_games_screen.dart b/lib/src/view/user/full_games_screen.dart index 360a7beba8..007444680a 100644 --- a/lib/src/view/user/full_games_screen.dart +++ b/lib/src/view/user/full_games_screen.dart @@ -2,6 +2,7 @@ import 'package:collection/collection.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; @@ -81,11 +82,11 @@ class _Body extends ConsumerWidget { }, error: (error, stackTrace) { debugPrint( - 'SEVERE: [RecentGames] could not recent games; $error\n$stackTrace', + 'SEVERE: [FullGames] could not load game history; $error\n$stackTrace', ); return Padding( padding: Styles.bodySectionPadding, - child: const Text('Could not load recent games.'), + child: const Text('Could not load game history.'), ); }, loading: () => const Center(child: CircularProgressIndicator.adaptive()), @@ -132,9 +133,17 @@ class _GameListState extends State<_GameList> { child: ListView.builder( controller: controller, itemBuilder: (context, index) { - return ExtendedGameListTile( - game: displayGames[index], - userId: userId, + return _SlideMenu( + menuItems: [ + IconButton( + icon: const Icon(Icons.bookmark_add_outlined), + onPressed: () {}, + ), + ], + child: ExtendedGameListTile( + game: displayGames[index], + userId: userId, + ), ); }, itemCount: displayGames.length, @@ -154,3 +163,88 @@ class _GameListState extends State<_GameList> { } } } + +class _SlideMenu extends StatefulWidget { + const _SlideMenu({required this.child, required this.menuItems}); + final Widget child; + final List menuItems; + + @override + _SlideMenuState createState() => _SlideMenuState(); +} + +class _SlideMenuState extends State<_SlideMenu> + with SingleTickerProviderStateMixin { + late AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 200), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final animation = Tween(begin: Offset.zero, end: const Offset(-0.15, 0.0)) + .animate(CurveTween(curve: Curves.decelerate).animate(_controller)); + + return GestureDetector( + onHorizontalDragUpdate: (data) { + setState(() { + _controller.value -= data.primaryDelta! / context.size!.width * 1.5; + }); + }, + onHorizontalDragEnd: (data) { + if (data.primaryVelocity! > 1500) { + _controller.animateTo(0); + } else if (_controller.value >= 0.3 || data.primaryVelocity! < -1500) { + _controller.animateTo(1.0); + } else { + _controller.animateTo(0); + } + }, + child: Stack( + children: [ + SlideTransition(position: animation, child: widget.child), + Positioned.fill( + child: LayoutBuilder( + builder: (context, constraint) { + return AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return Stack( + children: [ + Positioned( + right: 0.1, + top: 0, + bottom: 0, + width: constraint.maxWidth * animation.value.dx * -1, + child: Row( + children: widget.menuItems.map((child) { + return Expanded( + child: child, + ); + }).toList(), + ), + ), + ], + ); + }, + ); + }, + ), + ), + ], + ), + ); + } +} From 850562927c3c6c26accb67754263f7da4d23ae64 Mon Sep 17 00:00:00 2001 From: ZTL-UwU Date: Sat, 24 Feb 2024 22:35:32 +0800 Subject: [PATCH 03/34] Fix analysis --- lib/src/view/user/full_games_screen.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/view/user/full_games_screen.dart b/lib/src/view/user/full_games_screen.dart index 007444680a..1b290d55de 100644 --- a/lib/src/view/user/full_games_screen.dart +++ b/lib/src/view/user/full_games_screen.dart @@ -2,7 +2,6 @@ import 'package:collection/collection.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; From 78ff233c0c5c6471b91592512b90a6dcc04a72f1 Mon Sep 17 00:00:00 2001 From: ZTL-UwU Date: Sun, 25 Feb 2024 17:11:27 +0800 Subject: [PATCH 04/34] Add pagination --- lib/src/model/account/account_repository.dart | 17 ++- lib/src/model/game/archived_game.dart | 41 ++++++- lib/src/model/game/game_repository.dart | 20 ++-- lib/src/view/home/home_tab_screen.dart | 2 +- lib/src/view/user/full_games_screen.dart | 101 +++++++++++------- lib/src/view/user/recent_games.dart | 4 +- test/model/game/game_repository_test.dart | 2 +- 7 files changed, 132 insertions(+), 55 deletions(-) diff --git a/lib/src/model/account/account_repository.dart b/lib/src/model/account/account_repository.dart index 751721ca63..06a1ce1e43 100644 --- a/lib/src/model/account/account_repository.dart +++ b/lib/src/model/account/account_repository.dart @@ -50,13 +50,24 @@ Future> accountActivity(AccountActivityRef ref) async { @riverpod Future> accountRecentGames( AccountRecentGamesRef ref, - int gameCount, ) async { final session = ref.watch(authSessionProvider); if (session == null) return IList(); return ref.withClientCacheFor( - (client) => - GameRepository(client).getRecentGames(session.user.id, gameCount), + (client) => GameRepository(client).getRecentGames(session.user.id), + const Duration(hours: 1), + ); +} + +@riverpod +Future accountFullGames( + AccountFullGamesRef ref, + int page, +) async { + final session = ref.watch(authSessionProvider); + if (session == null) return null; + return ref.withClientCacheFor( + (client) => GameRepository(client).getFullGames(session.user.id, page), const Duration(hours: 1), ); } diff --git a/lib/src/model/game/archived_game.dart b/lib/src/model/game/archived_game.dart index 657bc947cb..c9f2afe18b 100644 --- a/lib/src/model/game/archived_game.dart +++ b/lib/src/model/game/archived_game.dart @@ -103,6 +103,25 @@ class LightArchivedGame with _$LightArchivedGame { } } +@freezed +class FullGamePaginator with _$FullGamePaginator { + const FullGamePaginator._(); + + const factory FullGamePaginator({ + int? currentPage, + int? maxPerPage, + int? previousPage, + int? nextPage, + int? nbResults, + int? nbPages, + required IList games, + }) = _FullGamePaginator; + + factory FullGamePaginator.fromServerJson(Map json) { + return _fullGamePaginatorFromPick(pick(json, 'paginator').required()); + } +} + IList? gameEvalsFromPick(RequiredPick pick) { return pick('analysis') .asListOrNull( @@ -198,20 +217,36 @@ LightArchivedGame _lightArchivedGameFromPick(RequiredPick pick) { rated: pick('rated').asBoolOrThrow(), speed: pick('speed').asSpeedOrThrow(), perf: pick('perf').asPerfOrThrow(), - createdAt: pick('createdAt').asDateTimeFromMillisecondsOrThrow(), - lastMoveAt: pick('lastMoveAt').asDateTimeFromMillisecondsOrThrow(), + createdAt: pick('timestamp').asDateTimeFromMillisecondsOrNull() ?? + pick('createdAt').asDateTimeFromMillisecondsOrThrow(), + lastMoveAt: pick('timestamp').asDateTimeFromMillisecondsOrNull() ?? + pick('lastMoveAt').asDateTimeFromMillisecondsOrThrow(), status: pick('status').asGameStatusOrThrow(), white: pick('players', 'white').letOrThrow(_playerFromUserGamePick), black: pick('players', 'black').letOrThrow(_playerFromUserGamePick), winner: pick('winner').asSideOrNull(), variant: pick('variant').asVariantOrThrow(), - lastFen: pick('lastFen').asStringOrNull(), + lastFen: pick('lastFen').asStringOrNull() ?? pick('fen').asStringOrNull(), lastMove: pick('lastMove').asUciMoveOrNull(), clock: pick('clock').letOrNull(_clockDataFromPick), opening: pick('opening').letOrNull(_openingFromPick), ); } +FullGamePaginator _fullGamePaginatorFromPick(RequiredPick pick) { + return FullGamePaginator( + currentPage: pick('currentPage').asIntOrNull(), + maxPerPage: pick('maxPerPage').asIntOrNull(), + previousPage: pick('previousPage').asIntOrNull(), + nextPage: pick('nextPage').asIntOrNull(), + nbResults: pick('nbResults').asIntOrNull(), + nbPages: pick('nbPages').asIntOrNull(), + games: IList( + pick('currentPageResults').asListOrThrow(_lightArchivedGameFromPick), + ), + ); +} + LightOpening _openingFromPick(RequiredPick pick) { return LightOpening( eco: pick('eco').asStringOrThrow(), diff --git a/lib/src/model/game/game_repository.dart b/lib/src/model/game/game_repository.dart index a02a784c83..1e07e82f99 100644 --- a/lib/src/model/game/game_repository.dart +++ b/lib/src/model/game/game_repository.dart @@ -32,21 +32,27 @@ class GameRepository { } } - Future> getRecentGames( - UserId userId, - int gameCount, - ) { + Future> getRecentGames(UserId userId) { return client.readNdJsonList( Uri.parse( - gameCount == -1 - ? '$kLichessHost/api/games/user/$userId?moves=false&lastFen=true&accuracy=true&opening=true' - : '$kLichessHost/api/games/user/$userId?max=$gameCount&moves=false&lastFen=true&accuracy=true&opening=true', + '$kLichessHost/api/games/user/$userId?max=10&moves=false&lastFen=true&accuracy=true&opening=true', ), headers: {'Accept': 'application/x-ndjson'}, mapper: LightArchivedGame.fromServerJson, ); } + Future getFullGames( + UserId userId, + int page, + ) { + return client.readJson( + Uri.parse('$kLichessHost/@/$userId/all?page=$page'), + headers: {'Accept': 'application/json'}, + mapper: FullGamePaginator.fromServerJson, + ); + } + /// Returns the games of the current user, given a list of ids. Future> getMyGamesByIds(ISet ids) { if (ids.isEmpty) { diff --git a/lib/src/view/home/home_tab_screen.dart b/lib/src/view/home/home_tab_screen.dart index d6a3dc096c..89acb7e05d 100644 --- a/lib/src/view/home/home_tab_screen.dart +++ b/lib/src/view/home/home_tab_screen.dart @@ -163,7 +163,7 @@ class _HomeScreenState extends ConsumerState { Future _refreshData() { return Future.wait([ - ref.refresh(accountRecentGamesProvider(10).future), + ref.refresh(accountRecentGamesProvider.future), ref.refresh(ongoingGamesProvider.future), ]); } diff --git a/lib/src/view/user/full_games_screen.dart b/lib/src/view/user/full_games_screen.dart index 1b290d55de..34465d8a47 100644 --- a/lib/src/view/user/full_games_screen.dart +++ b/lib/src/view/user/full_games_screen.dart @@ -1,5 +1,3 @@ -import 'package:collection/collection.dart'; -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -48,12 +46,13 @@ class FullGameScreen extends StatelessWidget { } @riverpod -Future> _userFullGames( +Future _userFullGames( _UserFullGamesRef ref, { required UserId userId, + required int page, }) { return ref.withClientCacheFor( - (client) => GameRepository(client).getRecentGames(userId, -1), + (client) => GameRepository(client).getFullGames(userId, page), // cache is important because the associated widget is in a [ListView] and // the provider may be instanciated multiple times in a short period of time // (e.g. when scrolling) @@ -69,15 +68,25 @@ class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final fullGames = user != null - ? ref.watch(_userFullGamesProvider(userId: user!.id)) - : ref.watch(accountRecentGamesProvider(-1)); - final userId = user?.id ?? ref.watch(authSessionProvider)?.user.id; - - return fullGames.when( + final initialGame = (userId != null + ? ref.watch( + _userFullGamesProvider( + page: 1, + userId: userId, + ), + ) + : ref.watch(accountFullGamesProvider(1))); + return initialGame.when( data: (data) { - return _GameList(gameListData: data, userId: userId); + if (data != null) { + return _GameList( + userId: userId, + initialPage: data, + ); + } else { + return const Text('nothing'); + } }, error: (error, stackTrace) { debugPrint( @@ -93,29 +102,30 @@ class _Body extends ConsumerWidget { } } -class _GameList extends StatefulWidget { - const _GameList({required this.gameListData, this.userId}); - final IList gameListData; +class _GameList extends ConsumerStatefulWidget { + const _GameList({this.userId, required this.initialPage}); final UserId? userId; + final FullGamePaginator initialPage; @override _GameListState createState() => _GameListState(); } -class _GameListState extends State<_GameList> { +class _GameListState extends ConsumerState<_GameList> { ScrollController controller = ScrollController(); - late IList> gameListData; - late List displayGames; + late List games; + late FullGamePaginator page; late UserId? userId; - int count = 0; + bool isLoading = false; @override void initState() { super.initState(); + userId = widget.userId; - gameListData = widget.gameListData.slices(10).toIList(); - displayGames = gameListData[0]; + page = widget.initialPage; + games = page.games.toList(); controller = ScrollController()..addListener(_scrollListener); } @@ -132,33 +142,48 @@ class _GameListState extends State<_GameList> { child: ListView.builder( controller: controller, itemBuilder: (context, index) { - return _SlideMenu( - menuItems: [ - IconButton( - icon: const Icon(Icons.bookmark_add_outlined), - onPressed: () {}, + if (index == games.length) { + return const Center(child: CircularProgressIndicator.adaptive()); + } else { + return _SlideMenu( + menuItems: [ + IconButton( + icon: const Icon(Icons.bookmark_add_outlined), + onPressed: () {}, + ), + ], + child: ExtendedGameListTile( + game: games[index], + userId: userId, ), - ], - child: ExtendedGameListTile( - game: displayGames[index], - userId: userId, - ), - ); + ); + } }, - itemCount: displayGames.length, + itemCount: games.length + 1, ), ), ); } - void _scrollListener() { - if (controller.position.extentAfter < 500) { + Future _scrollListener() async { + if (controller.position.extentAfter < 500 && + page.nextPage != null && + !isLoading) { + isLoading = true; + final nextPage = (userId != null + ? await ref.read( + _userFullGamesProvider( + page: page.nextPage!, + userId: userId!, + ).future, + ) + : await ref.read(accountFullGamesProvider(page.nextPage!).future)); + if (nextPage != null) page = nextPage; + setState(() { - if (count < gameListData.length - 1) { - count++; - displayGames.addAll(gameListData[count]); - } + games.addAll(nextPage?.games ?? []); }); + isLoading = false; } } } diff --git a/lib/src/view/user/recent_games.dart b/lib/src/view/user/recent_games.dart index b9135cfbb2..a2bbedb92a 100644 --- a/lib/src/view/user/recent_games.dart +++ b/lib/src/view/user/recent_games.dart @@ -27,7 +27,7 @@ Future> _userRecentGames( required UserId userId, }) { return ref.withClientCacheFor( - (client) => GameRepository(client).getRecentGames(userId, 10), + (client) => GameRepository(client).getRecentGames(userId), // cache is important because the associated widget is in a [ListView] and // the provider may be instanciated multiple times in a short period of time // (e.g. when scrolling) @@ -46,7 +46,7 @@ class RecentGames extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final recentGames = user != null ? ref.watch(_userRecentGamesProvider(userId: user!.id)) - : ref.watch(accountRecentGamesProvider(10)); + : ref.watch(accountRecentGamesProvider); final userId = user?.id ?? ref.watch(authSessionProvider)?.user.id; diff --git a/test/model/game/game_repository_test.dart b/test/model/game/game_repository_test.dart index 5fe9298650..060b4e6ae8 100644 --- a/test/model/game/game_repository_test.dart +++ b/test/model/game/game_repository_test.dart @@ -26,7 +26,7 @@ void main() { final repo = GameRepository(mockClient); - final result = await repo.getRecentGames(const UserId('testUser'), 10); + final result = await repo.getRecentGames(const UserId('testUser')); expect(result, isA>()); expect(result.length, 3); expect(result[0].id, const GameId('Huk88k3D')); From dc9852aeec165c94be7dcdad0628623c545e9b41 Mon Sep 17 00:00:00 2001 From: ZTL-UwU Date: Sun, 25 Feb 2024 22:04:53 +0800 Subject: [PATCH 05/34] Fix loading indicator behavior --- lib/src/view/user/full_games_screen.dart | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/src/view/user/full_games_screen.dart b/lib/src/view/user/full_games_screen.dart index 34465d8a47..b64c750d36 100644 --- a/lib/src/view/user/full_games_screen.dart +++ b/lib/src/view/user/full_games_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; @@ -143,7 +144,14 @@ class _GameListState extends ConsumerState<_GameList> { controller: controller, itemBuilder: (context, index) { if (index == games.length) { - return const Center(child: CircularProgressIndicator.adaptive()); + if (isLoading || page.currentPage == 1) { + return const Center( + heightFactor: 2.0, + child: CircularProgressIndicator.adaptive(), + ); + } else { + return kEmptyWidget; + } } else { return _SlideMenu( menuItems: [ From 3ed4ffc555eff070a7ffdd9e4e1471d453dc8ebb Mon Sep 17 00:00:00 2001 From: ZTL-UwU Date: Fri, 12 Apr 2024 13:08:45 +0800 Subject: [PATCH 06/34] fix conflicts Signed-off-by: ZTL-UwU --- android/gradle.properties | 4 +++ lib/src/view/game/game_list_tile.dart | 50 +++++++++++++-------------- lib/src/view/user/recent_games.dart | 25 -------------- 3 files changed, 29 insertions(+), 50 deletions(-) diff --git a/android/gradle.properties b/android/gradle.properties index 6fedaefe63..f53666a52c 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -4,3 +4,7 @@ android.enableJetifier=true android.defaults.buildfeatures.buildconfig=true android.nonTransitiveRClass=false android.nonFinalResIds=false +systemProp.http.proxyHost=127.0.0.1 +systemProp.http.proxyPort=7890 +systemProp.https.proxyHost=127.0.0.1 +systemProp.https.proxyPort=7890 \ No newline at end of file diff --git a/lib/src/view/game/game_list_tile.dart b/lib/src/view/game/game_list_tile.dart index ea94b50617..4abbfc9684 100644 --- a/lib/src/view/game/game_list_tile.dart +++ b/lib/src/view/game/game_list_tile.dart @@ -458,37 +458,37 @@ class ExtendedGameListTile extends StatelessWidget { final LightArchivedGame game; final UserId? userId; - Widget getResultIcon(LightArchivedGame game, Side mySide) { - if (game.status == GameStatus.aborted || - game.status == GameStatus.noStart) { - return const Icon( - CupertinoIcons.xmark_square_fill, - color: LichessColors.grey, - ); - } else { - return game.winner == null - ? const Icon( - CupertinoIcons.equal_square_fill, - color: LichessColors.brag, - ) - : game.winner == mySide - ? const Icon( - CupertinoIcons.plus_square_fill, - color: LichessColors.good, - ) - : const Icon( - CupertinoIcons.minus_square_fill, - color: LichessColors.red, - ); - } - } - @override Widget build(BuildContext context) { final mySide = game.white.user?.id == userId ? Side.white : Side.black; final me = game.white.user?.id == userId ? game.white : game.black; final opponent = game.white.user?.id == userId ? game.black : game.white; + Widget getResultIcon(LightArchivedGame game, Side mySide) { + if (game.status == GameStatus.aborted || + game.status == GameStatus.noStart) { + return const Icon( + CupertinoIcons.xmark_square_fill, + color: LichessColors.grey, + ); + } else { + return game.winner == null + ? Icon( + CupertinoIcons.equal_square_fill, + color: context.lichessColors.brag, + ) + : game.winner == mySide + ? Icon( + CupertinoIcons.plus_square_fill, + color: context.lichessColors.good, + ) + : Icon( + CupertinoIcons.minus_square_fill, + color: context.lichessColors.error, + ); + } + } + return GameListTile( game: game, mySide: userId == game.white.user?.id ? Side.white : Side.black, diff --git a/lib/src/view/user/recent_games.dart b/lib/src/view/user/recent_games.dart index 551e16cc85..a2bbedb92a 100644 --- a/lib/src/view/user/recent_games.dart +++ b/lib/src/view/user/recent_games.dart @@ -50,31 +50,6 @@ class RecentGames extends ConsumerWidget { final userId = user?.id ?? ref.watch(authSessionProvider)?.user.id; - Widget getResultIcon(LightArchivedGame game, Side mySide) { - if (game.status == GameStatus.aborted || - game.status == GameStatus.noStart) { - return const Icon( - CupertinoIcons.xmark_square_fill, - color: LichessColors.grey, - ); - } else { - return game.winner == null - ? Icon( - CupertinoIcons.equal_square_fill, - color: context.lichessColors.brag, - ) - : game.winner == mySide - ? Icon( - CupertinoIcons.plus_square_fill, - color: context.lichessColors.good, - ) - : Icon( - CupertinoIcons.minus_square_fill, - color: context.lichessColors.error, - ); - } - } - return recentGames.when( data: (data) { if (data.isEmpty) { From 3dd513cf576aa88e0cee4415d16650bea0a6abee Mon Sep 17 00:00:00 2001 From: ZTL-UwU Date: Fri, 12 Apr 2024 13:09:42 +0800 Subject: [PATCH 07/34] remove gradle.properties proxy Signed-off-by: ZTL-UwU --- android/gradle.properties | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/android/gradle.properties b/android/gradle.properties index f53666a52c..368f02afe1 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -3,8 +3,4 @@ android.useAndroidX=true android.enableJetifier=true android.defaults.buildfeatures.buildconfig=true android.nonTransitiveRClass=false -android.nonFinalResIds=false -systemProp.http.proxyHost=127.0.0.1 -systemProp.http.proxyPort=7890 -systemProp.https.proxyHost=127.0.0.1 -systemProp.https.proxyPort=7890 \ No newline at end of file +android.nonFinalResIds=false \ No newline at end of file From 024af0397c02beb5599a66b1e04374a2a8c2fc08 Mon Sep 17 00:00:00 2001 From: ZTL-UwU Date: Fri, 12 Apr 2024 13:15:58 +0800 Subject: [PATCH 08/34] fix analyze Signed-off-by: ZTL-UwU --- lib/src/view/game/game_list_tile.dart | 2 +- lib/src/view/user/full_games_screen.dart | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/src/view/game/game_list_tile.dart b/lib/src/view/game/game_list_tile.dart index 4abbfc9684..b08b4d1554 100644 --- a/lib/src/view/game/game_list_tile.dart +++ b/lib/src/view/game/game_list_tile.dart @@ -4,8 +4,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; import 'package:lichess_mobile/src/model/game/game_repository.dart'; import 'package:lichess_mobile/src/model/game/game_share_service.dart'; diff --git a/lib/src/view/user/full_games_screen.dart b/lib/src/view/user/full_games_screen.dart index b64c750d36..c4d468356c 100644 --- a/lib/src/view/user/full_games_screen.dart +++ b/lib/src/view/user/full_games_screen.dart @@ -54,11 +54,6 @@ Future _userFullGames( }) { return ref.withClientCacheFor( (client) => GameRepository(client).getFullGames(userId, page), - // cache is important because the associated widget is in a [ListView] and - // the provider may be instanciated multiple times in a short period of time - // (e.g. when scrolling) - // TODO: consider debouncing the request instead of caching it, or make the - // request in the parent widget and pass the result to the child const Duration(minutes: 1), ); } From 1ff450d6372866ac449d80b4a619a7a0b46a9412 Mon Sep 17 00:00:00 2001 From: ZTL-UwU Date: Mon, 15 Apr 2024 12:46:49 +0800 Subject: [PATCH 09/34] use flutter_slidable use default ios navigation bar previousPage remove wrong comment --- lib/src/view/user/full_games_screen.dart | 107 +++-------------------- 1 file changed, 13 insertions(+), 94 deletions(-) diff --git a/lib/src/view/user/full_games_screen.dart b/lib/src/view/user/full_games_screen.dart index c4d468356c..d7496d158f 100644 --- a/lib/src/view/user/full_games_screen.dart +++ b/lib/src/view/user/full_games_screen.dart @@ -1,6 +1,8 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; @@ -16,7 +18,6 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'full_games_screen.g.dart'; -/// Create a Screen with Top 10 players for each Lichess Variant class FullGameScreen extends StatelessWidget { const FullGameScreen({this.user, super.key}); final LightUser? user; @@ -29,7 +30,6 @@ class FullGameScreen extends StatelessWidget { Widget _buildIos(BuildContext context) { return CupertinoPageScaffold( navigationBar: const CupertinoNavigationBar( - previousPageTitle: 'Home', middle: Text('Full Game History'), ), child: _Body(user: user), @@ -148,13 +148,17 @@ class _GameListState extends ConsumerState<_GameList> { return kEmptyWidget; } } else { - return _SlideMenu( - menuItems: [ - IconButton( - icon: const Icon(Icons.bookmark_add_outlined), - onPressed: () {}, - ), - ], + return Slidable( + endActionPane: const ActionPane( + motion: ScrollMotion(), + children: [ + SlidableAction( + onPressed: null, + icon: Icons.bookmark_add_outlined, + label: 'Bookmark', + ), + ], + ), child: ExtendedGameListTile( game: games[index], userId: userId, @@ -190,88 +194,3 @@ class _GameListState extends ConsumerState<_GameList> { } } } - -class _SlideMenu extends StatefulWidget { - const _SlideMenu({required this.child, required this.menuItems}); - final Widget child; - final List menuItems; - - @override - _SlideMenuState createState() => _SlideMenuState(); -} - -class _SlideMenuState extends State<_SlideMenu> - with SingleTickerProviderStateMixin { - late AnimationController _controller; - - @override - void initState() { - super.initState(); - _controller = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 200), - ); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final animation = Tween(begin: Offset.zero, end: const Offset(-0.15, 0.0)) - .animate(CurveTween(curve: Curves.decelerate).animate(_controller)); - - return GestureDetector( - onHorizontalDragUpdate: (data) { - setState(() { - _controller.value -= data.primaryDelta! / context.size!.width * 1.5; - }); - }, - onHorizontalDragEnd: (data) { - if (data.primaryVelocity! > 1500) { - _controller.animateTo(0); - } else if (_controller.value >= 0.3 || data.primaryVelocity! < -1500) { - _controller.animateTo(1.0); - } else { - _controller.animateTo(0); - } - }, - child: Stack( - children: [ - SlideTransition(position: animation, child: widget.child), - Positioned.fill( - child: LayoutBuilder( - builder: (context, constraint) { - return AnimatedBuilder( - animation: _controller, - builder: (context, child) { - return Stack( - children: [ - Positioned( - right: 0.1, - top: 0, - bottom: 0, - width: constraint.maxWidth * animation.value.dx * -1, - child: Row( - children: widget.menuItems.map((child) { - return Expanded( - child: child, - ); - }).toList(), - ), - ), - ], - ); - }, - ); - }, - ), - ), - ], - ), - ); - } -} From 1887dbc96d51049b887f61efae60d385a2bcc5e3 Mon Sep 17 00:00:00 2001 From: ZTL-UwU Date: Mon, 15 Apr 2024 12:52:29 +0800 Subject: [PATCH 10/34] remove unnecessary import Signed-off-by: ZTL-UwU --- lib/src/view/user/full_games_screen.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/view/user/full_games_screen.dart b/lib/src/view/user/full_games_screen.dart index d7496d158f..af0d22ecc8 100644 --- a/lib/src/view/user/full_games_screen.dart +++ b/lib/src/view/user/full_games_screen.dart @@ -1,6 +1,5 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:lichess_mobile/src/constants.dart'; From 3b02bf2273f1ab2a43ad09b12e53f7c02c8b702a Mon Sep 17 00:00:00 2001 From: ZTL-UwU Date: Thu, 16 May 2024 17:58:01 +0800 Subject: [PATCH 11/34] Refactor game histroy Signed-off-by: ZTL-UwU --- lib/src/model/account/account_repository.dart | 13 -- lib/src/model/game/archived_game.dart | 41 +--- lib/src/model/game/game_history.dart | 85 +++++++ lib/src/model/game/game_repository.dart | 25 +- .../model/game/game_repository_providers.dart | 16 ++ lib/src/view/user/full_games_screen.dart | 213 ++++++------------ lib/src/view/user/recent_games.dart | 52 ++--- 7 files changed, 212 insertions(+), 233 deletions(-) create mode 100644 lib/src/model/game/game_history.dart diff --git a/lib/src/model/account/account_repository.dart b/lib/src/model/account/account_repository.dart index 1c7d01b3f2..88d159fc97 100644 --- a/lib/src/model/account/account_repository.dart +++ b/lib/src/model/account/account_repository.dart @@ -57,19 +57,6 @@ Future> accountRecentGames( ); } -@riverpod -Future accountFullGames( - AccountFullGamesRef ref, - int page, -) async { - final session = ref.watch(authSessionProvider); - if (session == null) return null; - return ref.withClientCacheFor( - (client) => GameRepository(client).getFullGames(session.user.id, page), - const Duration(hours: 1), - ); -} - @riverpod Future> ongoingGames(OngoingGamesRef ref) async { final session = ref.watch(authSessionProvider); diff --git a/lib/src/model/game/archived_game.dart b/lib/src/model/game/archived_game.dart index 37d2df3f4d..4e01bdf8c8 100644 --- a/lib/src/model/game/archived_game.dart +++ b/lib/src/model/game/archived_game.dart @@ -132,25 +132,6 @@ class LightArchivedGame with _$LightArchivedGame { } } -@freezed -class FullGamePaginator with _$FullGamePaginator { - const FullGamePaginator._(); - - const factory FullGamePaginator({ - int? currentPage, - int? maxPerPage, - int? previousPage, - int? nextPage, - int? nbResults, - int? nbPages, - required IList games, - }) = _FullGamePaginator; - - factory FullGamePaginator.fromServerJson(Map json) { - return _fullGamePaginatorFromPick(pick(json, 'paginator').required()); - } -} - IList? gameEvalsFromPick(RequiredPick pick) { return pick('analysis') .asListOrNull( @@ -246,36 +227,20 @@ LightArchivedGame _lightArchivedGameFromPick(RequiredPick pick) { rated: pick('rated').asBoolOrThrow(), speed: pick('speed').asSpeedOrThrow(), perf: pick('perf').asPerfOrThrow(), - createdAt: pick('timestamp').asDateTimeFromMillisecondsOrNull() ?? - pick('createdAt').asDateTimeFromMillisecondsOrThrow(), - lastMoveAt: pick('timestamp').asDateTimeFromMillisecondsOrNull() ?? - pick('lastMoveAt').asDateTimeFromMillisecondsOrThrow(), + createdAt: pick('createdAt').asDateTimeFromMillisecondsOrThrow(), + lastMoveAt: pick('lastMoveAt').asDateTimeFromMillisecondsOrThrow(), status: pick('status').asGameStatusOrThrow(), white: pick('players', 'white').letOrThrow(_playerFromUserGamePick), black: pick('players', 'black').letOrThrow(_playerFromUserGamePick), winner: pick('winner').asSideOrNull(), variant: pick('variant').asVariantOrThrow(), - lastFen: pick('lastFen').asStringOrNull() ?? pick('fen').asStringOrNull(), + lastFen: pick('lastFen').asStringOrNull(), lastMove: pick('lastMove').asUciMoveOrNull(), clock: pick('clock').letOrNull(_clockDataFromPick), opening: pick('opening').letOrNull(_openingFromPick), ); } -FullGamePaginator _fullGamePaginatorFromPick(RequiredPick pick) { - return FullGamePaginator( - currentPage: pick('currentPage').asIntOrNull(), - maxPerPage: pick('maxPerPage').asIntOrNull(), - previousPage: pick('previousPage').asIntOrNull(), - nextPage: pick('nextPage').asIntOrNull(), - nbResults: pick('nbResults').asIntOrNull(), - nbPages: pick('nbPages').asIntOrNull(), - games: IList( - pick('currentPageResults').asListOrThrow(_lightArchivedGameFromPick), - ), - ); -} - LightOpening _openingFromPick(RequiredPick pick) { return LightOpening( eco: pick('eco').asStringOrThrow(), diff --git a/lib/src/model/game/game_history.dart b/lib/src/model/game/game_history.dart new file mode 100644 index 0000000000..33adb4775d --- /dev/null +++ b/lib/src/model/game/game_history.dart @@ -0,0 +1,85 @@ +import 'dart:async'; + +import 'package:async/async.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/game/archived_game.dart'; +import 'package:lichess_mobile/src/model/game/game_repository.dart'; +import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; +import 'package:lichess_mobile/src/utils/riverpod.dart'; +import 'package:result_extensions/result_extensions.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'game_history.freezed.dart'; +part 'game_history.g.dart'; + +const _nbPerPage = 20; + +@riverpod +class UserGameHistory extends _$UserGameHistory { + final _list = []; + + @override + Future build(UserId id) async { + ref.cacheFor(const Duration(minutes: 30)); + ref.onDispose(() { + _list.clear(); + }); + final recentGames = + await ref.watch(userRecentGamesProvider(userId: id).future); + _list.addAll(recentGames); + return UserGameHistoryState( + gameList: _list.toIList(), + isLoading: false, + hasMore: true, + hasError: false, + ); + } + + void getNext() { + if (!state.hasValue) return; + + final currentVal = state.requireValue; + state = AsyncData(currentVal.copyWith(isLoading: true)); + Result.capture( + ref.withClient( + (client) => GameRepository(client) + .getFullGames(id, _nbPerPage, until: _list.last.createdAt), + ), + ).fold( + (value) { + if (value.isEmpty) { + state = AsyncData( + currentVal.copyWith(hasMore: false, isLoading: false), + ); + return; + } + _list.addAll(value); + state = AsyncData( + UserGameHistoryState( + gameList: _list.toIList(), + isLoading: false, + hasMore: true, + hasError: false, + ), + ); + }, + (error, stackTrace) { + state = + AsyncData(currentVal.copyWith(isLoading: false, hasError: true)); + }, + ); + } +} + +@freezed +class UserGameHistoryState with _$UserGameHistoryState { + const factory UserGameHistoryState({ + required IList gameList, + required bool isLoading, + required bool hasMore, + required bool hasError, + }) = _UserGameHistoryState; +} diff --git a/lib/src/model/game/game_repository.dart b/lib/src/model/game/game_repository.dart index d02b4e93ff..8543b3323a 100644 --- a/lib/src/model/game/game_repository.dart +++ b/lib/src/model/game/game_repository.dart @@ -51,14 +51,25 @@ class GameRepository { ); } - Future getFullGames( + Future> getFullGames( UserId userId, - int page, - ) { - return client.readJson( - Uri.parse('$kLichessHost/@/$userId/all?page=$page'), - headers: {'Accept': 'application/json'}, - mapper: FullGamePaginator.fromServerJson, + int max, { + DateTime? until, + }) { + return client.readNdJsonList( + Uri( + path: '/api/games/user/$userId', + queryParameters: { + 'max': max.toString(), + if (until != null) 'until': until.millisecondsSinceEpoch.toString(), + 'moves': 'false', + 'lastFen': 'true', + 'accuracy': 'true', + 'opening': 'true', + }, + ), + headers: {'Accept': 'application/x-ndjson'}, + mapper: LightArchivedGame.fromServerJson, ); } diff --git a/lib/src/model/game/game_repository_providers.dart b/lib/src/model/game/game_repository_providers.dart index dded6e0df1..58b99c187a 100644 --- a/lib/src/model/game/game_repository_providers.dart +++ b/lib/src/model/game/game_repository_providers.dart @@ -24,6 +24,22 @@ Future archivedGame( ); } +@riverpod +Future> userRecentGames( + UserRecentGamesRef ref, { + required UserId userId, +}) { + return ref.withClientCacheFor( + (client) => GameRepository(client).getRecentGames(userId), + // cache is important because the associated widget is in a [ListView] and + // the provider may be instanciated multiple times in a short period of time + // (e.g. when scrolling) + // TODO: consider debouncing the request instead of caching it, or make the + // request in the parent widget and pass the result to the child + const Duration(minutes: 1), + ); +} + @riverpod Future> gamesById( GamesByIdRef ref, { diff --git a/lib/src/view/user/full_games_screen.dart b/lib/src/view/user/full_games_screen.dart index af0d22ecc8..35d08e6791 100644 --- a/lib/src/view/user/full_games_screen.dart +++ b/lib/src/view/user/full_games_screen.dart @@ -2,24 +2,16 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; -import 'package:lichess_mobile/src/constants.dart'; -import 'package:lichess_mobile/src/model/account/account_repository.dart'; -import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/game/archived_game.dart'; -import 'package:lichess_mobile/src/model/game/game_repository.dart'; +import 'package:lichess_mobile/src/model/game/game_history.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; -import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/view/game/game_list_tile.dart'; +import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'full_games_screen.g.dart'; class FullGameScreen extends StatelessWidget { - const FullGameScreen({this.user, super.key}); - final LightUser? user; + const FullGameScreen({required this.user, super.key}); + final LightUser user; @override Widget build(BuildContext context) { @@ -31,7 +23,7 @@ class FullGameScreen extends StatelessWidget { navigationBar: const CupertinoNavigationBar( middle: Text('Full Game History'), ), - child: _Body(user: user), + child: _Body(userId: user.id), ); } @@ -40,156 +32,99 @@ class FullGameScreen extends StatelessWidget { appBar: AppBar( title: const Text('Full Game History'), ), - body: _Body(user: user), + body: _Body(userId: user.id), ); } } -@riverpod -Future _userFullGames( - _UserFullGamesRef ref, { - required UserId userId, - required int page, -}) { - return ref.withClientCacheFor( - (client) => GameRepository(client).getFullGames(userId, page), - const Duration(minutes: 1), - ); -} - -class _Body extends ConsumerWidget { - const _Body({this.user}); - final LightUser? user; - +class _Body extends ConsumerStatefulWidget { + const _Body({required this.userId}); + final UserId userId; @override - Widget build(BuildContext context, WidgetRef ref) { - final userId = user?.id ?? ref.watch(authSessionProvider)?.user.id; - final initialGame = (userId != null - ? ref.watch( - _userFullGamesProvider( - page: 1, - userId: userId, - ), - ) - : ref.watch(accountFullGamesProvider(1))); - return initialGame.when( - data: (data) { - if (data != null) { - return _GameList( - userId: userId, - initialPage: data, - ); - } else { - return const Text('nothing'); - } - }, - error: (error, stackTrace) { - debugPrint( - 'SEVERE: [FullGames] could not load game history; $error\n$stackTrace', - ); - return Padding( - padding: Styles.bodySectionPadding, - child: const Text('Could not load game history.'), - ); - }, - loading: () => const Center(child: CircularProgressIndicator.adaptive()), - ); - } + ConsumerState<_Body> createState() => _BodyState(); } -class _GameList extends ConsumerStatefulWidget { - const _GameList({this.userId, required this.initialPage}); - final UserId? userId; - final FullGamePaginator initialPage; - - @override - _GameListState createState() => _GameListState(); -} - -class _GameListState extends ConsumerState<_GameList> { - ScrollController controller = ScrollController(); - - late List games; - late FullGamePaginator page; - late UserId? userId; - bool isLoading = false; +class _BodyState extends ConsumerState<_Body> { + final ScrollController _scrollController = ScrollController(); + bool _hasMore = true; + bool _isLoading = false; @override void initState() { super.initState(); - - userId = widget.userId; - page = widget.initialPage; - games = page.games.toList(); - controller = ScrollController()..addListener(_scrollListener); + _scrollController.addListener(_scrollListener); } @override void dispose() { - controller.removeListener(_scrollListener); + _scrollController.removeListener(_scrollListener); + _scrollController.dispose(); super.dispose(); } + void _scrollListener() { + if (_scrollController.position.pixels == + _scrollController.position.maxScrollExtent) { + if (_hasMore && !_isLoading) { + ref.read(userGameHistoryProvider(widget.userId).notifier).getNext(); + } + } + } + @override Widget build(BuildContext context) { - return Scaffold( - body: Scrollbar( - child: ListView.builder( - controller: controller, + final gameListState = ref.watch(userGameHistoryProvider(widget.userId)); + + return gameListState.when( + data: (state) { + _hasMore = state.hasMore; + _isLoading = state.isLoading; + if (state.hasError) { + showPlatformSnackbar( + context, + 'Error loading Game History', + type: SnackBarType.error, + ); + } + + final list = state.gameList; + return ListView.builder( + controller: _scrollController, + itemCount: list.length + (state.isLoading ? 1 : 0), itemBuilder: (context, index) { - if (index == games.length) { - if (isLoading || page.currentPage == 1) { - return const Center( - heightFactor: 2.0, - child: CircularProgressIndicator.adaptive(), - ); - } else { - return kEmptyWidget; - } - } else { - return Slidable( - endActionPane: const ActionPane( - motion: ScrollMotion(), - children: [ - SlidableAction( - onPressed: null, - icon: Icons.bookmark_add_outlined, - label: 'Bookmark', - ), - ], - ), - child: ExtendedGameListTile( - game: games[index], - userId: userId, - ), + if (state.isLoading && index == list.length) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 32.0), + child: CenterLoadingIndicator(), ); } + + return Slidable( + endActionPane: const ActionPane( + motion: ScrollMotion(), + children: [ + SlidableAction( + onPressed: null, + icon: Icons.bookmark_add_outlined, + label: 'Bookmark', + ), + ], + ), + child: ExtendedGameListTile( + game: list[index], + userId: widget.userId, + ), + ); }, - itemCount: games.length + 1, - ), - ), + ); + }, + error: (e, s) { + debugPrint( + 'SEVERE: [FullGameHistoryScreen] could not load game list', + ); + return const Center(child: Text('Could not load Game History')); + }, + loading: () => const CenterLoadingIndicator(), ); } - - Future _scrollListener() async { - if (controller.position.extentAfter < 500 && - page.nextPage != null && - !isLoading) { - isLoading = true; - final nextPage = (userId != null - ? await ref.read( - _userFullGamesProvider( - page: page.nextPage!, - userId: userId!, - ).future, - ) - : await ref.read(accountFullGamesProvider(page.nextPage!).future)); - if (nextPage != null) page = nextPage; - - setState(() { - games.addAll(nextPage?.games ?? []); - }); - isLoading = false; - } - } } diff --git a/lib/src/view/user/recent_games.dart b/lib/src/view/user/recent_games.dart index 19b926aaad..4297daf914 100644 --- a/lib/src/view/user/recent_games.dart +++ b/lib/src/view/user/recent_games.dart @@ -3,11 +3,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/game/archived_game.dart'; -import 'package:lichess_mobile/src/model/game/game_repository.dart'; -import 'package:lichess_mobile/src/model/game/game_status.dart'; +import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; import 'package:lichess_mobile/src/model/game/game_storage.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; @@ -19,25 +15,6 @@ import 'package:lichess_mobile/src/view/user/full_games_screen.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'recent_games.g.dart'; - -@riverpod -Future> _userRecentGames( - _UserRecentGamesRef ref, { - required UserId userId, -}) { - return ref.withClientCacheFor( - (client) => GameRepository(client).getRecentGames(userId), - // cache is important because the associated widget is in a [ListView] and - // the provider may be instanciated multiple times in a short period of time - // (e.g. when scrolling) - // TODO: consider debouncing the request instead of caching it, or make the - // request in the parent widget and pass the result to the child - const Duration(minutes: 1), - ); -} class RecentGames extends ConsumerWidget { const RecentGames({this.user, super.key}); @@ -51,7 +28,7 @@ class RecentGames extends ConsumerWidget { final userId = user?.id ?? session?.user.id; final recentGames = user != null - ? ref.watch(_userRecentGamesProvider(userId: user!.id)) + ? ref.watch(userRecentGamesProvider(userId: user!.id)) : session != null && (isOnline.valueOrNull ?? false) == true ? ref.watch(accountRecentGamesProvider) : ref.watch(recentStoredGamesProvider).whenData((data) { @@ -63,20 +40,23 @@ class RecentGames extends ConsumerWidget { if (data.isEmpty) { return const SizedBox.shrink(); } + final u = user ?? ref.watch(authSessionProvider)?.user; return ListSection( header: Text(context.l10n.recentGames), hasLeading: true, - headerTrailing: NoPaddingTextButton( - onPressed: () { - pushPlatformRoute( - context, - builder: (context) => FullGameScreen(user: user), - ); - }, - child: Text( - context.l10n.more, - ), - ), + headerTrailing: u != null + ? NoPaddingTextButton( + onPressed: () { + pushPlatformRoute( + context, + builder: (context) => FullGameScreen(user: u), + ); + }, + child: Text( + context.l10n.more, + ), + ) + : null, children: data.map((game) { return ExtendedGameListTile(game: game, userId: userId); }).toList(), From 1e03c9b746e8348ed0d2b609a8207581f9cee192 Mon Sep 17 00:00:00 2001 From: Tim McCabe Date: Mon, 27 May 2024 16:46:21 -0400 Subject: [PATCH 12/34] Clear engine suggestion arrows after reaching checkmate or stalemate --- lib/src/model/common/eval.dart | 5 ++++- lib/src/model/engine/uci_protocol.dart | 12 +++--------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/lib/src/model/common/eval.dart b/lib/src/model/common/eval.dart index ae15124fdf..7341e384d3 100644 --- a/lib/src/model/common/eval.dart +++ b/lib/src/model/common/eval.dart @@ -72,7 +72,10 @@ class ClientEval with _$ClientEval implements Eval { } IList get bestMoves { - return pvs.map((e) => Move.fromUci(e.moves.first)).toIList(); + return pvs + .where((e) => e.moves.isNotEmpty) + .map((e) => Move.fromUci(e.moves.first)) + .toIList(); } @override diff --git a/lib/src/model/engine/uci_protocol.dart b/lib/src/model/engine/uci_protocol.dart index 4292f8a077..549867eeb4 100644 --- a/lib/src/model/engine/uci_protocol.dart +++ b/lib/src/model/engine/uci_protocol.dart @@ -99,9 +99,9 @@ class UCIProtocol { _stopRequested != true && parts.first == 'info') { int depth = 0; - int? nodes; + int nodes = 0; int multiPv = 1; - int? elapsedMs; + int elapsedMs = 0; String? evalType; bool isMate = false; int? povEv; @@ -130,16 +130,10 @@ class UCIProtocol { } } - // Sometimes we get #0. Let's just skip it. - if (isMate && povEv == 0) return; - // Track max pv index to determine when pv prints are done. if (_expectedPvs < multiPv) _expectedPvs = multiPv; - if (depth < minDepth || - nodes == null || - elapsedMs == null || - povEv == null) return; + if ((depth < minDepth && moves.isNotEmpty) || povEv == null) return; final pivot = _work!.threatMode == true ? 0 : 1; final ev = _work!.ply % 2 == pivot ? -povEv : povEv; From 4348ef80a574f02c09d0060480d121ea7d71e52f Mon Sep 17 00:00:00 2001 From: ZTL-UwU Date: Tue, 28 May 2024 13:53:01 +0800 Subject: [PATCH 13/34] Add side for game history & add comment for ExtendedGameListTile & fix typo Signed-off-by: ZTL-UwU --- android/gradle.properties | 2 +- lib/src/model/game/game_history.dart | 12 ++++++---- lib/src/view/game/game_list_tile.dart | 29 ++++++++++++------------ lib/src/view/user/full_games_screen.dart | 2 +- lib/src/view/user/recent_games.dart | 29 +++++++++++++++++++++--- 5 files changed, 49 insertions(+), 25 deletions(-) diff --git a/android/gradle.properties b/android/gradle.properties index 368f02afe1..6fedaefe63 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -3,4 +3,4 @@ android.useAndroidX=true android.enableJetifier=true android.defaults.buildfeatures.buildconfig=true android.nonTransitiveRClass=false -android.nonFinalResIds=false \ No newline at end of file +android.nonFinalResIds=false diff --git a/lib/src/model/game/game_history.dart b/lib/src/model/game/game_history.dart index 33adb4775d..96ab9f25a7 100644 --- a/lib/src/model/game/game_history.dart +++ b/lib/src/model/game/game_history.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:async/async.dart'; +import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; @@ -19,7 +20,7 @@ const _nbPerPage = 20; @riverpod class UserGameHistory extends _$UserGameHistory { - final _list = []; + final _list = <(LightArchivedGame, Side)>[]; @override Future build(UserId id) async { @@ -29,7 +30,8 @@ class UserGameHistory extends _$UserGameHistory { }); final recentGames = await ref.watch(userRecentGamesProvider(userId: id).future); - _list.addAll(recentGames); + _list.addAll(recentGames + .map((e) => (e, e.white.user?.id == id ? Side.white : Side.black)),); return UserGameHistoryState( gameList: _list.toIList(), isLoading: false, @@ -46,7 +48,7 @@ class UserGameHistory extends _$UserGameHistory { Result.capture( ref.withClient( (client) => GameRepository(client) - .getFullGames(id, _nbPerPage, until: _list.last.createdAt), + .getFullGames(id, _nbPerPage, until: _list.last.$1.createdAt), ), ).fold( (value) { @@ -56,7 +58,7 @@ class UserGameHistory extends _$UserGameHistory { ); return; } - _list.addAll(value); + _list.addAll(value.map((e) => (e, e.white.user?.id == id ? Side.white : Side.black))); state = AsyncData( UserGameHistoryState( gameList: _list.toIList(), @@ -77,7 +79,7 @@ class UserGameHistory extends _$UserGameHistory { @freezed class UserGameHistoryState with _$UserGameHistoryState { const factory UserGameHistoryState({ - required IList gameList, + required IList<(LightArchivedGame, Side)> gameList, required bool isLoading, required bool hasMore, required bool hasError, diff --git a/lib/src/view/game/game_list_tile.dart b/lib/src/view/game/game_list_tile.dart index 99a22d72c4..989849583a 100644 --- a/lib/src/view/game/game_list_tile.dart +++ b/lib/src/view/game/game_list_tile.dart @@ -30,7 +30,7 @@ class GameListTile extends StatelessWidget { const GameListTile({ required this.game, required this.mySide, - required this.oppponentTitle, + required this.opponentTitle, this.icon, this.subtitle, this.trailing, @@ -41,7 +41,7 @@ class GameListTile extends StatelessWidget { final Side mySide; final IconData? icon; - final Widget oppponentTitle; + final Widget opponentTitle; final Widget? subtitle; final Widget? trailing; final GestureTapCallback? onTap; @@ -60,7 +60,7 @@ class GameListTile extends StatelessWidget { builder: (context) => _ContextMenu( game: game, mySide: mySide, - oppponentTitle: oppponentTitle, + oppponentTitle: opponentTitle, icon: icon, subtitle: subtitle, trailing: trailing, @@ -68,7 +68,7 @@ class GameListTile extends StatelessWidget { ); }, leading: icon != null ? Icon(icon) : null, - title: oppponentTitle, + title: opponentTitle, subtitle: subtitle != null ? DefaultTextStyle.merge( child: subtitle!, @@ -410,17 +410,18 @@ class _ContextMenu extends ConsumerWidget { } } +/// A list tile that shows extended game info including an accuracy meter and a result icon. class ExtendedGameListTile extends StatelessWidget { - const ExtendedGameListTile({required this.game, this.userId}); + const ExtendedGameListTile({required this.item, this.userId}); - final LightArchivedGame game; + final (LightArchivedGame, Side) item; final UserId? userId; @override Widget build(BuildContext context) { - final mySide = game.white.user?.id == userId ? Side.white : Side.black; - final me = game.white.user?.id == userId ? game.white : game.black; - final opponent = game.white.user?.id == userId ? game.black : game.white; + final (game, youAre) = item; + final me = youAre == Side.white ? game.white : game.black; + final opponent = youAre == Side.white ? game.black : game.white; Widget getResultIcon(LightArchivedGame game, Side mySide) { if (game.status == GameStatus.aborted || @@ -449,7 +450,7 @@ class ExtendedGameListTile extends StatelessWidget { return GameListTile( game: game, - mySide: userId == game.white.user?.id ? Side.white : Side.black, + mySide: youAre, onTap: game.variant.isSupported ? () { pushPlatformRoute( @@ -463,15 +464,13 @@ class ExtendedGameListTile extends StatelessWidget { ) : ArchivedGameScreen( gameData: game, - orientation: userId == game.white.user?.id - ? Side.white - : Side.black, + orientation: youAre, ), ); } : null, icon: game.perf.icon, - playerTitle: UserFullNameWidget.player( + opponentTitle: UserFullNameWidget.player( user: opponent.user, aiLevel: opponent.aiLevel, rating: opponent.rating, @@ -501,7 +500,7 @@ class ExtendedGameListTile extends StatelessWidget { ), const SizedBox(width: 5), ], - getResultIcon(game, mySide), + getResultIcon(game, youAre), ], ), ); diff --git a/lib/src/view/user/full_games_screen.dart b/lib/src/view/user/full_games_screen.dart index 35d08e6791..2b1842770c 100644 --- a/lib/src/view/user/full_games_screen.dart +++ b/lib/src/view/user/full_games_screen.dart @@ -111,7 +111,7 @@ class _BodyState extends ConsumerState<_Body> { ], ), child: ExtendedGameListTile( - game: list[index], + item: list[index], userId: widget.userId, ), ); diff --git a/lib/src/view/user/recent_games.dart b/lib/src/view/user/recent_games.dart index 5acf398df7..d6980798be 100644 --- a/lib/src/view/user/recent_games.dart +++ b/lib/src/view/user/recent_games.dart @@ -1,9 +1,13 @@ +import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; +import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/game/archived_game.dart'; +import 'package:lichess_mobile/src/model/game/game_repository.dart'; import 'package:lichess_mobile/src/model/game/game_storage.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; @@ -15,6 +19,25 @@ import 'package:lichess_mobile/src/view/user/full_games_screen.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'recent_games.g.dart'; + +@riverpod +Future> _userRecentGames( + _UserRecentGamesRef ref, { + required UserId userId, +}) { + return ref.withClientCacheFor( + (client) => GameRepository(client).getRecentGames(userId), + // cache is important because the associated widget is in a [ListView] and + // the provider may be instanciated multiple times in a short period of time + // (e.g. when scrolling) + // TODO: consider debouncing the request instead of caching it, or make the + // request in the parent widget and pass the result to the child + const Duration(minutes: 1), + ); +} class RecentGames extends ConsumerWidget { const RecentGames({this.user, super.key}); @@ -82,8 +105,8 @@ class RecentGames extends ConsumerWidget { ), ) : null, - children: data.map((game) { - return ExtendedGameListTile(game: game, userId: userId); + children: data.map((item) { + return ExtendedGameListTile(item: item, userId: userId); }).toList(), ); }, From ad69b563866f72b57e9e9ba10efc3e7623cfbac7 Mon Sep 17 00:00:00 2001 From: ZTL-UwU Date: Tue, 28 May 2024 17:24:04 +0800 Subject: [PATCH 14/34] Replace getRecentGame with getUserGames Signed-off-by: ZTL-UwU --- lib/src/model/account/account_repository.dart | 2 +- lib/src/model/game/game_history.dart | 13 +++++++---- lib/src/model/game/game_repository.dart | 23 +++---------------- .../model/game/game_repository_providers.dart | 2 +- lib/src/view/user/recent_games.dart | 2 +- test/model/game/game_repository_test.dart | 2 +- 6 files changed, 16 insertions(+), 28 deletions(-) diff --git a/lib/src/model/account/account_repository.dart b/lib/src/model/account/account_repository.dart index dfe0c66f03..33e86669ae 100644 --- a/lib/src/model/account/account_repository.dart +++ b/lib/src/model/account/account_repository.dart @@ -52,7 +52,7 @@ Future> accountRecentGames( final session = ref.watch(authSessionProvider); if (session == null) return IList(); return ref.withClientCacheFor( - (client) => GameRepository(client).getRecentGames(session.user.id), + (client) => GameRepository(client).getUserGames(session.user.id), const Duration(hours: 1), ); } diff --git a/lib/src/model/game/game_history.dart b/lib/src/model/game/game_history.dart index 96ab9f25a7..be2cbdb8cb 100644 --- a/lib/src/model/game/game_history.dart +++ b/lib/src/model/game/game_history.dart @@ -30,8 +30,10 @@ class UserGameHistory extends _$UserGameHistory { }); final recentGames = await ref.watch(userRecentGamesProvider(userId: id).future); - _list.addAll(recentGames - .map((e) => (e, e.white.user?.id == id ? Side.white : Side.black)),); + _list.addAll( + recentGames + .map((e) => (e, e.white.user?.id == id ? Side.white : Side.black)), + ); return UserGameHistoryState( gameList: _list.toIList(), isLoading: false, @@ -48,7 +50,7 @@ class UserGameHistory extends _$UserGameHistory { Result.capture( ref.withClient( (client) => GameRepository(client) - .getFullGames(id, _nbPerPage, until: _list.last.$1.createdAt), + .getUserGames(id, max: _nbPerPage, until: _list.last.$1.createdAt), ), ).fold( (value) { @@ -58,7 +60,10 @@ class UserGameHistory extends _$UserGameHistory { ); return; } - _list.addAll(value.map((e) => (e, e.white.user?.id == id ? Side.white : Side.black))); + _list.addAll( + value.map( + (e) => (e, e.white.user?.id == id ? Side.white : Side.black)), + ); state = AsyncData( UserGameHistoryState( gameList: _list.toIList(), diff --git a/lib/src/model/game/game_repository.dart b/lib/src/model/game/game_repository.dart index b95e492965..865b81aea8 100644 --- a/lib/src/model/game/game_repository.dart +++ b/lib/src/model/game/game_repository.dart @@ -34,26 +34,9 @@ class GameRepository { } } - Future> getRecentGames(UserId userId) { - return client.readNdJsonList( - Uri( - path: '/api/games/user/$userId', - queryParameters: { - 'max': '20', - 'moves': 'false', - 'lastFen': 'true', - 'accuracy': 'true', - 'opening': 'true', - }, - ), - headers: {'Accept': 'application/x-ndjson'}, - mapper: LightArchivedGame.fromServerJson, - ); - } - - Future> getFullGames( - UserId userId, - int max, { + Future> getUserGames( + UserId userId, { + int? max = 20, DateTime? until, }) { return client.readNdJsonList( diff --git a/lib/src/model/game/game_repository_providers.dart b/lib/src/model/game/game_repository_providers.dart index 58b99c187a..19f83803ee 100644 --- a/lib/src/model/game/game_repository_providers.dart +++ b/lib/src/model/game/game_repository_providers.dart @@ -30,7 +30,7 @@ Future> userRecentGames( required UserId userId, }) { return ref.withClientCacheFor( - (client) => GameRepository(client).getRecentGames(userId), + (client) => GameRepository(client).getUserGames(userId), // cache is important because the associated widget is in a [ListView] and // the provider may be instanciated multiple times in a short period of time // (e.g. when scrolling) diff --git a/lib/src/view/user/recent_games.dart b/lib/src/view/user/recent_games.dart index d6980798be..92a685907e 100644 --- a/lib/src/view/user/recent_games.dart +++ b/lib/src/view/user/recent_games.dart @@ -29,7 +29,7 @@ Future> _userRecentGames( required UserId userId, }) { return ref.withClientCacheFor( - (client) => GameRepository(client).getRecentGames(userId), + (client) => GameRepository(client).getUserGames(userId), // cache is important because the associated widget is in a [ListView] and // the provider may be instanciated multiple times in a short period of time // (e.g. when scrolling) diff --git a/test/model/game/game_repository_test.dart b/test/model/game/game_repository_test.dart index a5b1633c54..306e6270ce 100644 --- a/test/model/game/game_repository_test.dart +++ b/test/model/game/game_repository_test.dart @@ -26,7 +26,7 @@ void main() { final repo = GameRepository(mockClient); - final result = await repo.getRecentGames(const UserId('testUser')); + final result = await repo.getUserGames(const UserId('testUser')); expect(result, isA>()); expect(result.length, 3); expect(result[0].id, const GameId('Huk88k3D')); From b66e02fb58d489651e2a7a74405f0f1295293c36 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 28 May 2024 11:24:25 +0200 Subject: [PATCH 15/34] Add more missing context.mounted checks --- lib/src/view/analysis/analysis_screen.dart | 12 +++--- .../view/play/create_custom_game_screen.dart | 24 ++++++------ lib/src/view/puzzle/puzzle_screen.dart | 11 +++--- lib/src/view/puzzle/storm_screen.dart | 4 +- lib/src/view/user/perf_stats_screen.dart | 39 +++++++++---------- 5 files changed, 47 insertions(+), 43 deletions(-) diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 89a569fd20..466d4c88b5 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -1094,11 +1094,13 @@ class ServerAnalysisSummary extends ConsumerWidget { .read(ctrlProvider.notifier) .requestServerAnalysis() .catchError((Object e) { - showPlatformSnackbar( - context, - e.toString(), - type: SnackBarType.error, - ); + if (context.mounted) { + showPlatformSnackbar( + context, + e.toString(), + type: SnackBarType.error, + ); + } }); }); }, diff --git a/lib/src/view/play/create_custom_game_screen.dart b/lib/src/view/play/create_custom_game_screen.dart index 578edd5565..9078ecebe1 100644 --- a/lib/src/view/play/create_custom_game_screen.dart +++ b/lib/src/view/play/create_custom_game_screen.dart @@ -191,17 +191,19 @@ class _ChallengesBodyState extends ConsumerState<_ChallengesBody> { case 'redirect': final data = event.data as Map; final gameFullId = pick(data['id']).asGameFullIdOrThrow(); - pushPlatformRoute( - context, - rootNavigator: true, - builder: (BuildContext context) { - return StandaloneGameScreen( - params: InitialStandaloneGameParams( - id: gameFullId, - ), - ); - }, - ); + if (mounted) { + pushPlatformRoute( + context, + rootNavigator: true, + builder: (BuildContext context) { + return StandaloneGameScreen( + params: InitialStandaloneGameParams( + id: gameFullId, + ), + ); + }, + ); + } widget.setViewMode(_ViewMode.create); case 'reload_seeks': diff --git a/lib/src/view/puzzle/puzzle_screen.dart b/lib/src/view/puzzle/puzzle_screen.dart index a4099e0b45..53669eacb7 100644 --- a/lib/src/view/puzzle/puzzle_screen.dart +++ b/lib/src/view/puzzle/puzzle_screen.dart @@ -564,12 +564,11 @@ class _BottomBar extends ConsumerWidget { makeLabel: (context) => Text( context.l10n.puzzleFromGameLink(puzzleState.puzzle.game.id.value), ), - onPressed: (_) { - ref - .read( + onPressed: (_) async { + final game = await ref.read( archivedGameProvider(id: puzzleState.puzzle.game.id).future, - ) - .then((game) { + ); + if (context.mounted) { pushPlatformRoute( context, builder: (context) => ArchivedGameScreen( @@ -578,7 +577,7 @@ class _BottomBar extends ConsumerWidget { initialCursor: puzzleState.puzzle.puzzle.initialPly + 1, ), ); - }); + } }, ), ], diff --git a/lib/src/view/puzzle/storm_screen.dart b/lib/src/view/puzzle/storm_screen.dart index 0ff1868a7e..163a6e2bdc 100644 --- a/lib/src/view/puzzle/storm_screen.dart +++ b/lib/src/view/puzzle/storm_screen.dart @@ -130,7 +130,9 @@ class _Body extends ConsumerWidget { ref.listen(ctrlProvider, (prev, state) { if (prev?.mode != StormMode.ended && state.mode == StormMode.ended) { Future.delayed(const Duration(milliseconds: 200), () { - _showStats(context, ref.read(ctrlProvider).stats!); + if (context.mounted) { + _showStats(context, ref.read(ctrlProvider).stats!); + } }); } diff --git a/lib/src/view/user/perf_stats_screen.dart b/lib/src/view/user/perf_stats_screen.dart index 322070ce1f..91df7f59fb 100644 --- a/lib/src/view/user/perf_stats_screen.dart +++ b/lib/src/view/user/perf_stats_screen.dart @@ -656,28 +656,27 @@ class _GameListWidget extends ConsumerWidget { children: [ for (final game in games) _GameListTile( - onTap: () { + onTap: () async { final gameIds = ISet(games.map((g) => g.gameId)); - ref - .withClient( + final list = await ref.withClient( (client) => GameRepository(client).getGamesByIds(gameIds), - ) - .then((list) { - final gameData = - list.firstWhereOrNull((g) => g.id == game.gameId); - if (gameData != null && gameData.variant.isSupported) { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => ArchivedGameScreen( - gameData: gameData, - orientation: user.id == gameData.white.user?.id - ? Side.white - : Side.black, - ), - ); - } - }); + ); + final gameData = + list.firstWhereOrNull((g) => g.id == game.gameId); + if (context.mounted && + gameData != null && + gameData.variant.isSupported) { + pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => ArchivedGameScreen( + gameData: gameData, + orientation: user.id == gameData.white.user?.id + ? Side.white + : Side.black, + ), + ); + } }, playerTitle: UserFullNameWidget( user: game.opponent, From f339fc2e7ac37a7ea105b25fddd232d2868d8a27 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 28 May 2024 14:56:40 +0200 Subject: [PATCH 16/34] Fix format --- lib/src/view/analysis/analysis_screen.dart | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 466d4c88b5..c0b26316a3 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -1094,13 +1094,13 @@ class ServerAnalysisSummary extends ConsumerWidget { .read(ctrlProvider.notifier) .requestServerAnalysis() .catchError((Object e) { - if (context.mounted) { - showPlatformSnackbar( - context, - e.toString(), - type: SnackBarType.error, - ); - } + if (context.mounted) { + showPlatformSnackbar( + context, + e.toString(), + type: SnackBarType.error, + ); + } }); }); }, From 1f626f994bf07dd5deb52160f654525fdfbaa125 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 28 May 2024 15:03:24 +0200 Subject: [PATCH 17/34] Add a doc comment --- lib/src/widgets/feedback.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/src/widgets/feedback.dart b/lib/src/widgets/feedback.dart index f5f231ff29..5733a0348c 100644 --- a/lib/src/widgets/feedback.dart +++ b/lib/src/widgets/feedback.dart @@ -82,6 +82,10 @@ class CenterLoadingIndicator extends StatelessWidget { } } +/// A screen with an error message and a retry button. +/// +/// This widget is intended to be used when a request fails and the user can +/// retry it. class FullScreenRetryRequest extends StatelessWidget { const FullScreenRetryRequest({ super.key, From 3a45d3b76ab1dd5099465e4e41c1b1109620c2b3 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 28 May 2024 15:32:34 +0200 Subject: [PATCH 18/34] Improve LichessClient tests and types --- lib/src/model/account/account_repository.dart | 2 +- lib/src/model/auth/auth_repository.dart | 4 ++-- .../model/challenge/challenge_repository.dart | 3 ++- lib/src/model/common/http.dart | 8 +++---- lib/src/model/game/game_repository.dart | 2 +- lib/src/model/lobby/create_game_service.dart | 3 +-- lib/src/model/lobby/lobby_repository.dart | 2 +- lib/src/model/puzzle/puzzle_controller.dart | 4 ++-- lib/src/model/puzzle/puzzle_repository.dart | 2 +- .../model/relation/relation_repository.dart | 2 +- .../account/account_repository_test.dart | 6 +++++- test/model/auth/auth_controller_test.dart | 8 ++++--- test/model/game/game_repository_test.dart | 19 +++++++++++++---- test/model/lobby/lobby_repository_test.dart | 8 +++++-- test/model/puzzle/puzzle_repository_test.dart | 21 +++++-------------- test/model/puzzle/puzzle_service_test.dart | 2 +- .../relation/relation_repository_test.dart | 10 +++++++-- test/test_app.dart | 8 +++++-- test/test_container.dart | 18 +++++++++++++++- test/view/game/archived_game_screen_test.dart | 9 +++++--- test/view/puzzle/puzzle_screen_test.dart | 6 +++--- test/view/puzzle/storm_screen_test.dart | 17 +++++++++------ test/view/settings/settings_screen_test.dart | 5 +++-- test/view/user/leaderboard_screen_test.dart | 5 +++-- test/view/user/leaderboard_widget_test.dart | 5 +++-- test/view/user/perf_stats_screen_test.dart | 10 ++++++--- test/view/user/search_screen_test.dart | 8 ++++--- test/view/user/user_screen_test.dart | 5 +++-- 28 files changed, 128 insertions(+), 74 deletions(-) diff --git a/lib/src/model/account/account_repository.dart b/lib/src/model/account/account_repository.dart index dfe0c66f03..aa59b1008f 100644 --- a/lib/src/model/account/account_repository.dart +++ b/lib/src/model/account/account_repository.dart @@ -71,7 +71,7 @@ Future> ongoingGames(OngoingGamesRef ref) async { class AccountRepository { AccountRepository(this.client); - final http.Client client; + final LichessClient client; final Logger _log = Logger('AccountRepository'); Future getProfile() { diff --git a/lib/src/model/auth/auth_repository.dart b/lib/src/model/auth/auth_repository.dart index 2b316d0d7d..3311f11a40 100644 --- a/lib/src/model/auth/auth_repository.dart +++ b/lib/src/model/auth/auth_repository.dart @@ -21,12 +21,12 @@ FlutterAppAuth appAuth(AppAuthRef ref) { class AuthRepository { AuthRepository( - http.Client client, + LichessClient client, FlutterAppAuth appAuth, ) : _client = client, _appAuth = appAuth; - final http.Client _client; + final LichessClient _client; final Logger _log = Logger('AuthRepository'); final FlutterAppAuth _appAuth; diff --git a/lib/src/model/challenge/challenge_repository.dart b/lib/src/model/challenge/challenge_repository.dart index ec0f698bf0..ef8ad79175 100644 --- a/lib/src/model/challenge/challenge_repository.dart +++ b/lib/src/model/challenge/challenge_repository.dart @@ -1,11 +1,12 @@ import 'package:http/http.dart' as http; +import 'package:lichess_mobile/src/model/common/http.dart'; import './challenge_request.dart'; class ChallengeRepository { const ChallengeRepository(this.client); - final http.Client client; + final LichessClient client; Future challenge(String username, ChallengeRequest req) async { final uri = Uri(path: '/api/challenge/$username'); diff --git a/lib/src/model/common/http.dart b/lib/src/model/common/http.dart index 6c15dc7747..5c101a92a3 100644 --- a/lib/src/model/common/http.dart +++ b/lib/src/model/common/http.dart @@ -81,7 +81,7 @@ Client defaultClient(DefaultClientRef ref) { /// /// Only one instance of this client is created and kept alive for the whole app. @Riverpod(keepAlive: true) -Client lichessClient(LichessClientRef ref) { +LichessClient lichessClient(LichessClientRef ref) { final client = LichessClient( // Retry just once, after 500ms, on 429 Too Many Requests. RetryClient( @@ -478,7 +478,7 @@ extension ClientExtension on Client { extension ClientWidgetRefExtension on WidgetRef { /// Runs [fn] with a [LichessClient]. - Future withClient(Future Function(Client) fn) async { + Future withClient(Future Function(LichessClient) fn) async { final client = read(lichessClientProvider); return await fn(client); } @@ -486,7 +486,7 @@ extension ClientWidgetRefExtension on WidgetRef { extension ClientRefExtension on Ref { /// Runs [fn] with a [LichessClient]. - Future withClient(Future Function(Client) fn) async { + Future withClient(Future Function(LichessClient) fn) async { final client = read(lichessClientProvider); return await fn(client); } @@ -500,7 +500,7 @@ extension ClientAutoDisposeRefExtension on AutoDisposeRef { /// If [fn] throws with a [SocketException], the provider is not kept alive, this /// allows to retry the request later. Future withClientCacheFor( - Future Function(Client) fn, + Future Function(LichessClient) fn, Duration duration, ) async { final link = keepAlive(); diff --git a/lib/src/model/game/game_repository.dart b/lib/src/model/game/game_repository.dart index 510afced48..4365a2e872 100644 --- a/lib/src/model/game/game_repository.dart +++ b/lib/src/model/game/game_repository.dart @@ -8,7 +8,7 @@ import 'package:lichess_mobile/src/model/game/playable_game.dart'; class GameRepository { const GameRepository(this.client); - final http.Client client; + final LichessClient client; Future getGame(GameId id) { return client.readJson( diff --git a/lib/src/model/lobby/create_game_service.dart b/lib/src/model/lobby/create_game_service.dart index 01b3292630..2376b430a6 100644 --- a/lib/src/model/lobby/create_game_service.dart +++ b/lib/src/model/lobby/create_game_service.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:deep_pick/deep_pick.dart'; -import 'package:http/http.dart' as http; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; @@ -24,7 +23,7 @@ class CreateGameService { final CreateGameServiceRef ref; final Logger _log; - http.Client get lichessClient => ref.read(lichessClientProvider); + LichessClient get lichessClient => ref.read(lichessClientProvider); StreamSubscription? _pendingGameConnection; diff --git a/lib/src/model/lobby/lobby_repository.dart b/lib/src/model/lobby/lobby_repository.dart index 9c582983f9..38cad7d6e2 100644 --- a/lib/src/model/lobby/lobby_repository.dart +++ b/lib/src/model/lobby/lobby_repository.dart @@ -24,7 +24,7 @@ Future> correspondenceChallenges( class LobbyRepository { LobbyRepository(this.client); - final http.Client client; + final LichessClient client; Future createSeek(GameSeek seek, {required String sri}) async { final uri = Uri(path: '/api/board/seek', queryParameters: {'sri': sri}); diff --git a/lib/src/model/puzzle/puzzle_controller.dart b/lib/src/model/puzzle/puzzle_controller.dart index 25c68dd184..a8cf682f80 100644 --- a/lib/src/model/puzzle/puzzle_controller.dart +++ b/lib/src/model/puzzle/puzzle_controller.dart @@ -5,7 +5,6 @@ import 'package:collection/collection.dart'; import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:http/http.dart' as http; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/node.dart'; @@ -63,7 +62,8 @@ class PuzzleController extends _$PuzzleController { return _loadNewContext(initialContext, initialStreak); } - PuzzleRepository _repository(http.Client client) => PuzzleRepository(client); + PuzzleRepository _repository(LichessClient client) => + PuzzleRepository(client); PuzzleState _loadNewContext( PuzzleContext context, diff --git a/lib/src/model/puzzle/puzzle_repository.dart b/lib/src/model/puzzle/puzzle_repository.dart index fceb6dd676..de6028f972 100644 --- a/lib/src/model/puzzle/puzzle_repository.dart +++ b/lib/src/model/puzzle/puzzle_repository.dart @@ -26,7 +26,7 @@ part 'puzzle_repository.freezed.dart'; class PuzzleRepository { PuzzleRepository(this.client); - final http.Client client; + final LichessClient client; Future selectBatch({ required int nb, diff --git a/lib/src/model/relation/relation_repository.dart b/lib/src/model/relation/relation_repository.dart index 5efff45e2a..3ecd5bac89 100644 --- a/lib/src/model/relation/relation_repository.dart +++ b/lib/src/model/relation/relation_repository.dart @@ -6,7 +6,7 @@ import 'package:lichess_mobile/src/model/user/user.dart'; class RelationRepository { const RelationRepository(this.client); - final http.Client client; + final LichessClient client; Future> getFollowing() async { return client.readNdJsonList( diff --git a/test/model/account/account_repository_test.dart b/test/model/account/account_repository_test.dart index fbe1706cd4..1dd7d4f1c8 100644 --- a/test/model/account/account_repository_test.dart +++ b/test/model/account/account_repository_test.dart @@ -2,7 +2,9 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; +import 'package:lichess_mobile/src/model/common/http.dart'; +import '../../test_container.dart'; import '../../test_utils.dart'; void main() { @@ -59,7 +61,9 @@ void main() { return mockResponse('', 404); }); - final repo = AccountRepository(mockClient); + final container = await lichessClientContainer(mockClient); + final client = container.read(lichessClientProvider); + final repo = AccountRepository(client); final result = await repo.getPreferences(); expect(result, isA()); diff --git a/test/model/auth/auth_controller_test.dart b/test/model/auth/auth_controller_test.dart index 83a878c048..a7603671c0 100644 --- a/test/model/auth/auth_controller_test.dart +++ b/test/model/auth/auth_controller_test.dart @@ -22,7 +22,7 @@ class Listener extends Mock { void call(T? previous, T value); } -final lichessClient = MockClient((request) { +final client = MockClient((request) { if (request.url.path == '/api/account') { return mockResponse(testAccountResponse, 200); } else if (request.method == 'DELETE' && request.url.path == '/api/token') { @@ -79,7 +79,8 @@ void main() { overrides: [ appAuthProvider.overrideWithValue(mockFlutterAppAuth), sessionStorageProvider.overrideWithValue(mockSessionStorage), - lichessClientProvider.overrideWithValue(lichessClient), + lichessClientProvider + .overrideWith((ref) => LichessClient(client, ref)), ], ); @@ -121,7 +122,8 @@ void main() { overrides: [ appAuthProvider.overrideWithValue(mockFlutterAppAuth), sessionStorageProvider.overrideWithValue(mockSessionStorage), - lichessClientProvider.overrideWithValue(lichessClient), + lichessClientProvider + .overrideWith((ref) => LichessClient(client, ref)), ], ); diff --git a/test/model/game/game_repository_test.dart b/test/model/game/game_repository_test.dart index a5b1633c54..bc86bd9aa1 100644 --- a/test/model/game/game_repository_test.dart +++ b/test/model/game/game_repository_test.dart @@ -2,10 +2,12 @@ import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; +import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; import 'package:lichess_mobile/src/model/game/game_repository.dart'; +import '../../test_container.dart'; import '../../test_utils.dart'; void main() { @@ -24,7 +26,10 @@ void main() { return mockResponse('', 404); }); - final repo = GameRepository(mockClient); + final container = await lichessClientContainer(mockClient); + final client = container.read(lichessClientProvider); + + final repo = GameRepository(client); final result = await repo.getRecentGames(const UserId('testUser')); expect(result, isA>()); @@ -54,7 +59,9 @@ void main() { return mockResponse('', 404); }); - final repo = GameRepository(mockClient); + final container = await lichessClientContainer(mockClient); + final client = container.read(lichessClientProvider); + final repo = GameRepository(client); final result = await repo.getGamesByIds(ids); expect(result, isA>()); @@ -76,7 +83,9 @@ void main() { return mockResponse('', 404); }); - final repo = GameRepository(mockClient); + final container = await lichessClientContainer(mockClient); + final client = container.read(lichessClientProvider); + final repo = GameRepository(client); final game = await repo.getGame(const GameId('qVChCOTc')); expect(game, isA()); @@ -97,7 +106,9 @@ void main() { return mockResponse('', 404); }); - final repo = GameRepository(mockClient); + final container = await lichessClientContainer(mockClient); + final client = container.read(lichessClientProvider); + final repo = GameRepository(client); final result = await repo.getGame(const GameId('1vdsvmxp')); expect(result, isA()); diff --git a/test/model/lobby/lobby_repository_test.dart b/test/model/lobby/lobby_repository_test.dart index 1a385cbff4..e790bae327 100644 --- a/test/model/lobby/lobby_repository_test.dart +++ b/test/model/lobby/lobby_repository_test.dart @@ -1,10 +1,12 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; +import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/lobby/correspondence_challenge.dart'; import 'package:lichess_mobile/src/model/lobby/lobby_repository.dart'; +import '../../test_container.dart'; import '../../test_utils.dart'; void main() { @@ -15,10 +17,12 @@ void main() { return mockResponse('', 404); }); - final repo = LobbyRepository(mockClient); - group('LobbyRepository.getCorrespondenceChallenges', () { test('read json', () async { + final container = await lichessClientContainer(mockClient); + final client = container.read(lichessClientProvider); + final repo = LobbyRepository(client); + final data = await repo.getCorrespondenceChallenges(); expect(data, isA>()); diff --git a/test/model/puzzle/puzzle_repository_test.dart b/test/model/puzzle/puzzle_repository_test.dart index 7188db22f9..0f884807d0 100644 --- a/test/model/puzzle/puzzle_repository_test.dart +++ b/test/model/puzzle/puzzle_repository_test.dart @@ -1,4 +1,3 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; @@ -9,16 +8,6 @@ import '../../test_container.dart'; import '../../test_utils.dart'; void main() { - Future makeTestContainer(MockClient mockClient) async { - return makeContainer( - overrides: [ - lichessClientProvider.overrideWith((ref) { - return mockClient; - }), - ], - ); - } - group('PuzzleRepository', () { test('selectBatch', () async { final mockClient = MockClient((request) { @@ -33,7 +22,7 @@ void main() { return mockResponse('', 404); }); - final container = await makeTestContainer(mockClient); + final container = await lichessClientContainer(mockClient); final client = container.read(lichessClientProvider); final repo = PuzzleRepository(client); @@ -56,7 +45,7 @@ void main() { return mockResponse('', 404); }); - final container = await makeTestContainer(mockClient); + final container = await lichessClientContainer(mockClient); final client = container.read(lichessClientProvider); final repo = PuzzleRepository(client); @@ -79,7 +68,7 @@ void main() { } return mockResponse('', 404); }); - final container = await makeTestContainer(mockClient); + final container = await lichessClientContainer(mockClient); final client = container.read(lichessClientProvider); final repo = PuzzleRepository(client); @@ -103,7 +92,7 @@ void main() { return mockResponse('', 404); }); - final container = await makeTestContainer(mockClient); + final container = await lichessClientContainer(mockClient); final client = container.read(lichessClientProvider); final repo = PuzzleRepository(client); final result = await repo.streak(); @@ -124,7 +113,7 @@ void main() { return mockResponse('', 404); }); - final container = await makeTestContainer(mockClient); + final container = await lichessClientContainer(mockClient); final client = container.read(lichessClientProvider); final repo = PuzzleRepository(client); final result = await repo.puzzleDashboard(30); diff --git a/test/model/puzzle/puzzle_service_test.dart b/test/model/puzzle/puzzle_service_test.dart index b7f8a78b68..734b3b15b2 100644 --- a/test/model/puzzle/puzzle_service_test.dart +++ b/test/model/puzzle/puzzle_service_test.dart @@ -33,7 +33,7 @@ void main() { return db; }), lichessClientProvider.overrideWith((ref) { - return mockClient; + return LichessClient(mockClient, ref); }), ], ); diff --git a/test/model/relation/relation_repository_test.dart b/test/model/relation/relation_repository_test.dart index f90ced3eff..5800d76431 100644 --- a/test/model/relation/relation_repository_test.dart +++ b/test/model/relation/relation_repository_test.dart @@ -1,9 +1,11 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; +import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/relation/relation_repository.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; +import '../../test_container.dart'; import '../../test_utils.dart'; void main() { @@ -23,7 +25,9 @@ void main() { return mockResponse('', 404); }); - final repo = RelationRepository(mockClient); + final container = await lichessClientContainer(mockClient); + final client = container.read(lichessClientProvider); + final repo = RelationRepository(client); final result = await repo.getFollowing(); expect(result, isA>()); @@ -43,7 +47,9 @@ void main() { return mockResponse('', 404); }); - final repo = RelationRepository(mockClient); + final container = await lichessClientContainer(mockClient); + final client = container.read(lichessClientProvider); + final repo = RelationRepository(client); final result = await repo.getFollowing(); expect(result, isA>()); diff --git a/test/test_app.dart b/test/test_app.dart index 95deb16706..f88e881b9a 100644 --- a/test/test_app.dart +++ b/test/test_app.dart @@ -83,9 +83,13 @@ Future buildTestApp( return ProviderScope( overrides: [ // ignore: scoped_providers_should_specify_dependencies - lichessClientProvider.overrideWithValue(mockClient), + lichessClientProvider.overrideWith((ref) { + return LichessClient(mockClient, ref); + }), // ignore: scoped_providers_should_specify_dependencies - defaultClientProvider.overrideWithValue(mockClient), + defaultClientProvider.overrideWith((_) { + return mockClient; + }), // ignore: scoped_providers_should_specify_dependencies webSocketChannelFactoryProvider.overrideWith((ref) { return FakeWebSocketChannelFactory(() => FakeWebSocketChannel()); diff --git a/test/test_container.dart b/test/test_container.dart index 5a363bf20f..042f230b2f 100644 --- a/test/test_container.dart +++ b/test/test_container.dart @@ -6,6 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/app_dependencies.dart'; import 'package:lichess_mobile/src/crashlytics.dart'; @@ -38,6 +39,19 @@ class MockHttpClient extends Mock implements http.Client {} const shouldLog = false; +/// Returns a [ProviderContainer] with a mocked [LichessClient] configured with +/// the given [mockClient]. +Future lichessClientContainer(MockClient mockClient) async { + return makeContainer( + overrides: [ + lichessClientProvider.overrideWith((ref) { + return LichessClient(mockClient, ref); + }), + ], + ); +} + +/// Returns a [ProviderContainer] with default mocks, ready for testing. Future makeContainer({ List? overrides, AuthSessionState? userSession, @@ -68,7 +82,9 @@ Future makeContainer({ ref.onDispose(pool.dispose); return pool; }), - lichessClientProvider.overrideWithValue(MockHttpClient()), + lichessClientProvider.overrideWith((ref) { + return LichessClient(MockHttpClient(), ref); + }), connectivityProvider.overrideWith((ref) { return Stream.value( const ConnectivityStatus( diff --git a/test/view/game/archived_game_screen_test.dart b/test/view/game/archived_game_screen_test.dart index c8a22cfdef..4c699279b3 100644 --- a/test/view/game/archived_game_screen_test.dart +++ b/test/view/game/archived_game_screen_test.dart @@ -20,7 +20,7 @@ import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import '../../test_app.dart'; import '../../test_utils.dart'; -final lichessClient = MockClient((request) { +final client = MockClient((request) { if (request.url.path == '/game/export/qVChCOTc') { return mockResponse(gameResponse, 200); } @@ -39,7 +39,8 @@ void main() { orientation: Side.white, ), overrides: [ - lichessClientProvider.overrideWithValue(lichessClient), + lichessClientProvider + .overrideWith((ref) => LichessClient(client, ref)), ], ); @@ -111,7 +112,9 @@ void main() { orientation: Side.white, ), overrides: [ - lichessClientProvider.overrideWithValue(lichessClient), + lichessClientProvider.overrideWith((ref) { + return LichessClient(client, ref); + }), ], ); diff --git a/test/view/puzzle/puzzle_screen_test.dart b/test/view/puzzle/puzzle_screen_test.dart index e0b91529e7..04f6e6e671 100644 --- a/test/view/puzzle/puzzle_screen_test.dart +++ b/test/view/puzzle/puzzle_screen_test.dart @@ -156,7 +156,7 @@ void main() { ), overrides: [ lichessClientProvider.overrideWith((ref) { - return mockClient; + return LichessClient(mockClient, ref); }), puzzleBatchStorageProvider.overrideWith((ref) { return mockBatchStorage; @@ -270,7 +270,7 @@ void main() { ), overrides: [ lichessClientProvider.overrideWith((ref) { - return mockClient; + return LichessClient(mockClient, ref); }), puzzleBatchStorageProvider.overrideWith((ref) { return mockBatchStorage; @@ -370,7 +370,7 @@ void main() { ), overrides: [ lichessClientProvider.overrideWith((ref) { - return mockClient; + return LichessClient(mockClient, ref); }), puzzleBatchStorageProvider.overrideWith((ref) { return mockBatchStorage; diff --git a/test/view/puzzle/storm_screen_test.dart b/test/view/puzzle/storm_screen_test.dart index 9acd6787ec..7e38602448 100644 --- a/test/view/puzzle/storm_screen_test.dart +++ b/test/view/puzzle/storm_screen_test.dart @@ -13,7 +13,7 @@ import 'package:lichess_mobile/src/view/puzzle/storm_screen.dart'; import '../../test_app.dart'; import '../../test_utils.dart'; -final lichessClient = MockClient((request) { +final client = MockClient((request) { if (request.url.path == '/storm') { return mockResponse('', 200); } @@ -32,7 +32,8 @@ void main() { home: const StormScreen(), overrides: [ stormProvider.overrideWith((ref) => mockStromRun), - lichessClientProvider.overrideWithValue(lichessClient), + lichessClientProvider + .overrideWith((ref) => LichessClient(client, ref)), ], ); @@ -52,7 +53,8 @@ void main() { home: const StormScreen(), overrides: [ stormProvider.overrideWith((ref) => mockStromRun), - lichessClientProvider.overrideWithValue(lichessClient), + lichessClientProvider + .overrideWith((ref) => LichessClient(client, ref)), ], ); @@ -75,7 +77,8 @@ void main() { home: const StormScreen(), overrides: [ stormProvider.overrideWith((ref) => mockStromRun), - lichessClientProvider.overrideWithValue(lichessClient), + lichessClientProvider + .overrideWith((ref) => LichessClient(client, ref)), ], ); @@ -129,7 +132,8 @@ void main() { home: const StormScreen(), overrides: [ stormProvider.overrideWith((ref) => mockStromRun), - lichessClientProvider.overrideWithValue(lichessClient), + lichessClientProvider + .overrideWith((ref) => LichessClient(client, ref)), ], ); @@ -173,7 +177,8 @@ void main() { home: const StormScreen(), overrides: [ stormProvider.overrideWith((ref) => mockStromRun), - lichessClientProvider.overrideWithValue(lichessClient), + lichessClientProvider + .overrideWith((ref) => LichessClient(client, ref)), ], ); diff --git a/test/view/settings/settings_screen_test.dart b/test/view/settings/settings_screen_test.dart index 5ae01e6eec..4f63a24d4b 100644 --- a/test/view/settings/settings_screen_test.dart +++ b/test/view/settings/settings_screen_test.dart @@ -11,7 +11,7 @@ import '../../model/auth/fake_session_storage.dart'; import '../../test_app.dart'; import '../../test_utils.dart'; -final lichessClient = MockClient((request) { +final client = MockClient((request) { if (request.method == 'DELETE' && request.url.path == '/api/token') { return mockResponse('ok', 200); } @@ -65,7 +65,8 @@ void main() { home: const SettingsScreen(), userSession: fakeSession, overrides: [ - lichessClientProvider.overrideWithValue(lichessClient), + lichessClientProvider + .overrideWith((ref) => LichessClient(client, ref)), ], ); diff --git a/test/view/user/leaderboard_screen_test.dart b/test/view/user/leaderboard_screen_test.dart index 749be400ff..f060f3cf28 100644 --- a/test/view/user/leaderboard_screen_test.dart +++ b/test/view/user/leaderboard_screen_test.dart @@ -7,7 +7,7 @@ import 'package:lichess_mobile/src/view/user/leaderboard_screen.dart'; import '../../test_app.dart'; import '../../test_utils.dart'; -final lichessClient = MockClient((request) { +final client = MockClient((request) { if (request.url.path == '/api/player') { return mockResponse(leaderboardResponse, 200); } @@ -24,7 +24,8 @@ void main() { final app = await buildTestApp( tester, overrides: [ - lichessClientProvider.overrideWithValue(lichessClient), + lichessClientProvider + .overrideWith((ref) => LichessClient(client, ref)), ], home: const LeaderboardScreen(), ); diff --git a/test/view/user/leaderboard_widget_test.dart b/test/view/user/leaderboard_widget_test.dart index 0e0b55a135..b51d501dda 100644 --- a/test/view/user/leaderboard_widget_test.dart +++ b/test/view/user/leaderboard_widget_test.dart @@ -8,7 +8,7 @@ import 'package:lichess_mobile/src/view/user/leaderboard_widget.dart'; import '../../test_app.dart'; import '../../test_utils.dart'; -final lichessClient = MockClient((request) { +final client = MockClient((request) { if (request.url.path == '/api/player/top/1/standard') { return mockResponse(top1Response, 200); } @@ -25,7 +25,8 @@ void main() { tester, home: Column(children: [LeaderboardWidget()]), overrides: [ - lichessClientProvider.overrideWithValue(lichessClient), + lichessClientProvider + .overrideWith((ref) => LichessClient(client, ref)), ], ); diff --git a/test/view/user/perf_stats_screen_test.dart b/test/view/user/perf_stats_screen_test.dart index 7d363dfa32..a5791e4ca8 100644 --- a/test/view/user/perf_stats_screen_test.dart +++ b/test/view/user/perf_stats_screen_test.dart @@ -10,7 +10,7 @@ import '../../model/auth/fake_auth_repository.dart'; import '../../test_app.dart'; import '../../test_utils.dart'; -final lichessClient = MockClient((request) { +final client = MockClient((request) { if (request.url.path == '/api/user/${fakeUser.id}/perf/${testPerf.name}') { return mockResponse(userPerfStatsResponse, 200); } @@ -34,7 +34,9 @@ void main() { perf: testPerf, ), overrides: [ - lichessClientProvider.overrideWithValue(lichessClient), + lichessClientProvider.overrideWith((ref) { + return LichessClient(client, ref); + }), ], ); @@ -64,7 +66,9 @@ void main() { perf: testPerf, ), overrides: [ - lichessClientProvider.overrideWithValue(lichessClient), + lichessClientProvider.overrideWith((ref) { + return LichessClient(client, ref); + }), ], ); diff --git a/test/view/user/search_screen_test.dart b/test/view/user/search_screen_test.dart index f6e2da6d1f..9691567d41 100644 --- a/test/view/user/search_screen_test.dart +++ b/test/view/user/search_screen_test.dart @@ -10,7 +10,7 @@ import 'package:lichess_mobile/src/widgets/user_list_tile.dart'; import '../../test_app.dart'; import '../../test_utils.dart'; -final lichessClient = MockClient((request) { +final client = MockClient((request) { if (request.url.path == '/api/player/autocomplete') { if (request.url.queryParameters['term'] == 'joh') { return mockResponse(johResponse, 200); @@ -29,7 +29,8 @@ void main() { tester, home: const SearchScreen(), overrides: [ - lichessClientProvider.overrideWithValue(lichessClient), + lichessClientProvider + .overrideWith((ref) => LichessClient(client, ref)), ], ); @@ -69,7 +70,8 @@ void main() { tester, home: const SearchScreen(), overrides: [ - lichessClientProvider.overrideWithValue(lichessClient), + lichessClientProvider + .overrideWith((ref) => LichessClient(client, ref)), ], ); diff --git a/test/view/user/user_screen_test.dart b/test/view/user/user_screen_test.dart index 51e404c6af..f3e52f2830 100644 --- a/test/view/user/user_screen_test.dart +++ b/test/view/user/user_screen_test.dart @@ -9,7 +9,7 @@ import '../../model/user/user_repository_test.dart'; import '../../test_app.dart'; import '../../test_utils.dart'; -final lichessClient = MockClient((request) { +final client = MockClient((request) { if (request.url.path == '/api/games/user/$testUserId') { return mockResponse(userGameResponse, 200); } else if (request.url.path == '/api/user/$testUserId') { @@ -42,7 +42,8 @@ void main() { tester, home: const UserScreen(user: testUser), overrides: [ - lichessClientProvider.overrideWithValue(lichessClient), + lichessClientProvider + .overrideWith((ref) => LichessClient(client, ref)), ], ); From b9dd6308b97f9d54668e2ea60733915d1acc376d Mon Sep 17 00:00:00 2001 From: ZTL-UwU Date: Tue, 28 May 2024 22:26:50 +0800 Subject: [PATCH 19/34] Use gameStorage when offline Signed-off-by: ZTL-UwU --- lib/src/model/game/game_history.dart | 68 ++++++++++++++++++++++------ lib/src/view/user/recent_games.dart | 23 +--------- 2 files changed, 55 insertions(+), 36 deletions(-) diff --git a/lib/src/model/game/game_history.dart b/lib/src/model/game/game_history.dart index be2cbdb8cb..8ee010e488 100644 --- a/lib/src/model/game/game_history.dart +++ b/lib/src/model/game/game_history.dart @@ -9,6 +9,8 @@ import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; import 'package:lichess_mobile/src/model/game/game_repository.dart'; import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; +import 'package:lichess_mobile/src/model/game/game_storage.dart'; +import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/riverpod.dart'; import 'package:result_extensions/result_extensions.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -23,22 +25,39 @@ class UserGameHistory extends _$UserGameHistory { final _list = <(LightArchivedGame, Side)>[]; @override - Future build(UserId id) async { + Future build( + UserId id, + ) async { ref.cacheFor(const Duration(minutes: 30)); ref.onDispose(() { _list.clear(); }); - final recentGames = - await ref.watch(userRecentGamesProvider(userId: id).future); - _list.addAll( - recentGames - .map((e) => (e, e.white.user?.id == id ? Side.white : Side.black)), - ); + + final connectivity = ref.watch(connectivityProvider); + final online = connectivity.valueOrNull?.isOnline ?? false; + if (online) { + final recentGames = + await ref.watch(userRecentGamesProvider(userId: id).future); + _list.addAll( + recentGames.map( + (e) => + // user is not null for at least one of the players + (e, e.white.user?.id == id ? Side.white : Side.black), + ), + ); + } else { + final recentGames = await ref.watch(recentStoredGamesProvider.future); + _list.addAll( + recentGames.map((e) => (e.game.data, e.game.youAre ?? Side.white)), + ); + } + return UserGameHistoryState( gameList: _list.toIList(), isLoading: false, hasMore: true, hasError: false, + online: online, ); } @@ -48,10 +67,17 @@ class UserGameHistory extends _$UserGameHistory { final currentVal = state.requireValue; state = AsyncData(currentVal.copyWith(isLoading: true)); Result.capture( - ref.withClient( - (client) => GameRepository(client) - .getUserGames(id, max: _nbPerPage, until: _list.last.$1.createdAt), - ), + currentVal.online + ? ref.withClient( + (client) => GameRepository(client).getUserGames( + id, + max: _nbPerPage, + until: _list.last.$1.createdAt, + ), + ) + : ref + .watch(gameStorageProvider) + .page(max: _nbPerPage, until: _list.last.$1.createdAt), ).fold( (value) { if (value.isEmpty) { @@ -60,16 +86,27 @@ class UserGameHistory extends _$UserGameHistory { ); return; } - _list.addAll( - value.map( - (e) => (e, e.white.user?.id == id ? Side.white : Side.black)), - ); + if (value is IList) { + _list.addAll( + value.map( + (e) => (e, e.white.user?.id == id ? Side.white : Side.black), + ), + ); + } else if (value is IList< + ({ArchivedGame game, DateTime lastModified, UserId userId})>) { + _list.addAll( + value.map( + (e) => (e.game.data, e.game.youAre ?? Side.white), + ), + ); + } state = AsyncData( UserGameHistoryState( gameList: _list.toIList(), isLoading: false, hasMore: true, hasError: false, + online: currentVal.online, ), ); }, @@ -88,5 +125,6 @@ class UserGameHistoryState with _$UserGameHistoryState { required bool isLoading, required bool hasMore, required bool hasError, + required bool online, }) = _UserGameHistoryState; } diff --git a/lib/src/view/user/recent_games.dart b/lib/src/view/user/recent_games.dart index 92a685907e..e1cd5ab0b5 100644 --- a/lib/src/view/user/recent_games.dart +++ b/lib/src/view/user/recent_games.dart @@ -4,10 +4,9 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; -import 'package:lichess_mobile/src/model/game/game_repository.dart'; +import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; import 'package:lichess_mobile/src/model/game/game_storage.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; @@ -23,22 +22,6 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'recent_games.g.dart'; -@riverpod -Future> _userRecentGames( - _UserRecentGamesRef ref, { - required UserId userId, -}) { - return ref.withClientCacheFor( - (client) => GameRepository(client).getUserGames(userId), - // cache is important because the associated widget is in a [ListView] and - // the provider may be instanciated multiple times in a short period of time - // (e.g. when scrolling) - // TODO: consider debouncing the request instead of caching it, or make the - // request in the parent widget and pass the result to the child - const Duration(minutes: 1), - ); -} - class RecentGames extends ConsumerWidget { const RecentGames({this.user, super.key}); @@ -51,9 +34,7 @@ class RecentGames extends ConsumerWidget { final userId = user?.id ?? session?.user.id; final recentGames = user != null - ? ref - .watch(_userRecentGamesProvider(userId: user!.id)) - .whenData((data) { + ? ref.watch(userRecentGamesProvider(userId: user!.id)).whenData((data) { return data .map( (e) => From 854ce066129b3bdf04585abd1bbf9b13ac5fc68d Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 28 May 2024 16:37:33 +0200 Subject: [PATCH 20/34] Remove duplicate method --- lib/src/model/common/http.dart | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/lib/src/model/common/http.dart b/lib/src/model/common/http.dart index 5c101a92a3..43ee9f65fb 100644 --- a/lib/src/model/common/http.dart +++ b/lib/src/model/common/http.dart @@ -284,16 +284,16 @@ class LichessClient implements Client { return Response.fromStream(await send(request)); } +} - /// Throws an error if [response] is not successful. - void _checkResponseSuccess(Uri url, Response response) { - if (response.statusCode < 400) return; - var message = 'Request to $url failed with status ${response.statusCode}'; - if (response.reasonPhrase != null) { - message = '$message: ${response.reasonPhrase}'; - } - throw ClientException('$message.', url); +/// Throws an error if [response] is not successful. +void _checkResponseSuccess(Uri url, Response response) { + if (response.statusCode < 400) return; + var message = 'Request to $url failed with status ${response.statusCode}'; + if (response.reasonPhrase != null) { + message = '$message: ${response.reasonPhrase}'; } + throw ClientException('$message.', url); } extension ClientExtension on Client { @@ -464,16 +464,6 @@ extension ClientExtension on Client { ); } } - - /// Throws an error if [response] is not successful. - void _checkResponseSuccess(Uri url, Response response) { - if (response.statusCode < 400) return; - var message = 'Request to $url failed with status ${response.statusCode}'; - if (response.reasonPhrase != null) { - message = '$message: ${response.reasonPhrase}'; - } - throw ClientException('$message.', url); - } } extension ClientWidgetRefExtension on WidgetRef { From 6a72d1c0b5481bb8ed20fdc76d0cdc81e2c0cef0 Mon Sep 17 00:00:00 2001 From: ZTL-UwU Date: Tue, 28 May 2024 22:39:16 +0800 Subject: [PATCH 21/34] Remove unused imports Signed-off-by: ZTL-UwU --- lib/src/view/user/recent_games.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/src/view/user/recent_games.dart b/lib/src/view/user/recent_games.dart index e1cd5ab0b5..d7b642706b 100644 --- a/lib/src/view/user/recent_games.dart +++ b/lib/src/view/user/recent_games.dart @@ -4,8 +4,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/account/account_repository.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/archived_game.dart'; import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; import 'package:lichess_mobile/src/model/game/game_storage.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; From 0ab7560e8ac8b5b8ae50c8b00250a93cabb47556 Mon Sep 17 00:00:00 2001 From: ZTL-UwU Date: Tue, 28 May 2024 22:43:22 +0800 Subject: [PATCH 22/34] Remove unused imports Signed-off-by: ZTL-UwU --- lib/src/view/user/recent_games.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/src/view/user/recent_games.dart b/lib/src/view/user/recent_games.dart index d7b642706b..a52d42b3a2 100644 --- a/lib/src/view/user/recent_games.dart +++ b/lib/src/view/user/recent_games.dart @@ -16,9 +16,6 @@ import 'package:lichess_mobile/src/view/user/full_games_screen.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'recent_games.g.dart'; class RecentGames extends ConsumerWidget { const RecentGames({this.user, super.key}); From 5acb1a666b4b5824b51d744b4615db2b525dba53 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 28 May 2024 16:59:57 +0200 Subject: [PATCH 23/34] Remove comment --- lib/src/model/common/http.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/src/model/common/http.dart b/lib/src/model/common/http.dart index 43ee9f65fb..5b510ce38f 100644 --- a/lib/src/model/common/http.dart +++ b/lib/src/model/common/http.dart @@ -178,8 +178,6 @@ class LichessClient implements Client { final request = response.request!; final method = request.method; final url = request.url; - // TODD for now logging isn't much useful - // We could use improve it later to create an http logger in the app. _logger.warning( '$method $url responded with status ${response.statusCode} ${response.reasonPhrase}', ); From bcbc2d552f9f12f70658916ef655b12609833d5f Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 28 May 2024 19:18:20 +0200 Subject: [PATCH 24/34] Refactor recent game providers --- lib/src/model/account/account_repository.dart | 14 --- lib/src/model/game/archived_game.dart | 3 + lib/src/model/game/game_history.dart | 110 +++++++++++------- lib/src/model/game/game_repository.dart | 46 +++++--- .../model/game/game_repository_providers.dart | 2 +- lib/src/model/game/game_storage.dart | 8 -- lib/src/model/user/user.dart | 17 ++- lib/src/view/game/game_list_tile.dart | 4 +- lib/src/view/game/lobby_screen.dart | 6 +- lib/src/view/home/home_tab_screen.dart | 13 +-- lib/src/view/user/full_games_screen.dart | 14 ++- lib/src/view/user/perf_cards.dart | 6 +- lib/src/view/user/perf_stats_screen.dart | 3 +- lib/src/view/user/recent_games.dart | 38 +----- lib/src/widgets/user_list_tile.dart | 6 +- test/model/auth/fake_auth_repository.dart | 2 +- test/model/game/game_repository_test.dart | 4 +- 17 files changed, 145 insertions(+), 151 deletions(-) diff --git a/lib/src/model/account/account_repository.dart b/lib/src/model/account/account_repository.dart index 388c38b6ec..e85bc03221 100644 --- a/lib/src/model/account/account_repository.dart +++ b/lib/src/model/account/account_repository.dart @@ -7,8 +7,6 @@ import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; -import 'package:lichess_mobile/src/model/game/archived_game.dart'; -import 'package:lichess_mobile/src/model/game/game_repository.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/model/user/user_repository.dart'; import 'package:logging/logging.dart'; @@ -45,18 +43,6 @@ Future> accountActivity(AccountActivityRef ref) async { ); } -@riverpod -Future> accountRecentGames( - AccountRecentGamesRef ref, -) async { - final session = ref.watch(authSessionProvider); - if (session == null) return IList(); - return ref.withClientCacheFor( - (client) => GameRepository(client).getUserGames(session.user.id), - const Duration(hours: 1), - ); -} - @riverpod Future> ongoingGames(OngoingGamesRef ref) async { final session = ref.watch(authSessionProvider); diff --git a/lib/src/model/game/archived_game.dart b/lib/src/model/game/archived_game.dart index 4e01bdf8c8..99ca23a623 100644 --- a/lib/src/model/game/archived_game.dart +++ b/lib/src/model/game/archived_game.dart @@ -85,6 +85,9 @@ class ArchivedGame : white; } +/// A [LightArchivedGame] associated with a point of view of a player. +typedef LightArchivedGameWithPov = ({LightArchivedGame game, Side pov}); + /// A lichess game exported from the API, with less data than [ArchivedGame]. /// /// This is commonly used to display a list of games. diff --git a/lib/src/model/game/game_history.dart b/lib/src/model/game/game_history.dart index 8ee010e488..deff4fcf93 100644 --- a/lib/src/model/game/game_history.dart +++ b/lib/src/model/game/game_history.dart @@ -4,6 +4,7 @@ import 'package:async/async.dart'; import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.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/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; @@ -20,14 +21,35 @@ part 'game_history.g.dart'; const _nbPerPage = 20; +@riverpod +Future> myRecentGames(MyRecentGamesRef ref) { + final connectivity = ref.watch(connectivityProvider); + final session = ref.watch(authSessionProvider); + final online = connectivity.valueOrNull?.isOnline ?? false; + if (session != null && online) { + return ref.withClientCacheFor( + (client) => GameRepository(client).getUserGames(session.user.id), + const Duration(hours: 1), + ); + } else { + final storage = ref.watch(gameStorageProvider); + ref.cacheFor(const Duration(hours: 1)); + return storage.page(userId: session?.user.id).then( + (value) => value + // we can assume that `youAre` is not null either for logged + // in users or for anonymous users + .map((e) => (game: e.game.data, pov: e.game.youAre ?? Side.white)) + .toIList(), + ); + } +} + @riverpod class UserGameHistory extends _$UserGameHistory { - final _list = <(LightArchivedGame, Side)>[]; + final _list = []; @override - Future build( - UserId id, - ) async { + Future build(UserId? userId) async { ref.cacheFor(const Duration(minutes: 30)); ref.onDispose(() { _list.clear(); @@ -35,22 +57,13 @@ class UserGameHistory extends _$UserGameHistory { final connectivity = ref.watch(connectivityProvider); final online = connectivity.valueOrNull?.isOnline ?? false; - if (online) { - final recentGames = - await ref.watch(userRecentGamesProvider(userId: id).future); - _list.addAll( - recentGames.map( - (e) => - // user is not null for at least one of the players - (e, e.white.user?.id == id ? Side.white : Side.black), - ), - ); - } else { - final recentGames = await ref.watch(recentStoredGamesProvider.future); - _list.addAll( - recentGames.map((e) => (e.game.data, e.game.youAre ?? Side.white)), - ); - } + final session = ref.watch(authSessionProvider); + + final recentGames = userId != null + ? ref.read(userRecentGamesProvider(userId: userId).future) + : ref.read(myRecentGamesProvider.future); + + _list.addAll(await recentGames); return UserGameHistoryState( gameList: _list.toIList(), @@ -58,6 +71,7 @@ class UserGameHistory extends _$UserGameHistory { hasMore: true, hasError: false, online: online, + session: session, ); } @@ -67,17 +81,35 @@ class UserGameHistory extends _$UserGameHistory { final currentVal = state.requireValue; state = AsyncData(currentVal.copyWith(isLoading: true)); Result.capture( - currentVal.online + userId != null ? ref.withClient( (client) => GameRepository(client).getUserGames( - id, + userId!, max: _nbPerPage, - until: _list.last.$1.createdAt, + until: _list.last.game.createdAt, ), ) - : ref - .watch(gameStorageProvider) - .page(max: _nbPerPage, until: _list.last.$1.createdAt), + : currentVal.online && currentVal.session != null + ? ref.withClient( + (client) => GameRepository(client).getUserGames( + currentVal.session!.user.id, + max: _nbPerPage, + until: _list.last.game.createdAt, + ), + ) + : ref + .watch(gameStorageProvider) + .page(max: _nbPerPage, until: _list.last.game.createdAt) + .then( + (value) => value + // we can assume that `youAre` is not null either for logged + // in users or for anonymous users + .map((e) => ( + game: e.game.data, + pov: e.game.youAre ?? Side.white + )) + .toIList(), + ), ).fold( (value) { if (value.isEmpty) { @@ -86,27 +118,14 @@ class UserGameHistory extends _$UserGameHistory { ); return; } - if (value is IList) { - _list.addAll( - value.map( - (e) => (e, e.white.user?.id == id ? Side.white : Side.black), - ), - ); - } else if (value is IList< - ({ArchivedGame game, DateTime lastModified, UserId userId})>) { - _list.addAll( - value.map( - (e) => (e.game.data, e.game.youAre ?? Side.white), - ), - ); - } + + _list.addAll(value); + state = AsyncData( - UserGameHistoryState( + currentVal.copyWith( gameList: _list.toIList(), isLoading: false, - hasMore: true, - hasError: false, - online: currentVal.online, + hasMore: value.length == _nbPerPage, ), ); }, @@ -121,10 +140,11 @@ class UserGameHistory extends _$UserGameHistory { @freezed class UserGameHistoryState with _$UserGameHistoryState { const factory UserGameHistoryState({ - required IList<(LightArchivedGame, Side)> gameList, + required IList gameList, required bool isLoading, required bool hasMore, required bool hasError, required bool online, + AuthSessionState? session, }) = _UserGameHistoryState; } diff --git a/lib/src/model/game/game_repository.dart b/lib/src/model/game/game_repository.dart index 9d57236faa..6d4705c59f 100644 --- a/lib/src/model/game/game_repository.dart +++ b/lib/src/model/game/game_repository.dart @@ -1,3 +1,4 @@ +import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:http/http.dart' as http; import 'package:lichess_mobile/src/model/common/http.dart'; @@ -34,26 +35,39 @@ class GameRepository { } } - Future> getUserGames( + Future> getUserGames( UserId userId, { int? max = 20, DateTime? until, }) { - return client.readNdJsonList( - Uri( - path: '/api/games/user/$userId', - queryParameters: { - 'max': max.toString(), - if (until != null) 'until': until.millisecondsSinceEpoch.toString(), - 'moves': 'false', - 'lastFen': 'true', - 'accuracy': 'true', - 'opening': 'true', - }, - ), - headers: {'Accept': 'application/x-ndjson'}, - mapper: LightArchivedGame.fromServerJson, - ); + return client + .readNdJsonList( + Uri( + path: '/api/games/user/$userId', + queryParameters: { + 'max': max.toString(), + if (until != null) + 'until': until.millisecondsSinceEpoch.toString(), + 'moves': 'false', + 'lastFen': 'true', + 'accuracy': 'true', + 'opening': 'true', + }, + ), + headers: {'Accept': 'application/x-ndjson'}, + mapper: LightArchivedGame.fromServerJson, + ) + .then( + (value) => value + .map( + (e) => ( + game: e, + // we know here user is not null for at least one of the players + pov: e.white.user?.id == userId ? Side.white : Side.black, + ), + ) + .toIList(), + ); } /// Returns the games of the current user, given a list of ids. diff --git a/lib/src/model/game/game_repository_providers.dart b/lib/src/model/game/game_repository_providers.dart index 19f83803ee..6bf4a77c70 100644 --- a/lib/src/model/game/game_repository_providers.dart +++ b/lib/src/model/game/game_repository_providers.dart @@ -25,7 +25,7 @@ Future archivedGame( } @riverpod -Future> userRecentGames( +Future> userRecentGames( UserRecentGamesRef ref, { required UserId userId, }) { diff --git a/lib/src/model/game/game_storage.dart b/lib/src/model/game/game_storage.dart index 8650009c17..c7f76ef59b 100644 --- a/lib/src/model/game/game_storage.dart +++ b/lib/src/model/game/game_storage.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; 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/game/archived_game.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -18,13 +17,6 @@ GameStorage gameStorage( return GameStorage(db); } -@riverpod -Future> recentStoredGames(RecentStoredGamesRef ref) async { - final session = ref.watch(authSessionProvider); - final storage = ref.watch(gameStorageProvider); - return storage.page(userId: session?.user.id); -} - const kGameStorageTable = 'game'; typedef StoredGame = ({ diff --git a/lib/src/model/user/user.dart b/lib/src/model/user/user.dart index c207bd3277..c7e12ffbcc 100644 --- a/lib/src/model/user/user.dart +++ b/lib/src/model/user/user.dart @@ -116,6 +116,11 @@ class User with _$User { }), ); } + + int get totalGames => perfs.values.fold( + 0, + (previousValue, element) => previousValue + (element.games ?? 0), + ); } @freezed @@ -138,11 +143,14 @@ class PlayTime with _$PlayTime { @freezed class UserPerf with _$UserPerf { + const UserPerf._(); + const factory UserPerf({ required int rating, required int ratingDeviation, required int progression, - required int numberOfGames, + int? games, + int? runs, bool? provisional, }) = _UserPerf; @@ -153,7 +161,8 @@ class UserPerf with _$UserPerf { rating: pick('rating').asIntOrThrow(), ratingDeviation: pick('rd').asIntOrThrow(), progression: pick('prog').asIntOrThrow(), - numberOfGames: pick('games').asIntOrThrow(), + games: pick('games').asIntOrNull(), + runs: pick('runs').asIntOrNull(), provisional: pick('prov').asBoolOrNull(), ); @@ -161,9 +170,11 @@ class UserPerf with _$UserPerf { rating: UserActivityStreak.fromJson(json).score, ratingDeviation: 0, progression: 0, - numberOfGames: UserActivityStreak.fromJson(json).runs, + runs: UserActivityStreak.fromJson(json).runs, provisional: null, ); + + int get numberOfGamesOrRuns => games ?? runs ?? 0; } @freezed diff --git a/lib/src/view/game/game_list_tile.dart b/lib/src/view/game/game_list_tile.dart index 989849583a..f772850183 100644 --- a/lib/src/view/game/game_list_tile.dart +++ b/lib/src/view/game/game_list_tile.dart @@ -414,12 +414,12 @@ class _ContextMenu extends ConsumerWidget { class ExtendedGameListTile extends StatelessWidget { const ExtendedGameListTile({required this.item, this.userId}); - final (LightArchivedGame, Side) item; + final LightArchivedGameWithPov item; final UserId? userId; @override Widget build(BuildContext context) { - final (game, youAre) = item; + final (game: game, pov: youAre) = item; final me = youAre == Side.white ? game.white : game.black; final opponent = youAre == Side.white ? game.black : game.white; diff --git a/lib/src/view/game/lobby_screen.dart b/lib/src/view/game/lobby_screen.dart index 1becde68ef..8b71afd518 100644 --- a/lib/src/view/game/lobby_screen.dart +++ b/lib/src/view/game/lobby_screen.dart @@ -1,9 +1,8 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/game/game_storage.dart'; +import 'package:lichess_mobile/src/model/game/game_history.dart'; import 'package:lichess_mobile/src/model/lobby/create_game_service.dart'; import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; import 'package:lichess_mobile/src/navigation.dart'; @@ -68,8 +67,7 @@ class _LobbyScreenState extends ConsumerState with RouteAware { void didPop() { super.didPop(); if (mounted) { - ref.invalidate(accountRecentGamesProvider); - ref.invalidate(recentStoredGamesProvider); + ref.invalidate(myRecentGamesProvider); } } diff --git a/lib/src/view/home/home_tab_screen.dart b/lib/src/view/home/home_tab_screen.dart index 2df92734d3..1b1a3d6b4b 100644 --- a/lib/src/view/home/home_tab_screen.dart +++ b/lib/src/view/home/home_tab_screen.dart @@ -8,7 +8,7 @@ import 'package:lichess_mobile/src/model/account/ongoing_game.dart'; import 'package:lichess_mobile/src/model/auth/auth_controller.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_game_storage.dart'; -import 'package:lichess_mobile/src/model/game/game_storage.dart'; +import 'package:lichess_mobile/src/model/game/game_history.dart'; import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; import 'package:lichess_mobile/src/model/lobby/game_setup.dart'; import 'package:lichess_mobile/src/navigation.dart'; @@ -186,8 +186,7 @@ class _HomeScreenState extends ConsumerState with RouteAware { Future _refreshData() { return Future.wait([ - ref.refresh(accountRecentGamesProvider.future), - ref.refresh(recentStoredGamesProvider.future), + ref.refresh(myRecentGamesProvider.future), ref.refresh(ongoingGamesProvider.future), ]); } @@ -220,18 +219,14 @@ class _HomeBody extends ConsumerWidget { data: (status) { final session = ref.watch(authSessionProvider); final isTablet = isTabletOrLarger(context); - final emptyRecent = ref.watch(accountRecentGamesProvider).maybeWhen( - data: (data) => data.isEmpty, - orElse: () => false, - ); - final emptyStored = ref.watch(recentStoredGamesProvider).maybeWhen( + final emptyRecent = ref.watch(myRecentGamesProvider).maybeWhen( data: (data) => data.isEmpty, orElse: () => false, ); // Show the welcome screen if there are no recent games and no stored games // (i.e. first installation, or the user has never played a game) - if (emptyRecent && emptyStored) { + if (emptyRecent) { final welcomeWidgets = [ Padding( padding: Styles.horizontalBodyPadding, diff --git a/lib/src/view/user/full_games_screen.dart b/lib/src/view/user/full_games_screen.dart index 2b1842770c..948f27ea8a 100644 --- a/lib/src/view/user/full_games_screen.dart +++ b/lib/src/view/user/full_games_screen.dart @@ -9,16 +9,20 @@ import 'package:lichess_mobile/src/view/game/game_list_tile.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; -class FullGameScreen extends StatelessWidget { +class FullGameScreen extends ConsumerWidget { const FullGameScreen({required this.user, super.key}); final LightUser user; @override - Widget build(BuildContext context) { - return PlatformWidget(androidBuilder: _buildAndroid, iosBuilder: _buildIos); + Widget build(BuildContext context, WidgetRef ref) { + return ConsumerPlatformWidget( + ref: ref, + androidBuilder: _buildAndroid, + iosBuilder: _buildIos, + ); } - Widget _buildIos(BuildContext context) { + Widget _buildIos(BuildContext context, WidgetRef ref) { return CupertinoPageScaffold( navigationBar: const CupertinoNavigationBar( middle: Text('Full Game History'), @@ -27,7 +31,7 @@ class FullGameScreen extends StatelessWidget { ); } - Widget _buildAndroid(BuildContext context) { + Widget _buildAndroid(BuildContext context, WidgetRef ref) { return Scaffold( appBar: AppBar( title: const Text('Full Game History'), diff --git a/lib/src/view/user/perf_cards.dart b/lib/src/view/user/perf_cards.dart index f3fb288ccf..5f711fc481 100644 --- a/lib/src/view/user/perf_cards.dart +++ b/lib/src/view/user/perf_cards.dart @@ -25,13 +25,13 @@ class PerfCards extends StatelessWidget { List userPerfs = Perf.values.where((element) { final p = user.perfs[element]; return p != null && - p.numberOfGames > 0 && + p.numberOfGamesOrRuns > 0 && p.ratingDeviation < kClueLessDeviation; }).toList(growable: false); userPerfs.sort( - (p1, p2) => user.perfs[p1]!.numberOfGames - .compareTo(user.perfs[p2]!.numberOfGames), + (p1, p2) => user.perfs[p1]!.numberOfGamesOrRuns + .compareTo(user.perfs[p2]!.numberOfGamesOrRuns), ); userPerfs = userPerfs.reversed.toList(); diff --git a/lib/src/view/user/perf_stats_screen.dart b/lib/src/view/user/perf_stats_screen.dart index 91df7f59fb..fa247f316b 100644 --- a/lib/src/view/user/perf_stats_screen.dart +++ b/lib/src/view/user/perf_stats_screen.dart @@ -93,7 +93,8 @@ class _Title extends StatelessWidget { } final p = user.perfs[element]; return p != null && - p.numberOfGames > 0 && + p.games != null && + p.games! > 0 && p.ratingDeviation < kClueLessDeviation; }).toList(growable: false); return AppBarTextButton( diff --git a/lib/src/view/user/recent_games.dart b/lib/src/view/user/recent_games.dart index a52d42b3a2..7b43d0a730 100644 --- a/lib/src/view/user/recent_games.dart +++ b/lib/src/view/user/recent_games.dart @@ -1,14 +1,10 @@ -import 'package:dartchess/dartchess.dart'; -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/model/game/game_history.dart'; import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; -import 'package:lichess_mobile/src/model/game/game_storage.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/game/game_list_tile.dart'; @@ -24,40 +20,12 @@ class RecentGames extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final connectivity = ref.watch(connectivityProvider); final session = ref.watch(authSessionProvider); final userId = user?.id ?? session?.user.id; final recentGames = user != null - ? ref.watch(userRecentGamesProvider(userId: user!.id)).whenData((data) { - return data - .map( - (e) => - // user is not null for at least one of the players - (e, e.white.user?.id == userId ? Side.white : Side.black), - ) - .toIList(); - }) - : session != null && - (connectivity.valueOrNull?.isOnline ?? false) == true - ? ref.watch(accountRecentGamesProvider).whenData((data) { - return data - .map( - (e) => ( - e, - // user is not null for at least one of the players - e.white.user?.id == userId ? Side.white : Side.black - ), - ) - .toIList(); - }) - : ref.watch(recentStoredGamesProvider).whenData((data) { - return data - // we can assume that `youAre` is not null either for logged - // in users or for anonymous users - .map((e) => (e.game.data, e.game.youAre ?? Side.white)) - .toIList(); - }); + ? ref.watch(userRecentGamesProvider(userId: user!.id)) + : ref.watch(myRecentGamesProvider); return recentGames.when( data: (data) { diff --git a/lib/src/widgets/user_list_tile.dart b/lib/src/widgets/user_list_tile.dart index db9c7ee700..6e6690eb1c 100644 --- a/lib/src/widgets/user_list_tile.dart +++ b/lib/src/widgets/user_list_tile.dart @@ -124,14 +124,16 @@ class _UserRating extends StatelessWidget { List userPerfs = Perf.values.where((element) { final p = perfs[element]; return p != null && - p.numberOfGames > 0 && + p.numberOfGamesOrRuns > 0 && p.ratingDeviation < kClueLessDeviation; }).toList(growable: false); if (userPerfs.isEmpty) return const SizedBox.shrink(); userPerfs.sort( - (p1, p2) => perfs[p1]!.numberOfGames.compareTo(perfs[p2]!.numberOfGames), + (p1, p2) => perfs[p1]! + .numberOfGamesOrRuns + .compareTo(perfs[p2]!.numberOfGamesOrRuns), ); userPerfs = userPerfs.reversed.toList(); diff --git a/test/model/auth/fake_auth_repository.dart b/test/model/auth/fake_auth_repository.dart index 2fd048b557..c82b5b48da 100644 --- a/test/model/auth/fake_auth_repository.dart +++ b/test/model/auth/fake_auth_repository.dart @@ -32,5 +32,5 @@ const _fakePerf = UserPerf( rating: 1500, ratingDeviation: 0, progression: 0, - numberOfGames: 0, + games: 0, ); diff --git a/test/model/game/game_repository_test.dart b/test/model/game/game_repository_test.dart index 4abdcf398f..1d5311e424 100644 --- a/test/model/game/game_repository_test.dart +++ b/test/model/game/game_repository_test.dart @@ -32,9 +32,9 @@ void main() { final repo = GameRepository(client); final result = await repo.getUserGames(const UserId('testUser')); - expect(result, isA>()); + expect(result, isA>()); expect(result.length, 3); - expect(result[0].id, const GameId('Huk88k3D')); + expect(result[0].game.id, const GameId('Huk88k3D')); }); }); From 562b9efe6b0ad7c599a101683e76f0268c1ef5c4 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 29 May 2024 11:31:52 +0200 Subject: [PATCH 25/34] Show total number of games --- lib/src/model/game/game_history.dart | 19 +-- lib/src/model/game/game_storage.dart | 11 ++ lib/src/model/user/user.dart | 8 +- lib/src/view/user/full_games_screen.dart | 148 ++++++++++++++++------- lib/src/view/user/recent_games.dart | 7 +- test/model/game/mock_game_storage.dart | 5 + 6 files changed, 139 insertions(+), 59 deletions(-) diff --git a/lib/src/model/game/game_history.dart b/lib/src/model/game/game_history.dart index deff4fcf93..e0e24dc714 100644 --- a/lib/src/model/game/game_history.dart +++ b/lib/src/model/game/game_history.dart @@ -49,14 +49,15 @@ class UserGameHistory extends _$UserGameHistory { final _list = []; @override - Future build(UserId? userId) async { + Future build( + UserId? userId, { + required bool isOnline, + }) async { ref.cacheFor(const Duration(minutes: 30)); ref.onDispose(() { _list.clear(); }); - final connectivity = ref.watch(connectivityProvider); - final online = connectivity.valueOrNull?.isOnline ?? false; final session = ref.watch(authSessionProvider); final recentGames = userId != null @@ -70,7 +71,7 @@ class UserGameHistory extends _$UserGameHistory { isLoading: false, hasMore: true, hasError: false, - online: online, + online: isOnline, session: session, ); } @@ -104,10 +105,12 @@ class UserGameHistory extends _$UserGameHistory { (value) => value // we can assume that `youAre` is not null either for logged // in users or for anonymous users - .map((e) => ( - game: e.game.data, - pov: e.game.youAre ?? Side.white - )) + .map( + (e) => ( + game: e.game.data, + pov: e.game.youAre ?? Side.white + ), + ) .toIList(), ), ).fold( diff --git a/lib/src/model/game/game_storage.dart b/lib/src/model/game/game_storage.dart index c7f76ef59b..7d957682d0 100644 --- a/lib/src/model/game/game_storage.dart +++ b/lib/src/model/game/game_storage.dart @@ -29,6 +29,17 @@ class GameStorage { const GameStorage(this._db); final Database _db; + Future count({ + UserId? userId, + }) async { + final list = await _db.query( + kGameStorageTable, + where: 'userId = ?', + whereArgs: [userId ?? kStorageAnonId], + ); + return list.length; + } + Future> page({ UserId? userId, DateTime? until, diff --git a/lib/src/model/user/user.dart b/lib/src/model/user/user.dart index c7e12ffbcc..1c4fcba329 100644 --- a/lib/src/model/user/user.dart +++ b/lib/src/model/user/user.dart @@ -117,10 +117,10 @@ class User with _$User { ); } - int get totalGames => perfs.values.fold( - 0, - (previousValue, element) => previousValue + (element.games ?? 0), - ); + int get totalGames => perfs.entries.fold(0, (previousValue, element) { + return previousValue + + (element.key != Perf.puzzle ? element.value.games ?? 0 : 0); + }); } @freezed diff --git a/lib/src/view/user/full_games_screen.dart b/lib/src/view/user/full_games_screen.dart index 948f27ea8a..703193fa90 100644 --- a/lib/src/view/user/full_games_screen.dart +++ b/lib/src/view/user/full_games_screen.dart @@ -2,16 +2,38 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/account/account_repository.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/game/game_history.dart'; +import 'package:lichess_mobile/src/model/game/game_storage.dart'; 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/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/game/game_list_tile.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'full_games_screen.g.dart'; + +@riverpod +Future _userNumberOfGames( + _UserNumberOfGamesRef ref, + LightUser? user, { + required bool isOnline, +}) async { + final session = ref.watch(authSessionProvider); + return user != null + ? ref.watch(userProvider(id: user.id).selectAsync((u) => u.totalGames)) + : session != null && isOnline + ? ref.watch(accountProvider.selectAsync((u) => u?.totalGames ?? 0)) + : ref.watch(gameStorageProvider).count(userId: user?.id); +} class FullGameScreen extends ConsumerWidget { - const FullGameScreen({required this.user, super.key}); - final LightUser user; + const FullGameScreen({required this.user, required this.isOnline, super.key}); + final LightUser? user; + final bool isOnline; @override Widget build(BuildContext context, WidgetRef ref) { @@ -23,27 +45,44 @@ class FullGameScreen extends ConsumerWidget { } Widget _buildIos(BuildContext context, WidgetRef ref) { + final nbGamesAsync = ref.watch( + _userNumberOfGamesProvider(user, isOnline: isOnline), + ); return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar( - middle: Text('Full Game History'), + navigationBar: CupertinoNavigationBar( + middle: nbGamesAsync.when( + data: (nbGames) => Text(context.l10n.nbGames(nbGames)), + loading: () => const CupertinoActivityIndicator(), + error: (e, s) => const Text('All Games'), + ), ), - child: _Body(userId: user.id), + child: _Body(user: user, isOnline: isOnline), ); } Widget _buildAndroid(BuildContext context, WidgetRef ref) { + final nbGamesAsync = ref.watch( + _userNumberOfGamesProvider(user, isOnline: isOnline), + ); return Scaffold( appBar: AppBar( - title: const Text('Full Game History'), + title: nbGamesAsync.when( + data: (nbGames) => Text(context.l10n.nbGames(nbGames)), + loading: () => const ButtonLoadingIndicator(), + error: (e, s) => const Text('All Games'), + ), ), - body: _Body(userId: user.id), + body: _Body(user: user, isOnline: isOnline), ); } } class _Body extends ConsumerStatefulWidget { - const _Body({required this.userId}); - final UserId userId; + const _Body({required this.user, required this.isOnline}); + + final LightUser? user; + final bool isOnline; + @override ConsumerState<_Body> createState() => _BodyState(); } @@ -70,56 +109,73 @@ class _BodyState extends ConsumerState<_Body> { if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) { if (_hasMore && !_isLoading) { - ref.read(userGameHistoryProvider(widget.userId).notifier).getNext(); + ref + .read( + userGameHistoryProvider( + widget.user?.id, + isOnline: widget.isOnline, + ).notifier, + ) + .getNext(); } } } @override Widget build(BuildContext context) { - final gameListState = ref.watch(userGameHistoryProvider(widget.userId)); + final gameListState = ref.watch( + userGameHistoryProvider(widget.user?.id, isOnline: widget.isOnline), + ); return gameListState.when( data: (state) { _hasMore = state.hasMore; _isLoading = state.isLoading; - if (state.hasError) { - showPlatformSnackbar( - context, - 'Error loading Game History', - type: SnackBarType.error, - ); - } final list = state.gameList; - return ListView.builder( - controller: _scrollController, - itemCount: list.length + (state.isLoading ? 1 : 0), - itemBuilder: (context, index) { - if (state.isLoading && index == list.length) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: 32.0), - child: CenterLoadingIndicator(), - ); - } - - return Slidable( - endActionPane: const ActionPane( - motion: ScrollMotion(), - children: [ - SlidableAction( - onPressed: null, - icon: Icons.bookmark_add_outlined, - label: 'Bookmark', + + return SafeArea( + child: ListView.builder( + controller: _scrollController, + itemCount: list.length + (state.isLoading ? 1 : 0), + itemBuilder: (context, index) { + if (state.isLoading && index == list.length) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 32.0), + child: CenterLoadingIndicator(), + ); + } else if (state.hasError && + state.hasMore && + index == list.length) { + // TODO: add a retry button + return const Padding( + padding: EdgeInsets.symmetric(vertical: 32.0), + child: Center( + child: Text( + 'Could not load more games', + ), ), - ], - ), - child: ExtendedGameListTile( - item: list[index], - userId: widget.userId, - ), - ); - }, + ); + } + + return Slidable( + endActionPane: const ActionPane( + motion: ScrollMotion(), + children: [ + SlidableAction( + onPressed: null, + icon: Icons.bookmark_add_outlined, + label: 'Bookmark', + ), + ], + ), + child: ExtendedGameListTile( + item: list[index], + userId: widget.user?.id, + ), + ); + }, + ), ); }, error: (e, s) { diff --git a/lib/src/view/user/recent_games.dart b/lib/src/view/user/recent_games.dart index 7b43d0a730..a0942899e7 100644 --- a/lib/src/view/user/recent_games.dart +++ b/lib/src/view/user/recent_games.dart @@ -5,6 +5,7 @@ import 'package:lichess_mobile/src/model/game/game_history.dart'; import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/game/game_list_tile.dart'; @@ -20,6 +21,7 @@ class RecentGames extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final connectivity = ref.watch(connectivityProvider); final session = ref.watch(authSessionProvider); final userId = user?.id ?? session?.user.id; @@ -41,7 +43,10 @@ class RecentGames extends ConsumerWidget { onPressed: () { pushPlatformRoute( context, - builder: (context) => FullGameScreen(user: u), + builder: (context) => FullGameScreen( + user: u, + isOnline: connectivity.valueOrNull?.isOnline == true, + ), ); }, child: Text( diff --git a/test/model/game/mock_game_storage.dart b/test/model/game/mock_game_storage.dart index 7107e8002c..0ed719063f 100644 --- a/test/model/game/mock_game_storage.dart +++ b/test/model/game/mock_game_storage.dart @@ -27,4 +27,9 @@ class MockGameStorage implements GameStorage { Future save(ArchivedGame game) { return Future.value(); } + + @override + Future count({UserId? userId}) { + return Future.value(0); + } } From fb276e96bb6a354664b07c182de0a00786733435 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 29 May 2024 11:35:32 +0200 Subject: [PATCH 26/34] Retrict context menu analysis access to supported variants --- lib/src/view/game/game_list_tile.dart | 32 ++++++++++++++------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/lib/src/view/game/game_list_tile.dart b/lib/src/view/game/game_list_tile.dart index f772850183..9523436c50 100644 --- a/lib/src/view/game/game_list_tile.dart +++ b/lib/src/view/game/game_list_tile.dart @@ -218,21 +218,23 @@ class _ContextMenu extends ConsumerWidget { ), BottomSheetContextMenuAction( icon: Icons.biotech, - onPressed: () { - pushPlatformRoute( - context, - builder: (context) => AnalysisScreen( - title: context.l10n.gameAnalysis, - pgnOrId: game.id.value, - options: AnalysisOptions( - isLocalEvaluationAllowed: true, - variant: game.variant, - orientation: orientation, - id: game.id, - ), - ), - ); - }, + onPressed: game.variant.isSupported + ? () { + pushPlatformRoute( + context, + builder: (context) => AnalysisScreen( + title: context.l10n.gameAnalysis, + pgnOrId: game.id.value, + options: AnalysisOptions( + isLocalEvaluationAllowed: true, + variant: game.variant, + orientation: orientation, + id: game.id, + ), + ), + ); + } + : null, child: Text(context.l10n.gameAnalysis), ), BottomSheetContextMenuAction( From 15ac0d5c786e36d8ac2bd9bccfd6981dc4b9281c Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 29 May 2024 12:04:20 +0200 Subject: [PATCH 27/34] Remove bookmark action for now, refactor --- lib/src/model/game/game_history.dart | 26 +++++++++- .../model/game/game_repository_providers.dart | 16 ------ lib/src/view/account/profile_screen.dart | 4 +- lib/src/view/home/home_tab_screen.dart | 4 +- ...s_screen.dart => game_history_screen.dart} | 50 +++++++++---------- lib/src/view/user/recent_games.dart | 12 +++-- lib/src/view/user/user_screen.dart | 2 +- 7 files changed, 62 insertions(+), 52 deletions(-) rename lib/src/view/user/{full_games_screen.dart => game_history_screen.dart} (84%) diff --git a/lib/src/model/game/game_history.dart b/lib/src/model/game/game_history.dart index e0e24dc714..833e8ddfd1 100644 --- a/lib/src/model/game/game_history.dart +++ b/lib/src/model/game/game_history.dart @@ -9,7 +9,6 @@ import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; import 'package:lichess_mobile/src/model/game/game_repository.dart'; -import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; import 'package:lichess_mobile/src/model/game/game_storage.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/riverpod.dart'; @@ -21,6 +20,11 @@ part 'game_history.g.dart'; const _nbPerPage = 20; +/// A provider that fetches the current app user's recent games. +/// +/// 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> myRecentGames(MyRecentGamesRef ref) { final connectivity = ref.watch(connectivityProvider); @@ -44,6 +48,24 @@ Future> myRecentGames(MyRecentGamesRef ref) { } } +/// A provider that fetches the recent games from the server for a given user. +@riverpod +Future> userRecentGames( + UserRecentGamesRef ref, { + required UserId userId, +}) { + return ref.withClientCacheFor( + (client) => GameRepository(client).getUserGames(userId), + // cache is important because the associated widget is in a [ListView] and + // the provider may be instanciated multiple times in a short period of time + // (e.g. when scrolling) + // TODO: consider debouncing the request instead of caching it, or make the + // request in the parent widget and pass the result to the child + const Duration(minutes: 1), + ); +} + +/// A provider that fetches the game history for a given user, or the current app user if no user is provided. @riverpod class UserGameHistory extends _$UserGameHistory { final _list = []; @@ -53,7 +75,7 @@ class UserGameHistory extends _$UserGameHistory { UserId? userId, { required bool isOnline, }) async { - ref.cacheFor(const Duration(minutes: 30)); + ref.cacheFor(const Duration(minutes: 5)); ref.onDispose(() { _list.clear(); }); diff --git a/lib/src/model/game/game_repository_providers.dart b/lib/src/model/game/game_repository_providers.dart index 6bf4a77c70..dded6e0df1 100644 --- a/lib/src/model/game/game_repository_providers.dart +++ b/lib/src/model/game/game_repository_providers.dart @@ -24,22 +24,6 @@ Future archivedGame( ); } -@riverpod -Future> userRecentGames( - UserRecentGamesRef ref, { - required UserId userId, -}) { - return ref.withClientCacheFor( - (client) => GameRepository(client).getUserGames(userId), - // cache is important because the associated widget is in a [ListView] and - // the provider may be instanciated multiple times in a short period of time - // (e.g. when scrolling) - // TODO: consider debouncing the request instead of caching it, or make the - // request in the parent widget and pass the result to the child - const Duration(minutes: 1), - ); -} - @riverpod Future> gamesById( GamesByIdRef ref, { diff --git a/lib/src/view/account/profile_screen.dart b/lib/src/view/account/profile_screen.dart index 47f47f85ae..e437bd911c 100644 --- a/lib/src/view/account/profile_screen.dart +++ b/lib/src/view/account/profile_screen.dart @@ -62,7 +62,7 @@ class ProfileScreen extends ConsumerWidget { UserProfile(user: user), const _PerfCards(), const UserActivityWidget(), - const RecentGames(), + const RecentGamesWidget(), ], ); }, @@ -110,7 +110,7 @@ class ProfileScreen extends ConsumerWidget { UserProfile(user: user), const _PerfCards(), const UserActivityWidget(), - const RecentGames(), + const RecentGamesWidget(), ], ), ); diff --git a/lib/src/view/home/home_tab_screen.dart b/lib/src/view/home/home_tab_screen.dart index 1b1a3d6b4b..402c8f9ec2 100644 --- a/lib/src/view/home/home_tab_screen.dart +++ b/lib/src/view/home/home_tab_screen.dart @@ -334,7 +334,7 @@ class _HomeBody extends ConsumerWidget { mainAxisAlignment: MainAxisAlignment.start, children: [ SizedBox(height: 8.0), - RecentGames(), + RecentGamesWidget(), ], ), ), @@ -347,7 +347,7 @@ class _HomeBody extends ConsumerWidget { const _OngoingGamesCarousel(maxGamesToShow: 20) else const _OfflineCorrespondenceCarousel(maxGamesToShow: 20), - const RecentGames(), + const RecentGamesWidget(), if (Theme.of(context).platform == TargetPlatform.iOS) const SizedBox(height: 70.0) else diff --git a/lib/src/view/user/full_games_screen.dart b/lib/src/view/user/game_history_screen.dart similarity index 84% rename from lib/src/view/user/full_games_screen.dart rename to lib/src/view/user/game_history_screen.dart index 703193fa90..db9db3ad39 100644 --- a/lib/src/view/user/full_games_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -1,7 +1,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/game/game_history.dart'; @@ -14,7 +13,7 @@ import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'full_games_screen.g.dart'; +part 'game_history_screen.g.dart'; @riverpod Future _userNumberOfGames( @@ -30,8 +29,12 @@ Future _userNumberOfGames( : ref.watch(gameStorageProvider).count(userId: user?.id); } -class FullGameScreen extends ConsumerWidget { - const FullGameScreen({required this.user, required this.isOnline, super.key}); +class GameHistoryScreen extends ConsumerWidget { + const GameHistoryScreen({ + required this.user, + required this.isOnline, + super.key, + }); final LightUser? user; final bool isOnline; @@ -89,8 +92,6 @@ class _Body extends ConsumerStatefulWidget { class _BodyState extends ConsumerState<_Body> { final ScrollController _scrollController = ScrollController(); - bool _hasMore = true; - bool _isLoading = false; @override void initState() { @@ -108,7 +109,21 @@ class _BodyState extends ConsumerState<_Body> { void _scrollListener() { if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) { - if (_hasMore && !_isLoading) { + final state = ref.read( + userGameHistoryProvider( + widget.user?.id, + isOnline: widget.isOnline, + ), + ); + + if (!state.hasValue) { + return; + } + + final hasMore = state.requireValue.hasMore; + final isLoading = state.requireValue.isLoading; + + if (hasMore && !isLoading) { ref .read( userGameHistoryProvider( @@ -129,9 +144,6 @@ class _BodyState extends ConsumerState<_Body> { return gameListState.when( data: (state) { - _hasMore = state.hasMore; - _isLoading = state.isLoading; - final list = state.gameList; return SafeArea( @@ -158,21 +170,9 @@ class _BodyState extends ConsumerState<_Body> { ); } - return Slidable( - endActionPane: const ActionPane( - motion: ScrollMotion(), - children: [ - SlidableAction( - onPressed: null, - icon: Icons.bookmark_add_outlined, - label: 'Bookmark', - ), - ], - ), - child: ExtendedGameListTile( - item: list[index], - userId: widget.user?.id, - ), + return ExtendedGameListTile( + item: list[index], + userId: widget.user?.id, ); }, ), diff --git a/lib/src/view/user/recent_games.dart b/lib/src/view/user/recent_games.dart index a0942899e7..240fdbd359 100644 --- a/lib/src/view/user/recent_games.dart +++ b/lib/src/view/user/recent_games.dart @@ -9,13 +9,17 @@ import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/game/game_list_tile.dart'; -import 'package:lichess_mobile/src/view/user/full_games_screen.dart'; +import 'package:lichess_mobile/src/view/user/game_history_screen.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; -class RecentGames extends ConsumerWidget { - const RecentGames({this.user, super.key}); +/// A widget that show a list of recent games for a given player or the current user. +/// +/// If [user] is not provided, the current logged in user's recent games are displayed. +/// If the current user is not logged in, or there is no connectivity, the stored recent games are displayed instead. +class RecentGamesWidget extends ConsumerWidget { + const RecentGamesWidget({this.user, super.key}); final LightUser? user; @@ -43,7 +47,7 @@ class RecentGames extends ConsumerWidget { onPressed: () { pushPlatformRoute( context, - builder: (context) => FullGameScreen( + builder: (context) => GameHistoryScreen( user: u, isOnline: connectivity.valueOrNull?.isOnline == true, ), diff --git a/lib/src/view/user/user_screen.dart b/lib/src/view/user/user_screen.dart index a5bd77e130..da1bf8d27f 100644 --- a/lib/src/view/user/user_screen.dart +++ b/lib/src/view/user/user_screen.dart @@ -119,7 +119,7 @@ class _UserProfileListView extends StatelessWidget { UserProfile(user: user), PerfCards(user: user, isMe: false), UserActivityWidget(user: user), - RecentGames(user: user.lightUser), + RecentGamesWidget(user: user.lightUser), ], ); } From 5cb6b541167d3fc758929ffa8a466ee0b178f424 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 29 May 2024 12:20:32 +0200 Subject: [PATCH 28/34] Use correct game count --- lib/src/model/user/user.dart | 43 ++++++++++++++++++++-- lib/src/view/user/game_history_screen.dart | 6 ++- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/lib/src/model/user/user.dart b/lib/src/model/user/user.dart index 1c4fcba329..c6245724da 100644 --- a/lib/src/model/user/user.dart +++ b/lib/src/model/user/user.dart @@ -78,6 +78,7 @@ class User with _$User { required IMap perfs, PlayTime? playTime, Profile? profile, + UserGameCount? count, }) = _User; LightUser get lightUser => LightUser( @@ -106,6 +107,7 @@ class User with _$User { seenAt: pick('seenAt').asDateTimeFromMillisecondsOrNull(), playTime: pick('playTime').letOrNull(PlayTime.fromPick), profile: pick('profile').letOrNull(Profile.fromPick), + count: pick('count').letOrNull(UserGameCount.fromPick), perfs: IMap({ for (final entry in receivedPerfsMap.entries) if (Perf.nameMap.containsKey(entry.key)) @@ -116,11 +118,44 @@ class User with _$User { }), ); } +} - int get totalGames => perfs.entries.fold(0, (previousValue, element) { - return previousValue + - (element.key != Perf.puzzle ? element.value.games ?? 0 : 0); - }); +@freezed +class UserGameCount with _$UserGameCount { + const factory UserGameCount({ + required int all, + required int rated, + required int ai, + required int draw, + required int drawH, + required int win, + required int winH, + required int loss, + required int lossH, + required int bookmark, + required int playing, + required int import, + required int me, + }) = _UserGameCount; + + factory UserGameCount.fromJson(Map json) => + UserGameCount.fromPick(pick(json).required()); + + factory UserGameCount.fromPick(RequiredPick pick) => UserGameCount( + all: pick('all').asIntOrThrow(), + rated: pick('rated').asIntOrThrow(), + ai: pick('ai').asIntOrThrow(), + draw: pick('draw').asIntOrThrow(), + drawH: pick('drawH').asIntOrThrow(), + win: pick('win').asIntOrThrow(), + winH: pick('winH').asIntOrThrow(), + loss: pick('loss').asIntOrThrow(), + lossH: pick('lossH').asIntOrThrow(), + bookmark: pick('bookmark').asIntOrThrow(), + playing: pick('playing').asIntOrThrow(), + import: pick('import').asIntOrThrow(), + me: pick('me').asIntOrThrow(), + ); } @freezed diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index db9db3ad39..ecc394247f 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -23,9 +23,11 @@ Future _userNumberOfGames( }) async { final session = ref.watch(authSessionProvider); return user != null - ? ref.watch(userProvider(id: user.id).selectAsync((u) => u.totalGames)) + ? ref.watch( + userProvider(id: user.id).selectAsync((u) => u.count?.all ?? 0), + ) : session != null && isOnline - ? ref.watch(accountProvider.selectAsync((u) => u?.totalGames ?? 0)) + ? ref.watch(accountProvider.selectAsync((u) => u?.count?.all ?? 0)) : ref.watch(gameStorageProvider).count(userId: user?.id); } From bd7534916ac610e3990690504077088e53c09ed7 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 29 May 2024 12:30:05 +0200 Subject: [PATCH 29/34] Make max parameter mandatory --- lib/src/model/game/game_repository.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/model/game/game_repository.dart b/lib/src/model/game/game_repository.dart index 6d4705c59f..6834c4851d 100644 --- a/lib/src/model/game/game_repository.dart +++ b/lib/src/model/game/game_repository.dart @@ -37,7 +37,7 @@ class GameRepository { Future> getUserGames( UserId userId, { - int? max = 20, + int max = 20, DateTime? until, }) { return client From cbe7b4d7bf17b99596a48fb6bb94f3d546a1e406 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 29 May 2024 12:32:49 +0200 Subject: [PATCH 30/34] Remove unused import --- lib/src/view/user/recent_games.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/view/user/recent_games.dart b/lib/src/view/user/recent_games.dart index 240fdbd359..ee61ecdcb3 100644 --- a/lib/src/view/user/recent_games.dart +++ b/lib/src/view/user/recent_games.dart @@ -2,7 +2,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/game/game_history.dart'; -import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; From b37150a54a64be18266e4ec7330f9c13c5ff61bc Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 29 May 2024 13:02:20 +0200 Subject: [PATCH 31/34] Only show more button if number of games > recent games --- lib/src/model/game/game_history.dart | 33 ++++++++++++++++++++-- lib/src/view/user/game_history_screen.dart | 29 ++----------------- lib/src/view/user/recent_games.dart | 13 +++++++-- 3 files changed, 44 insertions(+), 31 deletions(-) diff --git a/lib/src/model/game/game_history.dart b/lib/src/model/game/game_history.dart index 833e8ddfd1..314de0f1f9 100644 --- a/lib/src/model/game/game_history.dart +++ b/lib/src/model/game/game_history.dart @@ -4,12 +4,15 @@ import 'package:async/async.dart'; import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; import 'package:lichess_mobile/src/model/game/game_repository.dart'; import 'package:lichess_mobile/src/model/game/game_storage.dart'; +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/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/riverpod.dart'; import 'package:result_extensions/result_extensions.dart'; @@ -18,6 +21,8 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'game_history.freezed.dart'; part 'game_history.g.dart'; +const kNumberOfRecentGames = 20; + const _nbPerPage = 20; /// A provider that fetches the current app user's recent games. @@ -32,13 +37,16 @@ Future> myRecentGames(MyRecentGamesRef ref) { final online = connectivity.valueOrNull?.isOnline ?? false; if (session != null && online) { return ref.withClientCacheFor( - (client) => GameRepository(client).getUserGames(session.user.id), + (client) => GameRepository(client) + .getUserGames(session.user.id, max: kNumberOfRecentGames), const Duration(hours: 1), ); } else { final storage = ref.watch(gameStorageProvider); ref.cacheFor(const Duration(hours: 1)); - return storage.page(userId: session?.user.id).then( + return storage + .page(userId: session?.user.id, max: kNumberOfRecentGames) + .then( (value) => value // we can assume that `youAre` is not null either for logged // in users or for anonymous users @@ -65,6 +73,27 @@ Future> userRecentGames( ); } +/// 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 userNumberOfGames( + UserNumberOfGamesRef ref, + LightUser? user, { + required bool isOnline, +}) async { + final session = ref.watch(authSessionProvider); + return user != null + ? ref.watch( + userProvider(id: user.id).selectAsync((u) => u.count?.all ?? 0), + ) + : session != null && isOnline + ? ref.watch(accountProvider.selectAsync((u) => u?.count?.all ?? 0)) + : ref.watch(gameStorageProvider).count(userId: user?.id); +} + /// A provider that fetches the game history for a given user, or the current app user if no user is provided. @riverpod class UserGameHistory extends _$UserGameHistory { diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index ecc394247f..3555baa437 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -1,35 +1,12 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/model/account/account_repository.dart'; -import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/game/game_history.dart'; -import 'package:lichess_mobile/src/model/game/game_storage.dart'; 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/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/game/game_list_tile.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'game_history_screen.g.dart'; - -@riverpod -Future _userNumberOfGames( - _UserNumberOfGamesRef ref, - LightUser? user, { - required bool isOnline, -}) async { - final session = ref.watch(authSessionProvider); - return user != null - ? ref.watch( - userProvider(id: user.id).selectAsync((u) => u.count?.all ?? 0), - ) - : session != null && isOnline - ? ref.watch(accountProvider.selectAsync((u) => u?.count?.all ?? 0)) - : ref.watch(gameStorageProvider).count(userId: user?.id); -} class GameHistoryScreen extends ConsumerWidget { const GameHistoryScreen({ @@ -51,7 +28,7 @@ class GameHistoryScreen extends ConsumerWidget { Widget _buildIos(BuildContext context, WidgetRef ref) { final nbGamesAsync = ref.watch( - _userNumberOfGamesProvider(user, isOnline: isOnline), + userNumberOfGamesProvider(user, isOnline: isOnline), ); return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( @@ -67,7 +44,7 @@ class GameHistoryScreen extends ConsumerWidget { Widget _buildAndroid(BuildContext context, WidgetRef ref) { final nbGamesAsync = ref.watch( - _userNumberOfGamesProvider(user, isOnline: isOnline), + userNumberOfGamesProvider(user, isOnline: isOnline), ); return Scaffold( appBar: AppBar( @@ -182,7 +159,7 @@ class _BodyState extends ConsumerState<_Body> { }, error: (e, s) { debugPrint( - 'SEVERE: [FullGameHistoryScreen] could not load game list', + 'SEVERE: [GameHistoryScreen] could not load game list', ); return const Center(child: Text('Could not load Game History')); }, diff --git a/lib/src/view/user/recent_games.dart b/lib/src/view/user/recent_games.dart index ee61ecdcb3..26c13a6d56 100644 --- a/lib/src/view/user/recent_games.dart +++ b/lib/src/view/user/recent_games.dart @@ -32,22 +32,29 @@ class RecentGamesWidget extends ConsumerWidget { ? ref.watch(userRecentGamesProvider(userId: user!.id)) : ref.watch(myRecentGamesProvider); + final nbOfGames = ref + .watch( + userNumberOfGamesProvider(user, + isOnline: connectivity.valueOrNull?.isOnline == true), + ) + .valueOrNull ?? + 0; + return recentGames.when( data: (data) { if (data.isEmpty) { return const SizedBox.shrink(); } - final u = user ?? ref.watch(authSessionProvider)?.user; return ListSection( header: Text(context.l10n.recentGames), hasLeading: true, - headerTrailing: u != null + headerTrailing: nbOfGames > data.length ? NoPaddingTextButton( onPressed: () { pushPlatformRoute( context, builder: (context) => GameHistoryScreen( - user: u, + user: user, isOnline: connectivity.valueOrNull?.isOnline == true, ), ); From 7b6fb8f1494519eb986346ddd1e0f72328e112e9 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 29 May 2024 13:08:56 +0200 Subject: [PATCH 32/34] Add missing trailing comma --- lib/src/view/user/recent_games.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/src/view/user/recent_games.dart b/lib/src/view/user/recent_games.dart index 26c13a6d56..8339263216 100644 --- a/lib/src/view/user/recent_games.dart +++ b/lib/src/view/user/recent_games.dart @@ -34,8 +34,10 @@ class RecentGamesWidget extends ConsumerWidget { final nbOfGames = ref .watch( - userNumberOfGamesProvider(user, - isOnline: connectivity.valueOrNull?.isOnline == true), + userNumberOfGamesProvider( + user, + isOnline: connectivity.valueOrNull?.isOnline == true, + ), ) .valueOrNull ?? 0; From c3a082ea0fa1c76602955cdc00a20320df91606c Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 30 May 2024 10:12:30 +0200 Subject: [PATCH 33/34] Tweak comment --- lib/src/model/game/game_history.dart | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/src/model/game/game_history.dart b/lib/src/model/game/game_history.dart index 314de0f1f9..3aa5cbeaa2 100644 --- a/lib/src/model/game/game_history.dart +++ b/lib/src/model/game/game_history.dart @@ -94,7 +94,10 @@ Future userNumberOfGames( : ref.watch(gameStorageProvider).count(userId: user?.id); } -/// A provider that fetches the game history for a given user, or the current app user if no user is provided. +/// 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 _list = []; @@ -102,6 +105,12 @@ class UserGameHistory extends _$UserGameHistory { @override Future 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, }) async { ref.cacheFor(const Duration(minutes: 5)); @@ -127,6 +136,7 @@ class UserGameHistory extends _$UserGameHistory { ); } + /// Fetches the next page of games. void getNext() { if (!state.hasValue) return; From d29afccdd5113a25164508333daf13b4af25c6e0 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 30 May 2024 10:21:28 +0200 Subject: [PATCH 34/34] Comment out game count fields that we don't use for now --- lib/src/model/user/user.dart | 50 +++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/lib/src/model/user/user.dart b/lib/src/model/user/user.dart index c6245724da..f4c0dcc6ab 100644 --- a/lib/src/model/user/user.dart +++ b/lib/src/model/user/user.dart @@ -124,18 +124,19 @@ class User with _$User { class UserGameCount with _$UserGameCount { const factory UserGameCount({ required int all, - required int rated, - required int ai, - required int draw, - required int drawH, - required int win, - required int winH, - required int loss, - required int lossH, - required int bookmark, - required int playing, - required int import, - required int me, + // TODO(#454): enable rest of fields when needed for filtering + // required int rated, + // required int ai, + // required int draw, + // required int drawH, + // required int win, + // required int winH, + // required int loss, + // required int lossH, + // required int bookmark, + // required int playing, + // required int imported, + // required int me, }) = _UserGameCount; factory UserGameCount.fromJson(Map json) => @@ -143,18 +144,19 @@ class UserGameCount with _$UserGameCount { factory UserGameCount.fromPick(RequiredPick pick) => UserGameCount( all: pick('all').asIntOrThrow(), - rated: pick('rated').asIntOrThrow(), - ai: pick('ai').asIntOrThrow(), - draw: pick('draw').asIntOrThrow(), - drawH: pick('drawH').asIntOrThrow(), - win: pick('win').asIntOrThrow(), - winH: pick('winH').asIntOrThrow(), - loss: pick('loss').asIntOrThrow(), - lossH: pick('lossH').asIntOrThrow(), - bookmark: pick('bookmark').asIntOrThrow(), - playing: pick('playing').asIntOrThrow(), - import: pick('import').asIntOrThrow(), - me: pick('me').asIntOrThrow(), + // TODO(#454): enable rest of fields when needed for filtering + // rated: pick('rated').asIntOrThrow(), + // ai: pick('ai').asIntOrThrow(), + // draw: pick('draw').asIntOrThrow(), + // drawH: pick('drawH').asIntOrThrow(), + // win: pick('win').asIntOrThrow(), + // winH: pick('winH').asIntOrThrow(), + // loss: pick('loss').asIntOrThrow(), + // lossH: pick('lossH').asIntOrThrow(), + // bookmark: pick('bookmark').asIntOrThrow(), + // playing: pick('playing').asIntOrThrow(), + // imported: pick('import').asIntOrThrow(), + // me: pick('me').asIntOrThrow(), ); }