feat: add "small board" option (again), reduce "small screen" detection threshold (#3157)

This commit is contained in:
Tom Praschan
2026-05-13 10:58:53 +02:00
committed by GitHub
parent 4bc185af4f
commit 8daabc4377
13 changed files with 118 additions and 31 deletions
+55
View File
@@ -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<Application> {
StreamSubscription<List<SharedMediaFile>>? _intentSub;
// Adjusts some settings for small screens based on the MediaQuery data.
Future<void> _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();
+1 -1
View File
@@ -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 {
-21
View File
@@ -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<void> initializeApp() async {
await prefs.setString(PrefCategory.board.storageKey, jsonEncode(boardPrefs.toJson()));
}
_screenSizeBasedInitialization();
_logger.info('First run initialization completed.');
}
@@ -162,19 +157,3 @@ 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()));
}
@@ -55,6 +55,10 @@ class AnalysisPreferences extends Notifier<AnalysisPrefs> with PreferencesStorag
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)
@@ -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<String, dynamic> json) {
@@ -61,6 +61,10 @@ class BroadcastPreferences extends Notifier<BroadcastPrefs>
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)
@@ -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<String, dynamic> json) => _$BroadcastPrefsFromJson(json);
@@ -55,6 +55,10 @@ class StudyPreferencesNotifier extends Notifier<StudyPrefs> with PreferencesStor
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)
@@ -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<String, dynamic> json) {
+19 -9
View File
@@ -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,
@@ -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)
@@ -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<void>(
@@ -286,6 +286,7 @@ class _Body extends ConsumerWidget {
boardSize: boardSize,
boardRadius: borderRadius,
),
smallBoard: broadcastPrefs.smallBoard,
boardHeader: _PlayerWidget(
tournamentId: tournamentId,
roundId: roundId,
@@ -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<void>(
+5
View File
@@ -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) {
+6
View File
@@ -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<void>(