From 8daabc4377b12ddf92fe14671e92e0f456e730ed Mon Sep 17 00:00:00 2001 From: Tom Praschan <13141438+tom-anders@users.noreply.github.com> Date: Wed, 13 May 2026 10:58:53 +0200 Subject: [PATCH] feat: add "small board" option (again), reduce "small screen" detection threshold (#3157) --- lib/src/app.dart | 55 +++++++++++++++++++ lib/src/constants.dart | 2 +- lib/src/init.dart | 21 ------- .../model/analysis/analysis_preferences.dart | 6 ++ .../broadcast/broadcast_preferences.dart | 6 ++ lib/src/model/study/study_preferences.dart | 6 ++ lib/src/view/analysis/analysis_layout.dart | 28 +++++++--- lib/src/view/analysis/analysis_screen.dart | 1 + .../analysis/analysis_settings_screen.dart | 6 ++ .../view/broadcast/broadcast_game_screen.dart | 1 + .../broadcast_game_settings_screen.dart | 6 ++ lib/src/view/study/study_screen.dart | 5 ++ lib/src/view/study/study_settings.dart | 6 ++ 13 files changed, 118 insertions(+), 31 deletions(-) diff --git a/lib/src/app.dart b/lib/src/app.dart index bb97b03a5..6655e6227 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -9,11 +9,14 @@ import 'package:home_widget/home_widget.dart'; import 'package:l10n_esperanto/l10n_esperanto.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/app_links_service.dart'; +import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/constants.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/account/ongoing_game.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; import 'package:lichess_mobile/src/model/announce/announce_service.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_preferences.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_service.dart'; import 'package:lichess_mobile/src/model/common/preloaded_data.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_service.dart'; @@ -22,6 +25,7 @@ import 'package:lichess_mobile/src/model/message/message_service.dart'; import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; +import 'package:lichess_mobile/src/model/study/study_preferences.dart'; import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/network/socket.dart'; import 'package:lichess_mobile/src/quick_actions.dart'; @@ -81,8 +85,59 @@ class _AppState extends ConsumerState { StreamSubscription>? _intentSub; + // Adjusts some settings for small screens based on the MediaQuery data. + Future _screenSizeBasedInitialization(WidgetRef ref) async { + // Bump version here in case we adjust the thresholds for screen size based initialization + // and want it to run again for users who already launched the app with a previous version. + const kDoneScreenSizeInitKey = 'done_screen_size_init_v1'; + + final prefs = LichessBinding.instance.sharedPreferences; + if (prefs.getBool(kDoneScreenSizeInitKey) == true) { + return; + } + + final mediaQueryData = MediaQueryData.fromView( + WidgetsBinding.instance.platformDispatcher.views.first, + ); + final isTablet = mediaQueryData.size.shortestSide > FormFactor.tablet; + final isSmallScreen = estimateHeightMinusBoard(mediaQueryData) < kSmallHeightMinusBoard; + final showEngineLines = + isTablet || estimateHeightMinusBoard(mediaQueryData) > kSmallHeightMinusBoard - 30; + + // For tablets in portrait mode using the full board size makes the bottom analysis tabs tiny, + // see https://github.com/lichess-org/mobile/issues/3150, + // so use a small board there by default as well. + final smallBoard = isTablet || isSmallScreen; + + await ref + .read(analysisPreferencesProvider.notifier) + .save( + ref + .read(analysisPreferencesProvider) + .copyWith(smallBoard: smallBoard, showEngineLines: showEngineLines), + ); + await ref + .read(studyPreferencesProvider.notifier) + .save( + ref + .read(studyPreferencesProvider) + .copyWith(smallBoard: smallBoard, showEngineLines: showEngineLines), + ); + await ref + .read(broadcastPreferencesProvider.notifier) + .save( + ref + .read(broadcastPreferencesProvider) + .copyWith(smallBoard: smallBoard, showEngineLines: showEngineLines), + ); + + await prefs.setBool(kDoneScreenSizeInitKey, true); + } + @override void initState() { + _screenSizeBasedInitialization(ref); + // Start services ref.read(appLogServiceProvider).start(); ref.read(notificationServiceProvider).start(); diff --git a/lib/src/constants.dart b/lib/src/constants.dart index 84bce83fb..4a28ce962 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -69,7 +69,7 @@ const kBottomBarHeight = 56.0; const kMaterialPopupMenuMaxWidth = 500.0; /// The threshold to detect screens with a small remaining height minus board. -const kSmallHeightMinusBoard = 170; +const kSmallHeightMinusBoard = 200; // annotations class _AllowedWidgetReturn { diff --git a/lib/src/init.dart b/lib/src/init.dart index 3613f7ecd..ddf1b8ea2 100644 --- a/lib/src/init.dart +++ b/lib/src/init.dart @@ -9,13 +9,10 @@ import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/db/secure_storage.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/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'; @@ -51,8 +48,6 @@ Future initializeApp() async { await prefs.setString(PrefCategory.board.storageKey, jsonEncode(boardPrefs.toJson())); } - _screenSizeBasedInitialization(); - _logger.info('First run initialization completed.'); } @@ -162,19 +157,3 @@ Future 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 _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())); -} diff --git a/lib/src/model/analysis/analysis_preferences.dart b/lib/src/model/analysis/analysis_preferences.dart index a8d056258..1800886d3 100644 --- a/lib/src/model/analysis/analysis_preferences.dart +++ b/lib/src/model/analysis/analysis_preferences.dart @@ -55,6 +55,10 @@ class AnalysisPreferences extends Notifier with PreferencesStorag Future toggleInlineNotation() { return save(state.copyWith(inlineNotation: !state.inlineNotation)); } + + Future toggleSmallBoard() { + return save(state.copyWith(smallBoard: !state.smallBoard)); + } } @Freezed(fromJson: true, toJson: true) @@ -69,6 +73,7 @@ sealed class AnalysisPrefs with _$AnalysisPrefs implements Serializable, CommonA required bool showAnnotations, required bool showPgnComments, @JsonKey(defaultValue: false) required bool inlineNotation, + @JsonKey(defaultValue: false) required bool smallBoard, }) = _AnalysisPrefs; static const defaults = AnalysisPrefs( @@ -79,6 +84,7 @@ sealed class AnalysisPrefs with _$AnalysisPrefs implements Serializable, CommonA showAnnotations: true, showPgnComments: true, inlineNotation: false, + smallBoard: false, ); factory AnalysisPrefs.fromJson(Map json) { diff --git a/lib/src/model/broadcast/broadcast_preferences.dart b/lib/src/model/broadcast/broadcast_preferences.dart index 1f00aff3f..6459bcd82 100644 --- a/lib/src/model/broadcast/broadcast_preferences.dart +++ b/lib/src/model/broadcast/broadcast_preferences.dart @@ -61,6 +61,10 @@ class BroadcastPreferences extends Notifier Future toggleInlineNotation() { return save(state.copyWith(inlineNotation: !state.inlineNotation)); } + + Future toggleSmallBoard() { + return save(state.copyWith(smallBoard: !state.smallBoard)); + } } @Freezed(fromJson: true, toJson: true) @@ -74,6 +78,7 @@ sealed class BroadcastPrefs with _$BroadcastPrefs implements Serializable, Commo @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( @@ -85,6 +90,7 @@ sealed class BroadcastPrefs with _$BroadcastPrefs implements Serializable, Commo showAnnotations: true, showPgnComments: true, inlineNotation: false, + smallBoard: false, ); factory BroadcastPrefs.fromJson(Map json) => _$BroadcastPrefsFromJson(json); diff --git a/lib/src/model/study/study_preferences.dart b/lib/src/model/study/study_preferences.dart index 6040597c5..abb914489 100644 --- a/lib/src/model/study/study_preferences.dart +++ b/lib/src/model/study/study_preferences.dart @@ -55,6 +55,10 @@ class StudyPreferencesNotifier extends Notifier with PreferencesStor Future toggleInlineNotation() { return save(state.copyWith(inlineNotation: !state.inlineNotation)); } + + Future toggleSmallBoard() { + return save(state.copyWith(smallBoard: !state.smallBoard)); + } } @Freezed(fromJson: true, toJson: true) @@ -69,6 +73,7 @@ sealed class StudyPrefs with _$StudyPrefs implements Serializable, CommonAnalysi @JsonKey(defaultValue: true) required bool showAnnotations, @JsonKey(defaultValue: true) required bool showPgnComments, @JsonKey(defaultValue: false) required bool inlineNotation, + @JsonKey(defaultValue: false) required bool smallBoard, }) = _StudyPrefs; static const defaults = StudyPrefs( @@ -79,6 +84,7 @@ sealed class StudyPrefs with _$StudyPrefs implements Serializable, CommonAnalysi showAnnotations: true, showPgnComments: true, inlineNotation: false, + smallBoard: false, ); factory StudyPrefs.fromJson(Map json) { diff --git a/lib/src/view/analysis/analysis_layout.dart b/lib/src/view/analysis/analysis_layout.dart index 81af51a90..7111091ed 100644 --- a/lib/src/view/analysis/analysis_layout.dart +++ b/lib/src/view/analysis/analysis_layout.dart @@ -72,6 +72,7 @@ class AnalysisLayout extends ConsumerWidget { this.engineGaugeBuilder, this.engineLines, this.bottomBar, + this.smallBoard = false, this.pockets, super.key, }); @@ -118,6 +119,11 @@ class AnalysisLayout extends ConsumerWidget { /// A widget to show at the bottom of the screen. final Widget? bottomBar; + /// If true, the board is displayed in a small size on portrait orientation. + /// + /// This is `false` by default. + final bool smallBoard; + /// Current state of the pockets, in variants like crazyhouse. /// /// If not null, will render a [PocketsMenu] for each player. @@ -278,18 +284,22 @@ class AnalysisLayout extends ConsumerWidget { ), ); } else { - final evalGaugeWidth = getEvalGaugeWidth(context); - final defaultBoardSize = constraints.biggest.shortestSide; + final evalGaugeSize = engineGaugeBuilder != null + ? getEvalGaugeWidth(context) + : 0.0; + + final defaultBoardSize = + (smallBoard ? kSmallBoardScale : 1.0) * + (constraints.biggest.shortestSide - evalGaugeSize); + final remainingHeight = constraints.maxHeight - defaultBoardSize; final isSmallScreen = remainingHeight < kSmallHeightMinusBoard; - final evalGaugeSize = engineGaugeBuilder != null ? evalGaugeWidth : 0.0; final additionalBoardSidePaddingForPockets = isSmallScreen ? 70.0 : 16.0; - final boardSize = isTablet || isSmallScreen || pockets != null - ? defaultBoardSize - - evalGaugeSize - - kTabletBoardTableSidePadding * 2 - - (pockets != null ? additionalBoardSidePaddingForPockets : 0.0) - : defaultBoardSize - evalGaugeSize; + + final boardSize = + defaultBoardSize - + (isTablet ? kTabletBoardTableSidePadding * 2 : 0) - + (pockets != null ? additionalBoardSidePaddingForPockets : 0.0); return Column( mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 1508e8899..64d69fda6 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -302,6 +302,7 @@ class _Body extends ConsumerWidget { sideToMove: analysisState.currentPosition.turn, boardBuilder: (context, boardSize, borderRadius) => GameAnalysisBoard(options: options, boardSize: boardSize, boardRadius: borderRadius), + smallBoard: analysisPrefs.smallBoard, boardHeader: boardHeader, boardFooter: boardFooter, engineGaugeBuilder: showEvaluationGauge && analysisState.hasAvailableEval(enginePrefs) diff --git a/lib/src/view/analysis/analysis_settings_screen.dart b/lib/src/view/analysis/analysis_settings_screen.dart index 69efcfad4..ad1f13737 100644 --- a/lib/src/view/analysis/analysis_settings_screen.dart +++ b/lib/src/view/analysis/analysis_settings_screen.dart @@ -39,6 +39,12 @@ class AnalysisSettingsScreen extends ConsumerWidget { onChanged: (value) => ref.read(analysisPreferencesProvider.notifier).toggleInlineNotation(), ), + SwitchSettingTile( + title: const Text('Small board'), // TODO l10n + value: prefs.smallBoard, + onChanged: (value) => + ref.read(analysisPreferencesProvider.notifier).toggleSmallBoard(), + ), ListTile( title: Text(context.l10n.openingExplorer), onTap: () => showModalBottomSheet( diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index bc5cfdb4c..9f2b174ed 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -286,6 +286,7 @@ class _Body extends ConsumerWidget { boardSize: boardSize, boardRadius: borderRadius, ), + smallBoard: broadcastPrefs.smallBoard, boardHeader: _PlayerWidget( tournamentId: tournamentId, roundId: roundId, diff --git a/lib/src/view/broadcast/broadcast_game_settings_screen.dart b/lib/src/view/broadcast/broadcast_game_settings_screen.dart index 025c15cf1..85987e109 100644 --- a/lib/src/view/broadcast/broadcast_game_settings_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_settings_screen.dart @@ -41,6 +41,12 @@ class BroadcastGameSettingsScreen extends ConsumerWidget { onChanged: (value) => ref.read(broadcastPreferencesProvider.notifier).toggleInlineNotation(), ), + SwitchSettingTile( + title: const Text('Small board'), // TODO l10n + value: broadcastPrefs.smallBoard, + onChanged: (value) => + ref.read(broadcastPreferencesProvider.notifier).toggleSmallBoard(), + ), ListTile( title: Text(context.l10n.openingExplorer), onTap: () => showModalBottomSheet( diff --git a/lib/src/view/study/study_screen.dart b/lib/src/view/study/study_screen.dart index 6e29e90dc..21d1f4ece 100644 --- a/lib/src/view/study/study_screen.dart +++ b/lib/src/view/study/study_screen.dart @@ -69,6 +69,7 @@ class _StudyScreenLoader extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final boardPrefs = ref.watch(boardPreferencesProvider); + final studyPrefs = ref.watch(studyPreferencesProvider); switch (ref.watch(studyControllerProvider(options))) { case AsyncData(:final value): return _StudyScreen(options: options, studyState: value); @@ -90,6 +91,7 @@ class _StudyScreenLoader extends ConsumerWidget { orientation: Side.white, fen: kEmptyFEN, ), + smallBoard: studyPrefs.smallBoard, children: const [Center(child: Text('Failed to load study.'))], ), ), @@ -127,6 +129,7 @@ class _StudyScreenLoader extends ConsumerWidget { orientation: Side.white, fen: kEmptyFEN, ), + smallBoard: studyPrefs.smallBoard, children: const [Center(child: CircularProgressIndicator.adaptive())], ), ), @@ -439,6 +442,7 @@ class _Body extends ConsumerWidget { dimension: boardSize, child: Center(child: Text('${variant.label} is not supported yet.')), ), + smallBoard: studyPrefs.smallBoard, children: const [SizedBox.shrink()], ), ); @@ -463,6 +467,7 @@ class _Body extends ConsumerWidget { sideToMove: studyState.currentPosition?.turn, boardBuilder: (context, boardSize, borderRadius) => StudyAnalysisBoard(options: options, boardSize: boardSize, boardRadius: borderRadius), + smallBoard: studyPrefs.smallBoard, engineGaugeBuilder: isComputerAnalysisAllowed && showEvaluationGauge && engineGaugeParams != null ? (context) { diff --git a/lib/src/view/study/study_settings.dart b/lib/src/view/study/study_settings.dart index 1cf012537..e9df00c1d 100644 --- a/lib/src/view/study/study_settings.dart +++ b/lib/src/view/study/study_settings.dart @@ -40,6 +40,12 @@ class StudySettingsScreen extends ConsumerWidget { onChanged: (value) => ref.read(studyPreferencesProvider.notifier).toggleInlineNotation(), ), + SwitchSettingTile( + title: const Text('Small board'), // TODO l10n + value: studyPrefs.smallBoard, + onChanged: (value) => + ref.read(studyPreferencesProvider.notifier).toggleSmallBoard(), + ), ListTile( title: Text(context.l10n.openingExplorer), onTap: () => showModalBottomSheet(