Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bookmark games #1302

Closed
wants to merge 32 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
2d34caf
feat: bookmark in game list context menu
ZTL-UwU Jul 17, 2024
c58e691
feat: game history bookmark slidable
ZTL-UwU Jul 21, 2024
e8ef9b6
chore: remove unused import & var
ZTL-UwU Jul 21, 2024
b8ed607
chore: remove unused import
ZTL-UwU Jul 21, 2024
8083469
feat: bookmark button in game screens
ZTL-UwU Jul 21, 2024
0ccc01f
feat: bookmark action in game history slidable
ZTL-UwU Jul 21, 2024
56fccaa
feat: bookmark action in game analysis
ZTL-UwU Jul 21, 2024
f1e26e9
Merge branch 'main' into main
ZTL-UwU Aug 5, 2024
40ef91a
Merge upstream into bookmark-games
julien4215 Dec 23, 2024
52c9fce
Add bookmark translation
julien4215 Dec 24, 2024
50ccaf2
More work on game bookmark
julien4215 Dec 24, 2024
47878d7
Merge upstream into bookmark-games
julien4215 Jan 7, 2025
fa5ca9f
Add bookmark state on game history screen
julien4215 Jan 10, 2025
3c9bd14
Merge upstream into bookmark-games
julien4215 Jan 13, 2025
90511bb
Add a provider to know if user is logged in
julien4215 Jan 19, 2025
38b715d
Show an error message if bookmark fails
julien4215 Jan 19, 2025
42411a4
Simplify bookmark method in game repository
julien4215 Jan 19, 2025
8d1b28d
Use isLoggedIn provider instead of watching authSessionProvider
julien4215 Jan 20, 2025
1b7deb5
Remove bookmark button on analysis screen
julien4215 Jan 20, 2025
3a68579
Change color of slidable to match theme
julien4215 Jan 20, 2025
653a168
More work on bookmarks
julien4215 Jan 22, 2025
ea8dfc2
Merge upstream main branch into bookmark-games
julien4215 Jan 23, 2025
d1aa50e
Introduce a new bookmark provider to solve sync issues between providers
julien4215 Jan 23, 2025
f142db1
Add bookmark functionality to archived game screen and game screen
julien4215 Jan 23, 2025
95b6299
Rename provider and add a doc comment to the provider
julien4215 Jan 24, 2025
2c8bdfb
Tweak bookmarks
julien4215 Jan 24, 2025
b057274
Merge upstream into bookmark-games to get the cache removal of games
julien4215 Jan 29, 2025
762bfa2
Merge upstream to fix conflicts
julien4215 Feb 3, 2025
0c2f3a7
Use a drop down menu for bookmark on game screens
julien4215 Feb 3, 2025
a2e007b
Add bookmark to recent games widget
julien4215 Feb 5, 2025
2eb1d11
Merge upstream into bookmark-games for CI test
julien4215 Feb 5, 2025
01ba938
Tweak code
julien4215 Feb 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions lib/src/model/auth/auth_session.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/model/auth/session_storage.dart';
import 'package:lichess_mobile/src/model/common/preloaded_data.dart';
Expand Down Expand Up @@ -34,3 +35,8 @@ class AuthSessionState with _$AuthSessionState {

factory AuthSessionState.fromJson(Map<String, dynamic> json) => _$AuthSessionStateFromJson(json);
}

@riverpod
bool isLoggedIn(Ref ref) {
return ref.watch(authSessionProvider.select((authSession) => authSession != null));
}
19 changes: 12 additions & 7 deletions lib/src/model/game/archived_game.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ class ArchivedGame with _$ArchivedGame, BaseGame, IndexableSteps implements Base
///
/// Currently, those endpoints are supported:
/// - GET /game/export/<id>
factory ArchivedGame.fromServerJson(Map<String, dynamic> json) {
return _archivedGameFromPick(pick(json).required());
factory ArchivedGame.fromServerJson(Map<String, dynamic> json, {bool withBookmarked = false}) {
return _archivedGameFromPick(pick(json).required(), withBookmarked: withBookmarked);
}

/// Create an archived game from a local storage JSON.
Expand Down Expand Up @@ -99,10 +99,14 @@ class LightArchivedGame with _$LightArchivedGame {
@MoveConverter() Move? lastMove,
Side? winner,
ClockData? clock,
bool? bookmarked,
}) = _ArchivedGameData;

