Skip to content
12 changes: 8 additions & 4 deletions lib/src/app_links.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import 'package:dartchess/dartchess.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/widgets.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/puzzle/puzzle_angle.dart';
import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart';
import 'package:lichess_mobile/src/view/broadcast/broadcast_game_screen.dart';
import 'package:lichess_mobile/src/view/broadcast/broadcast_round_screen.dart';
import 'package:lichess_mobile/src/view/game/archived_game_screen.dart';
import 'package:lichess_mobile/src/view/puzzle/puzzle_screen.dart';
import 'package:lichess_mobile/src/view/study/study_screen.dart';
import 'package:lichess_mobile/src/view/tournament/tournament_screen.dart';
Expand Down Expand Up @@ -42,10 +43,13 @@ Route<dynamic>? resolveAppLinkUri(BuildContext context, Uri appLinkUri) {
final orientation = appLinkUri.pathSegments.getOrNull(2);
// The game id can also be a challenge. Challenge by link is not supported yet so let's ignore it.
if (gameId.isValid) {
return ArchivedGameScreen.buildRoute(
return AnalysisScreen.buildRoute(
context,
gameId: gameId,
orientation: orientation == 'black' ? Side.black : Side.white,
AnalysisOptions(
orientation: orientation == 'black' ? Side.black : Side.white,
gameId: gameId,
initialMoveCursor: 0,
),
);
}
}
Expand Down
21 changes: 21 additions & 0 deletions lib/src/init.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@ import 'package:lichess_mobile/src/constants.dart';
import 'package:lichess_mobile/src/db/secure_storage.dart';
import 'package:lichess_mobile/src/model/account/home_preferences.dart';
import 'package:lichess_mobile/src/model/account/home_widgets.dart';
import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart';
import 'package:lichess_mobile/src/model/auth/auth_session.dart';
import 'package:lichess_mobile/src/model/auth/session_storage.dart';
import 'package:lichess_mobile/src/model/broadcast/broadcast_preferences.dart';
import 'package:lichess_mobile/src/model/notifications/notification_service.dart';
import 'package:lichess_mobile/src/model/notifications/notifications.dart';
import 'package:lichess_mobile/src/model/settings/board_preferences.dart';
import 'package:lichess_mobile/src/model/settings/preferences_storage.dart';
import 'package:lichess_mobile/src/model/study/study_preferences.dart';
import 'package:lichess_mobile/src/utils/chessboard.dart';
import 'package:lichess_mobile/src/utils/color_palette.dart';
import 'package:lichess_mobile/src/utils/screen.dart';
Expand Down Expand Up @@ -51,6 +54,8 @@ Future<void> setupFirstLaunch() async {
await prefs.setString(PrefCategory.board.storageKey, jsonEncode(boardPrefs.toJson()));
}

_screenSizeBasedInitialization();

await prefs.setBool('first_run', false);
}

Expand Down Expand Up @@ -165,3 +170,19 @@ Future<void> androidDisplayInitialization(WidgetsBinding widgetsBinding) async {
// This setting is per session.
await FlutterDisplayMode.setPreferredMode(mostOptimalMode);
}

// Adjusts some settings for small screens based on the MediaQuery data.
Future<void> _screenSizeBasedInitialization() async {
final prefs = LichessBinding.instance.sharedPreferences;
final mediaQueryData = MediaQueryData.fromView(
WidgetsBinding.instance.platformDispatcher.views.first,
);
final isSmallScreen = estimateHeightMinusBoard(mediaQueryData) < kSmallHeightMinusBoard;

final analysisPrefs = AnalysisPrefs.defaults.copyWith(showEngineLines: !isSmallScreen);
await prefs.setString(PrefCategory.analysis.storageKey, jsonEncode(analysisPrefs.toJson()));
final studyPrefs = StudyPrefs.defaults.copyWith(showEngineLines: !isSmallScreen);
await prefs.setString(PrefCategory.study.storageKey, jsonEncode(studyPrefs.toJson()));
final broadcastPrefs = BroadcastPrefs.defaults.copyWith(showEngineLines: !isSmallScreen);
await prefs.setString(PrefCategory.broadcast.storageKey, jsonEncode(broadcastPrefs.toJson()));
}
5 changes: 1 addition & 4 deletions lib/src/model/account/account_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,7 @@ Future<User?> account(Ref ref) async {
final session = ref.watch(authSessionProvider);
if (session == null) return null;

return ref.withClientCacheFor(
(client) => AccountRepository(client).getProfile(),
const Duration(hours: 1),
);
return ref.withClient((client) => AccountRepository(client).getProfile());
}

