Skip to content

Commit a957fc3

Browse files
committed
feat: add tip to home screen when nnue files are outdated
1 parent d37e57d commit a957fc3

File tree

5 files changed

+253
-2
lines changed

5 files changed

+253
-2
lines changed

lib/src/model/engine/nnue_service.dart

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,29 @@ class NnueService {
6060
return (bigNet: bigNetFile, smallNet: smallNetFile);
6161
}
6262

63+
Future<bool> hasOutdatedNNUEFiles() async {
64+
if (await checkNNUEFiles()) {
65+
return false;
66+
}
67+
68+
final appSupportDirectory = _ref.read(preloadedDataProvider).requireValue.appSupportDirectory;
69+
if (appSupportDirectory == null) {
70+
return false;
71+
}
72+
73+
final NNUEFiles files = nnueFiles;
74+
75+
await for (final entity in appSupportDirectory.list(followLinks: false)) {
76+
if (entity is File &&
77+
entity.path.endsWith('.nnue') &&
78+
entity.path != files.bigNet.path &&
79+
entity.path != files.smallNet.path) {
80+
return true;
81+
}
82+
}
83+
return false;
84+
}
85+
6386
/// Check the presence and integrity of the NNUE files.
6487
Future<bool> checkNNUEFiles() async {
6588
final NNUEFiles files;

lib/src/view/home/home_tab_screen.dart

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import 'package:lichess_mobile/src/model/challenge/challenges.dart';
1717
import 'package:lichess_mobile/src/model/common/id.dart';
1818
import 'package:lichess_mobile/src/model/correspondence/correspondence_game_storage.dart';
1919
import 'package:lichess_mobile/src/model/correspondence/offline_correspondence_game.dart';
20+
import 'package:lichess_mobile/src/model/engine/evaluation_preferences.dart';
21+
import 'package:lichess_mobile/src/model/engine/nnue_service.dart';
2022
import 'package:lichess_mobile/src/model/game/game_history.dart';
2123
import 'package:lichess_mobile/src/model/message/message_repository.dart';
2224
import 'package:lichess_mobile/src/model/tournament/tournament.dart';
@@ -45,6 +47,7 @@ import 'package:lichess_mobile/src/view/play/ongoing_games_screen.dart';
4547
import 'package:lichess_mobile/src/view/play/play_bottom_sheet.dart';
4648
import 'package:lichess_mobile/src/view/play/play_menu.dart';
4749
import 'package:lichess_mobile/src/view/play/quick_game_matrix.dart';
50+
import 'package:lichess_mobile/src/view/settings/engine_settings_screen.dart';
4851
import 'package:lichess_mobile/src/view/tournament/tournament_list_screen.dart';
4952
import 'package:lichess_mobile/src/view/user/challenge_requests_screen.dart';
5053
import 'package:lichess_mobile/src/view/user/player_screen.dart';
@@ -253,7 +256,10 @@ class _HomeScreenState extends ConsumerState<HomeTabScreen> {
253256
shouldShow: true,
254257
child: _GreetingWidget(),
255258
),
256-
if (!widget.editModeEnabled) const _HomeCustomizationTip(),
259+
if (!widget.editModeEnabled) ...[
260+
const _HomeCustomizationTip(),
261+
const _NNUEFilesOutdatedTip(),
262+
],
257263
if (status.isOnline)
258264
_EditableWidget(
259265
widget: HomeEditableWidget.perfCards,
@@ -310,7 +316,10 @@ class _HomeScreenState extends ConsumerState<HomeTabScreen> {
310316
shouldShow: true,
311317
child: _GreetingWidget(),
312318
),
313-
if (!widget.editModeEnabled) const _HomeCustomizationTip(),
319+
if (!widget.editModeEnabled) ...[
320+
const _HomeCustomizationTip(),
321+
const _NNUEFilesOutdatedTip(),
322+
],
314323
_EditableWidget(
315324
widget: HomeEditableWidget.perfCards,
316325
shouldShow: authUser != null && status.isOnline,
@@ -1000,6 +1009,98 @@ class _WelcomeMessageCardState extends State<_WelcomeMessageCard> {
10001009
}
10011010
}
10021011

1012+
class _NNUEFilesOutdatedTip extends ConsumerStatefulWidget {
1013+
const _NNUEFilesOutdatedTip();
1014+
1015+
@override
1016+
ConsumerState<_NNUEFilesOutdatedTip> createState() => _NNUEFilesOutdatedTipState();
1017+
}
1018+
1019+
class _NNUEFilesOutdatedTipState extends ConsumerState<_NNUEFilesOutdatedTip> {
1020+
bool _openedSettings = false;
1021+
1022+
@override
1023+
Widget build(BuildContext context) {
1024+
final chessEnginePref = ref.watch(engineEvaluationPreferencesProvider).enginePref;
1025+
if (chessEnginePref != ChessEnginePref.sfLatest) {
1026+
return const SizedBox.shrink();
1027+
}
1028+
1029+
final nnueService = ref.watch(nnueServiceProvider);
1030+
if (nnueService.isDownloadingNNUEFiles) {
1031+
return const SizedBox.shrink();
1032+
}
1033+
1034+
return FocusDetector(
1035+
// If we come back from the settings, trigger rebuild to hide the widget if the user has updated the NNUE files
1036+
onFocusRegained: () {
1037+
if (_openedSettings) {
1038+
setState(() {});
1039+
}
1040+
},
1041+
child: FutureBuilder(
1042+
future: nnueService.hasOutdatedNNUEFiles(),
1043+
builder: (context, snapshot) {
1044+
final hasOutdatedNNUEFiles = snapshot.data ?? false;
1045+
if (!hasOutdatedNNUEFiles) {
1046+
return const SizedBox.shrink();
1047+
}
1048+
1049+
return Padding(
1050+
padding: Styles.bodyPadding,
1051+
child: Card(
1052+
child: Padding(
1053+
padding: const EdgeInsets.all(8.0),
1054+
child: Column(
1055+
crossAxisAlignment: CrossAxisAlignment.start,
1056+
children: [
1057+
Padding(
1058+
padding: const EdgeInsets.all(8.0),
1059+
child: Row(
1060+
children: [
1061+
Icon(
1062+
Icons.warning,
1063+
size: 25.0,
1064+
color: Theme.of(context).colorScheme.primary,
1065+
),
1066+
const SizedBox(width: 8.0),
1067+
const Flexible(
1068+
child: Text(
1069+
// TODO l10n
1070+
'New Stockfish version available! Go to the settings to download the updated NNUE files.',
1071+
),
1072+
),
1073+
],
1074+
),
1075+
),
1076+
Row(
1077+
children: [
1078+
TextButton(
1079+
onPressed: () {
1080+
setState(() {
1081+
_openedSettings = true;
1082+
});
1083+
Navigator.of(
1084+
context,
1085+
rootNavigator: true,
1086+
).push(EngineSettingsScreen.buildRoute(context));
1087+
},
1088+
// TODO l10n
1089+
child: const Text('Open settings'),
1090+
),
1091+
],
1092+
),
1093+
],
1094+
),
1095+
),
1096+
),
1097+
);
1098+
},
1099+
),
1100+
);
1101+
}
1102+
}
1103+
10031104
class _HomeCustomizationTip extends StatefulWidget {
10041105
const _HomeCustomizationTip();
10051106

test/model/engine/fake_nnue_service.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ class FakeNnueService implements NnueService {
3232
return true;
3333
}
3434

35+
@override
36+
Future<bool> hasOutdatedNNUEFiles() async {
37+
return false;
38+
}
39+
3540
@override
3641
Future<bool> downloadNNUEFiles({bool inBackground = true}) async {
3742
return false;
@@ -46,6 +51,7 @@ class FakeNnueService implements NnueService {
4651
/// A fake [NnueService] that simulates missing/unavailable NNUE files.
4752
///
4853
/// - Always returns false for [checkNNUEFiles]
54+
/// - Always returns true for [hasOutdatedNNUEFiles]
4955
/// - All other behaviour is identical to [FakeNnueService]
5056
class FakeNnueServiceUnavailable implements NnueService {
5157
FakeNnueServiceUnavailable();
@@ -68,6 +74,11 @@ class FakeNnueServiceUnavailable implements NnueService {
6874
return false;
6975
}
7076

77+
@override
78+
Future<bool> hasOutdatedNNUEFiles() async {
79+
return true;
80+
}
81+
7182
@override
7283
Future<bool> downloadNNUEFiles({bool inBackground = true}) async {
7384
return false;

test/model/engine/nnue_service_test.dart

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,33 @@ void main() {
159159
});
160160
});
161161

162+
group('hasOutdatedNNUEFiles', () {
163+
test('returns false when appSupportDirectory is null', () async {
164+
final container = await makeNnueTestContainer(appSupportDirectory: null);
165+
addTearDown(container.dispose);
166+
167+
final service = container.read(nnueServiceProvider);
168+
final result = await service.hasOutdatedNNUEFiles();
169+
170+
expect(result, isFalse);
171+
});
172+
173+
test('returns true if we have outdated nnue files', () async {
174+
final tempDir = await Directory.systemTemp.createTemp('nnue_test_');
175+
addTearDown(() => tempDir.delete(recursive: true));
176+
177+
File('${tempDir.path}/someOldFile.nnue').create();
178+
179+
final container = await makeNnueTestContainer(appSupportDirectory: tempDir);
180+
addTearDown(container.dispose);
181+
182+
final service = container.read(nnueServiceProvider);
183+
final result = await service.hasOutdatedNNUEFiles();
184+
185+
expect(result, isTrue);
186+
});
187+
});
188+
162189
group('deleteNNUEFiles', () {
163190
test('throws exception when appSupportDirectory is null', () async {
164191
final container = await makeNnueTestContainer(appSupportDirectory: null);

test/view/home/home_tab_screen_test.dart

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1+
import 'dart:convert';
2+
13
import 'package:flutter/material.dart';
24
import 'package:flutter_riverpod/flutter_riverpod.dart';
35
import 'package:flutter_test/flutter_test.dart';
46
import 'package:http/testing.dart';
57
import 'package:lichess_mobile/src/app.dart';
8+
import 'package:lichess_mobile/src/model/engine/evaluation_preferences.dart';
9+
import 'package:lichess_mobile/src/model/engine/nnue_service.dart';
610
import 'package:lichess_mobile/src/model/game/game_storage.dart';
11+
import 'package:lichess_mobile/src/model/settings/preferences_storage.dart';
712
import 'package:lichess_mobile/src/network/http.dart';
813
import 'package:lichess_mobile/src/styles/lichess_icons.dart';
914
import 'package:lichess_mobile/src/view/game/game_list_tile.dart';
@@ -18,6 +23,7 @@ import '../../example_data.dart';
1823
import '../../mock_server_responses.dart';
1924
import '../../model/auth/fake_auth_storage.dart';
2025
import '../../model/challenge/challenge_repository_test.dart';
26+
import '../../model/engine/fake_nnue_service.dart';
2127
import '../../network/fake_http_client_factory.dart';
2228
import '../../test_helpers.dart';
2329
import '../../test_provider_scope.dart';
@@ -464,5 +470,88 @@ void main() {
464470
expect(find.text(customizeTip), findsNothing);
465471
});
466472
});
473+
474+
group('NNUE files missing tip', () {
475+
const nnueFilesMissingTip =
476+
'New Stockfish version available! Go to the settings to download the updated NNUE files.';
477+
testWidgets('Shown if engine pref is latest sf and NNUE files are missing', (tester) async {
478+
final app = await makeTestProviderScope(
479+
tester,
480+
overrides: {
481+
nnueServiceProvider: nnueServiceProvider.overrideWithValue(
482+
FakeNnueServiceUnavailable(),
483+
),
484+
},
485+
authUser: fakeAuthUser,
486+
defaultPreferences: {
487+
PrefCategory.engineEvaluation.storageKey: jsonEncode(
488+
EngineEvaluationPrefState.defaults
489+
.copyWith(enginePref: ChessEnginePref.sfLatest)
490+
.toJson(),
491+
),
492+
},
493+
child: const Application(),
494+
);
495+
496+
await tester.pumpWidget(app);
497+
498+
// Wait for hasOutdatedNNUEFiles() future to complete
499+
await tester.pumpAndSettle();
500+
501+
expect(find.text(nnueFilesMissingTip), findsOneWidget);
502+
});
503+
504+
testWidgets('Not shown if nnue files are available', (tester) async {
505+
final app = await makeTestProviderScope(
506+
tester,
507+
overrides: {
508+
nnueServiceProvider: nnueServiceProvider.overrideWithValue(FakeNnueService()),
509+
},
510+
authUser: fakeAuthUser,
511+
defaultPreferences: {
512+
PrefCategory.engineEvaluation.storageKey: jsonEncode(
513+
EngineEvaluationPrefState.defaults
514+
.copyWith(enginePref: ChessEnginePref.sfLatest)
515+
.toJson(),
516+
),
517+
},
518+
child: const Application(),
519+
);
520+
521+
await tester.pumpWidget(app);
522+
523+
// Wait for hasOutdatedNNUEFiles() future to complete
524+
await tester.pumpAndSettle();
525+
526+
expect(find.text(nnueFilesMissingTip), findsNothing);
527+
});
528+
529+
testWidgets('Not shown if engine pref is sf16', (tester) async {
530+
final app = await makeTestProviderScope(
531+
tester,
532+
overrides: {
533+
nnueServiceProvider: nnueServiceProvider.overrideWithValue(
534+
FakeNnueServiceUnavailable(),
535+
),
536+
},
537+
authUser: fakeAuthUser,
538+
defaultPreferences: {
539+
PrefCategory.engineEvaluation.storageKey: jsonEncode(
540+
EngineEvaluationPrefState.defaults
541+
.copyWith(enginePref: ChessEnginePref.sf16)
542+
.toJson(),
543+
),
544+
},
545+
child: const Application(),
546+
);
547+
548+
await tester.pumpWidget(app);
549+
550+
// Wait for hasOutdatedNNUEFiles() future to complete
551+
await tester.pumpAndSettle();
552+
553+
expect(find.text(nnueFilesMissingTip), findsNothing);
554+
});
555+
});
467556
});
468557
}

0 commit comments

Comments
 (0)