factory LightArchivedGame.fromServerJson(Map<String, dynamic> json) {
return _lightArchivedGameFromPick(pick(json).required());
factory LightArchivedGame.fromServerJson(
Map<String, dynamic> json, {
bool withBookmarked = false,
}) {
return _lightArchivedGameFromPick(pick(json).required(), withBookmarked: withBookmarked);
}

factory LightArchivedGame.fromJson(Map<String, dynamic> json) =>
Expand All @@ -129,8 +133,8 @@ IList<ExternalEval>? gameEvalsFromPick(RequiredPick pick) {
?.lock;
}

ArchivedGame _archivedGameFromPick(RequiredPick pick) {
final data = _lightArchivedGameFromPick(pick);
ArchivedGame _archivedGameFromPick(RequiredPick pick, {bool withBookmarked = false}) {
final data = _lightArchivedGameFromPick(pick, withBookmarked: withBookmarked);
final clocks = pick(
'clocks',
).asListOrNull<Duration>((p0) => Duration(milliseconds: p0.asIntOrThrow() * 10));
Expand Down Expand Up @@ -202,7 +206,7 @@ ArchivedGame _archivedGameFromPick(RequiredPick pick) {
);
}

LightArchivedGame _lightArchivedGameFromPick(RequiredPick pick) {
LightArchivedGame _lightArchivedGameFromPick(RequiredPick pick, {bool withBookmarked = false}) {
return LightArchivedGame(
id: pick('id').asGameIdOrThrow(),
fullId: pick('fullId').asGameFullIdOrNull(),
Expand All @@ -223,6 +227,7 @@ LightArchivedGame _lightArchivedGameFromPick(RequiredPick pick) {
lastMove: pick('lastMove').asUciMoveOrNull(),
clock: pick('clock').letOrNull(_clockDataFromPick),
opening: pick('opening').letOrNull(_openingFromPick),
bookmarked: withBookmarked ? pick('bookmarked').asBoolOrFalse() : null,
);
}

Expand Down
32 changes: 29 additions & 3 deletions lib/src/model/game/game_history.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ Future<IList<LightArchivedGameWithPov>> myRecentGames(Ref ref) async {
final session = ref.watch(authSessionProvider);
if (session != null && online) {
return ref.withClient(
(client) => GameRepository(client).getUserGames(session.user.id, max: kNumberOfRecentGames),
(client) => GameRepository(
client,
).getUserGames(session.user.id, max: kNumberOfRecentGames, withBookmarked: true),
);
} else {
final storage = await ref.watch(gameStorageProvider.future);
Expand All @@ -58,7 +60,9 @@ Future<IList<LightArchivedGameWithPov>> myRecentGames(Ref ref) async {
/// A provider that fetches the recent games from the server for a given user.
@riverpod
Future<IList<LightArchivedGameWithPov>> userRecentGames(Ref ref, {required UserId userId}) {
return ref.withClient((client) => GameRepository(client).getUserGames(userId));
return ref.withClient(
(client) => GameRepository(client).getUserGames(userId, withBookmarked: true),
);
}

/// A provider that fetches the total number of games played by given user, or the current app user if no user is provided.
Expand Down Expand Up @@ -110,7 +114,10 @@ class UserGameHistory extends _$UserGameHistory {
final id = userId ?? session?.user.id;
final recentGames =
id != null && online
? ref.withClient((client) => GameRepository(client).getUserGames(id, filter: filter))
? ref.withClient(
(client) =>
GameRepository(client).getUserGames(id, filter: filter, withBookmarked: true),
)
: storage
.page(userId: id, filter: filter)
.then(
Expand Down Expand Up @@ -149,6 +156,7 @@ class UserGameHistory extends _$UserGameHistory {
max: _nbPerPage,
until: _list.last.game.createdAt,
filter: currentVal.filter,
withBookmarked: true,
),
)
: currentVal.online && currentVal.session != null
Expand All @@ -158,6 +166,7 @@ class UserGameHistory extends _$UserGameHistory {
max: _nbPerPage,
until: _list.last.game.createdAt,
filter: currentVal.filter,
withBookmarked: true,
),
)
: (await ref.watch(gameStorageProvider.future))
Expand Down Expand Up @@ -192,6 +201,23 @@ class UserGameHistory extends _$UserGameHistory {
},
);
}

void toggleBookmark(int index) {
if (!state.hasValue) return;

final gameList = state.requireValue.gameList;
final game = gameList[index].game;
final pov = gameList[index].pov;

state = AsyncData(
state.requireValue.copyWith(
gameList: gameList.replace(index, (
game: game.copyWith(bookmarked: !game.bookmarked!),
pov: pov,
)),
),
);
}
}

@freezed
Expand Down
26 changes: 22 additions & 4 deletions lib/src/model/game/game_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,18 @@ class GameRepository {

final LichessClient client;

Future<ArchivedGame> getGame(GameId id) {
Future<ArchivedGame> getGame(GameId id, {bool withBookmarked = false}) {
return client.readJson(
Uri(path: '/game/export/$id', queryParameters: {'clocks': '1', 'accuracy': '1'}),
Uri(
path: '/game/export/$id',
queryParameters: {
'clocks': '1',
'accuracy': '1',
if (withBookmarked) 'withBookmarked': '1',
},
),
headers: {'Accept': 'application/json'},
mapper: ArchivedGame.fromServerJson,
mapper: (json) => ArchivedGame.fromServerJson(json, withBookmarked: withBookmarked),
);
}

Expand All @@ -34,6 +41,7 @@ class GameRepository {
int max = 20,
DateTime? until,
GameFilterState filter = const GameFilterState(),
bool withBookmarked = false,
}) {
assert(!filter.perfs.contains(Perf.fromPosition));
assert(!filter.perfs.contains(Perf.puzzle));
Expand All @@ -53,10 +61,11 @@ class GameRepository {
if (filter.perfs.isNotEmpty)
'perfType': filter.perfs.map((perf) => perf.name).join(','),
if (filter.side != null) 'color': filter.side!.name,
if (withBookmarked) 'withBookmarked': 'true',
},
),
headers: {'Accept': 'application/x-ndjson'},
mapper: LightArchivedGame.fromServerJson,
mapper: (json) => LightArchivedGame.fromServerJson(json, withBookmarked: withBookmarked),
)
.then(
(value) =>
Expand Down Expand Up @@ -91,4 +100,13 @@ class GameRepository {
mapper: LightArchivedGame.fromServerJson,
);
}

/// Bookmark the game for the given `id` if `bookmark` is true else unbookmark it
Future<void> bookmark(GameId id, {required bool bookmark}) async {
final uri = Uri(path: '/bookmark/$id', queryParameters: {'v': bookmark ? '1' : '0'});
final response = await client.post(uri);
if (response.statusCode >= 400) {
throw http.ClientException('Failed to bookmark game: ${response.statusCode}', uri);
}
}
}
6 changes: 5 additions & 1 deletion lib/src/model/game/game_repository_providers.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lichess_mobile/src/model/auth/auth_session.dart';
import 'package:lichess_mobile/src/model/common/id.dart';
import 'package:lichess_mobile/src/model/game/archived_game.dart';
import 'package:lichess_mobile/src/model/game/game_repository.dart';
Expand All @@ -14,7 +15,10 @@ part 'game_repository_providers.g.dart';
Future<ArchivedGame> archivedGame(Ref ref, {required GameId id}) async {
ArchivedGame game;
try {
game = await ref.withClient((client) => GameRepository(client).getGame(id));
final isLoggedIn = ref.watch(isLoggedInProvider);
game = await ref.withClient(
(client) => GameRepository(client).getGame(id, withBookmarked: isLoggedIn),
);
} catch (_) {
final gameStorage = await ref.watch(gameStorageProvider.future);
final storedGame = await gameStorage.fetch(gameId: id);
Expand Down
41 changes: 38 additions & 3 deletions lib/src/view/game/archived_game_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import 'package:lichess_mobile/src/model/analysis/analysis_controller.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.dart';
Expand Down Expand Up @@ -89,7 +90,7 @@ class _LoadGame extends ConsumerWidget {
}
}

class _Body extends StatelessWidget {
class _Body extends ConsumerWidget {
const _Body({required this.gameData, required this.orientation, this.initialCursor, this.error});

final LightArchivedGame? gameData;
Expand All @@ -98,13 +99,18 @@ class _Body extends StatelessWidget {
final int? initialCursor;

@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final isLoggedIn = ref.watch(isLoggedInProvider);

return PlatformBoardThemeScaffold(
appBar: PlatformAppBar(
title: gameData != null ? _GameTitle(gameData: gameData!) : const SizedBox.shrink(),
actions: [
if (gameData == null && error == null) const PlatformAppBarLoadingIndicator(),
const ToggleSoundButton(),
if (gameData != null && isLoggedIn)
_GameMenu(id: gameData!.id, bookmarked: gameData!.bookmarked!)
else
const ToggleSoundButton(),
],
),
body: SafeArea(
Expand Down Expand Up @@ -153,6 +159,35 @@ class _GameTitle extends StatelessWidget {
}
}

class _GameMenu extends ConsumerWidget {
const _GameMenu({required this.id, required this.bookmarked});

final GameId id;
final bool bookmarked;

@override
Widget build(BuildContext context, WidgetRef ref) {
return PlatformContextMenuAnchor(
builder:
(context, controller, _) => AppBarIconButton(
icon: const Icon(Icons.more_horiz),
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
semanticsLabel: 'Game menu',
),
actions: [
toggleSoundMenuAction(context, ref),
toggleBookmarkMenuAction(context, ref, id: id.gameId, bookmarked: bookmarked),
],
);
}
}

class _BoardBody extends ConsumerWidget {
const _BoardBody({
required this.archivedGameData,
Expand Down
Loading