@riverpod
Expand Down
9 changes: 8 additions & 1 deletion lib/src/model/account/account_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ class AccountService {
StreamSubscription<(NotificationResponse, LocalNotification)>? _notificationResponseSubscription;
Timer? _refreshTimer;

/// Stream of bookmark changes for the current user.
final StreamController<(GameId, bool)> _bookmarkChangesController = StreamController.broadcast();

/// Stream of bookmark changes for the current user.
Stream<(GameId, bool)> get bookmarkChanges => _bookmarkChangesController.stream;

final Ref _ref;

static const _storageKey = 'account.playban_notification_date';
Expand Down Expand Up @@ -86,6 +92,7 @@ class AccountService {
_refreshTimer?.cancel();
_accountProviderSubscription?.close();
_notificationResponseSubscription?.cancel();
_bookmarkChangesController.close();
}

Future<void> showPlaybanDialog(TemporaryBan playban) async {
Expand Down Expand Up @@ -117,6 +124,6 @@ class AccountService {

await _ref.withClient((client) => AccountRepository(client).bookmark(id, bookmark: bookmark));

_ref.invalidate(accountProvider);
_bookmarkChangesController.add((id, bookmark));
}
}
17 changes: 17 additions & 0 deletions lib/src/model/analysis/analysis_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:dartchess/dartchess.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:intl/intl.dart';
import 'package:lichess_mobile/src/model/account/account_service.dart';
import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart';
import 'package:lichess_mobile/src/model/analysis/opening_service.dart';
import 'package:lichess_mobile/src/model/analysis/server_analysis_service.dart';
Expand Down Expand Up @@ -429,6 +430,22 @@ class AnalysisController extends _$AnalysisController
return nodes.map((node) => node.sanMove.san).join(' ');
}

Future<void> toggleBookmark() async {
if (state.hasValue) {
final game = state.requireValue.archivedGame;
if (game == null) return;
final toggledBookmark = !(game.data.bookmarked ?? false);
await ref.read(accountServiceProvider).setGameBookmark(game.id, bookmark: toggledBookmark);
state = AsyncValue.data(
state.requireValue.copyWith(
archivedGame: state.requireValue.archivedGame?.copyWith(
data: game.data.copyWith(bookmarked: toggledBookmark),
),
),
);
}
}

void _setPath(
UciPath path, {
bool shouldForceShowVariation = false,
Expand Down
6 changes: 6 additions & 0 deletions lib/src/model/analysis/analysis_preferences.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ class AnalysisPreferences extends _$AnalysisPreferences with PreferencesStorage<
return save(state.copyWith(showEvaluationGauge: !state.showEvaluationGauge));
}

Future<void> toggleShowEngineLines() {
return save(state.copyWith(showEngineLines: !state.showEngineLines));
}

Future<void> toggleAnnotations() {
return save(state.copyWith(showAnnotations: !state.showAnnotations));
}
Expand Down Expand Up @@ -59,6 +63,7 @@ sealed class AnalysisPrefs with _$AnalysisPrefs implements Serializable {
const factory AnalysisPrefs({
@JsonKey(defaultValue: true) required bool enableComputerAnalysis,
required bool showEvaluationGauge,
@JsonKey(defaultValue: true) required bool showEngineLines,
required bool showBestMoveArrow,
required bool showAnnotations,
required bool showPgnComments,
Expand All @@ -69,6 +74,7 @@ sealed class AnalysisPrefs with _$AnalysisPrefs implements Serializable {
static const defaults = AnalysisPrefs(
enableComputerAnalysis: true,
showEvaluationGauge: true,
showEngineLines: true,
showBestMoveArrow: true,
showAnnotations: true,
showPgnComments: true,
Expand Down
6 changes: 3 additions & 3 deletions lib/src/model/broadcast/broadcast_analysis_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/widgets.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart';
import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart';
import 'package:lichess_mobile/src/model/broadcast/broadcast_preferences.dart';
import 'package:lichess_mobile/src/model/broadcast/broadcast_repository.dart';
import 'package:lichess_mobile/src/model/common/chess.dart';
import 'package:lichess_mobile/src/model/common/id.dart';
Expand Down Expand Up @@ -127,7 +127,7 @@ class BroadcastAnalysisController extends _$BroadcastAnalysisController

// don't use ref.watch here: we don't want to invalidate state when the
// analysis preferences change
final prefs = ref.read(analysisPreferencesProvider);
final prefs = ref.read(broadcastPreferencesProvider);

final broadcastState = BroadcastAnalysisState(
id: gameId,
Expand Down Expand Up @@ -420,7 +420,7 @@ class BroadcastAnalysisController extends _$BroadcastAnalysisController
///
/// Acts both on engine evaluation and server analysis.
Future<void> toggleComputerAnalysis() async {
await ref.read(analysisPreferencesProvider.notifier).toggleEnableComputerAnalysis();
await ref.read(broadcastPreferencesProvider.notifier).toggleEnableComputerAnalysis();

final curState = state.requireValue;
final engineWasAvailable = curState.isEngineAvailable(evaluationPrefs);
Expand Down
56 changes: 54 additions & 2 deletions lib/src/model/broadcast/broadcast_preferences.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,65 @@ class BroadcastPreferences extends _$BroadcastPreferences with PreferencesStorag
Future<void> toggleEvaluationBar() {
return save(state.copyWith(showEvaluationBar: !state.showEvaluationBar));
}

Future<void> toggleEnableComputerAnalysis() {
return save(state.copyWith(enableComputerAnalysis: !state.enableComputerAnalysis));
}

Future<void> toggleShowEvaluationGauge() {
return save(state.copyWith(showEvaluationGauge: !state.showEvaluationGauge));
}

Future<void> toggleShowEngineLines() {
return save(state.copyWith(showEngineLines: !state.showEngineLines));
}

Future<void> toggleAnnotations() {
return save(state.copyWith(showAnnotations: !state.showAnnotations));
}

Future<void> togglePgnComments() {
return save(state.copyWith(showPgnComments: !state.showPgnComments));
}

Future<void> toggleShowBestMoveArrow() {
return save(state.copyWith(showBestMoveArrow: !state.showBestMoveArrow));
}

Future<void> toggleInlineNotation() {
return save(state.copyWith(inlineNotation: !state.inlineNotation));
}

Future<void> toggleSmallBoard() {
return save(state.copyWith(smallBoard: !state.smallBoard));
}
}

@Freezed(fromJson: true, toJson: true)
sealed class BroadcastPrefs with _$BroadcastPrefs implements Serializable {
const factory BroadcastPrefs({required bool showEvaluationBar}) = _BroadcastPrefs;
const factory BroadcastPrefs({
required bool showEvaluationBar,
@JsonKey(defaultValue: true) required bool enableComputerAnalysis,
@JsonKey(defaultValue: true) required bool showEvaluationGauge,
@JsonKey(defaultValue: true) required bool showEngineLines,
@JsonKey(defaultValue: true) required bool showBestMoveArrow,
@JsonKey(defaultValue: true) required bool showAnnotations,
@JsonKey(defaultValue: true) required bool showPgnComments,
@JsonKey(defaultValue: false) required bool inlineNotation,
@JsonKey(defaultValue: false) required bool smallBoard,
}) = _BroadcastPrefs;

static const defaults = BroadcastPrefs(showEvaluationBar: true);
static const defaults = BroadcastPrefs(
showEvaluationBar: true,
enableComputerAnalysis: true,
showEvaluationGauge: true,
showEngineLines: true,
showBestMoveArrow: true,
showAnnotations: true,
showPgnComments: true,
inlineNotation: false,
smallBoard: false,
);

factory BroadcastPrefs.fromJson(Map<String, dynamic> json) => _$BroadcastPrefsFromJson(json);
}
4 changes: 4 additions & 0 deletions lib/src/model/game/game.dart
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,10 @@ mixin IndexableSteps on BaseGame {

Duration? archivedBlackClockAt(int cursor) => steps[cursor].archivedBlackClock;

Duration? archivedClockOf(Side side, int cursor) {
return side == Side.white ? steps[cursor].archivedWhiteClock : steps[cursor].archivedBlackClock;
}

Move? get lastMove {
return steps.last.sanMove?.move;
}
Expand Down
19 changes: 14 additions & 5 deletions lib/src/model/game/game_history.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/model/account/account_repository.dart';
import 'package:lichess_mobile/src/model/account/account_service.dart';
import 'package:lichess_mobile/src/model/auth/auth_session.dart';
import 'package:lichess_mobile/src/model/common/id.dart';
import 'package:lichess_mobile/src/model/game/exported_game.dart';
Expand Down Expand Up @@ -86,6 +87,8 @@ Future<int> userNumberOfGames(Ref ref, LightUser? user) async {
class UserGameHistory extends _$UserGameHistory {
final _list = <LightExportedGameWithPov>[];

StreamSubscription<(GameId, bool)>? _bookmarkChangesSubscription;

@override
Future<UserGameHistoryState> build(
UserId? userId, {
Expand All @@ -99,8 +102,17 @@ class UserGameHistory extends _$UserGameHistory {
required bool isOnline,
GameFilterState filter = const GameFilterState(),
}) async {
_bookmarkChangesSubscription?.cancel();
_bookmarkChangesSubscription = ref.read(accountServiceProvider).bookmarkChanges.listen((data) {
final (id, bookmarked) = data;
if (state.hasValue) {
setBookmark(id, bookmarked: bookmarked);
}
});

ref.cacheFor(const Duration(minutes: 5));
ref.onDispose(() {
_bookmarkChangesSubscription?.cancel();
_list.clear();
});

Expand Down Expand Up @@ -201,7 +213,7 @@ class UserGameHistory extends _$UserGameHistory {
}
}

void toggleBookmark(GameId id) {
void setBookmark(GameId id, {required bool bookmarked}) {
if (!state.hasValue) return;

final gameList = state.requireValue.gameList;
Expand All @@ -213,10 +225,7 @@ class UserGameHistory extends _$UserGameHistory {

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