Skip to content

Commit d85ee13

Browse files
committed
Give users more control over the exercise cache
This allows users to manually refresh the cache and load all exercises from the server.
1 parent c89ccb6 commit d85ee13

10 files changed

+177
-24
lines changed

lib/providers/exercises.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ class ExercisesProvider with ChangeNotifier {
4747
static const EXERCISE_CACHE_DAYS = 7;
4848
static const CACHE_VERSION = 4;
4949

50+
static const exerciseUrlPath = 'exercise';
5051
static const exerciseInfoUrlPath = 'exerciseinfo';
5152
static const exerciseSearchPath = 'exercise/search';
5253

@@ -274,6 +275,18 @@ class ExercisesProvider with ChangeNotifier {
274275
}
275276
}
276277

278+
Future<void> fetchAndSetAllExercises() async {
279+
_logger.info('Loading all exercises from API');
280+
final exerciseData = await baseProvider.fetchPaginated(
281+
baseProvider.makeUrl(exerciseUrlPath, query: {'limit': API_MAX_PAGE_SIZE}),
282+
);
283+
final exerciseIds = exerciseData.map<int>((e) => e['id'] as int).toSet();
284+
285+
for (final exerciseId in exerciseIds) {
286+
await handleUpdateExerciseFromApi(database, exerciseId);
287+
}
288+
}
289+
277290
/// Returns the exercise with the given ID
278291
///
279292
/// If the exercise is not known locally, it is fetched from the server.
@@ -291,6 +304,7 @@ class ExercisesProvider with ChangeNotifier {
291304

292305
return exercise;
293306
} on NoSuchEntryException {
307+
// _logger.finer('Exercise not found locally, fetching from the API');
294308
return handleUpdateExerciseFromApi(database, exerciseId);
295309
}
296310
}

lib/providers/routines.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,13 +153,14 @@ class RoutinesProvider with ChangeNotifier {
153153
/// Fetches and sets all workout plans fully, i.e. with all corresponding child
154154
/// attributes
155155
Future<void> fetchAndSetAllRoutinesFull() async {
156-
final data = await baseProvider.fetch(
156+
_logger.fine('Fetching all routines fully');
157+
final data = await baseProvider.fetchPaginated(
157158
baseProvider.makeUrl(
158159
_routinesUrlPath,
159160
query: {'ordering': '-creation_date', 'limit': API_MAX_PAGE_SIZE, 'is_template': 'false'},
160161
),
161162
);
162-
for (final entry in data['results']) {
163+
for (final entry in data) {
163164
await fetchAndSetRoutineFull(entry['id']);
164165
}
165166

@@ -169,6 +170,7 @@ class RoutinesProvider with ChangeNotifier {
169170
/// Fetches all routines sparsely, i.e. only with the data on the object itself
170171
/// and no child attributes
171172
Future<void> fetchAndSetAllRoutinesSparse() async {
173+
_logger.fine('Fetching all routines sparsely');
172174
final data = await baseProvider.fetch(
173175
baseProvider.makeUrl(
174176
_routinesUrlPath,

lib/widgets/core/settings.dart

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@
2020
import 'package:flutter/material.dart';
2121
import 'package:provider/provider.dart';
2222
import 'package:wger/l10n/generated/app_localizations.dart';
23-
import 'package:wger/providers/exercises.dart';
2423
import 'package:wger/providers/nutrition.dart';
2524
import 'package:wger/providers/user.dart';
2625
import 'package:wger/screens/configure_plates_screen.dart';
26+
import 'package:wger/widgets/core/settings/exercise_cache.dart';
2727

2828
class SettingsPage extends StatelessWidget {
2929
static String routeName = '/SettingsPage';
@@ -33,7 +33,6 @@ class SettingsPage extends StatelessWidget {
3333
@override
3434
Widget build(BuildContext context) {
3535
final i18n = AppLocalizations.of(context);
36-
final exerciseProvider = Provider.of<ExercisesProvider>(context, listen: false);
3736
final nutritionProvider = Provider.of<NutritionPlansProvider>(context, listen: false);
3837
final userProvider = Provider.of<UserProvider>(context);
3938

@@ -47,24 +46,7 @@ class SettingsPage extends StatelessWidget {
4746
style: Theme.of(context).textTheme.headlineSmall,
4847
),
4948
),
50-
ListTile(
51-
title: Text(i18n.settingsExerciseCacheDescription),
52-
trailing: IconButton(
53-
key: const ValueKey('cacheIconExercises'),
54-
icon: const Icon(Icons.delete),
55-
onPressed: () async {
56-
await exerciseProvider.clearAllCachesAndPrefs();
57-
58-
if (context.mounted) {
59-
final snackBar = SnackBar(
60-
content: Text(i18n.settingsCacheDeletedSnackbar),
61-
);
62-
63-
ScaffoldMessenger.of(context).showSnackBar(snackBar);
64-
}
65-
},
66-
),
67-
),
49+
const SettingsExerciseCache(),
6850
ListTile(
6951
title: Text(i18n.settingsIngredientCacheDescription),
7052
trailing: IconButton(
@@ -83,6 +65,12 @@ class SettingsPage extends StatelessWidget {
8365
},
8466
),
8567
),
68+
ListTile(
69+
title: Text(
70+
i18n.others,
71+
style: Theme.of(context).textTheme.headlineSmall,
72+
),
73+
),
8674
ListTile(
8775
title: Text(i18n.themeMode),
8876
trailing: DropdownButton<ThemeMode>(
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:provider/provider.dart';
3+
import 'package:wger/l10n/generated/app_localizations.dart';
4+
import 'package:wger/providers/exercises.dart';
5+
6+
class SettingsExerciseCache extends StatefulWidget {
7+
const SettingsExerciseCache({super.key});
8+
9+
@override
10+
State<SettingsExerciseCache> createState() => _SettingsExerciseCacheState();
11+
}
12+
13+
class _SettingsExerciseCacheState extends State<SettingsExerciseCache> {
14+
bool _isRefreshLoading = false;
15+
String _subtitle = '';
16+
17+
@override
18+
Widget build(BuildContext context) {
19+
final exerciseProvider = Provider.of<ExercisesProvider>(context, listen: false);
20+
final i18n = AppLocalizations.of(context);
21+
22+
return ListTile(
23+
enabled: !_isRefreshLoading,
24+
title: Text(i18n.settingsExerciseCacheDescription),
25+
subtitle: _subtitle.isNotEmpty ? Text(_subtitle) : null,
26+
trailing: Row(mainAxisSize: MainAxisSize.min, children: [
27+
IconButton(
28+
key: const ValueKey('cacheIconExercisesRefresh'),
29+
icon: _isRefreshLoading
30+
? const SizedBox(
31+
width: 24,
32+
height: 24,
33+
child: CircularProgressIndicator(strokeWidth: 2),
34+
)
35+
: const Icon(Icons.refresh),
36+
onPressed: _isRefreshLoading
37+
? null
38+
: () async {
39+
setState(() => _isRefreshLoading = true);
40+
41+
// Note: status messages are currently left in English on purpose
42+
try {
43+
setState(() => _subtitle = 'Clearing cache...');
44+
await exerciseProvider.clearAllCachesAndPrefs();
45+
46+
if (mounted) {
47+
setState(() => _subtitle = 'Loading languages and units...');
48+
}
49+
await exerciseProvider.fetchAndSetInitialData();
50+
51+
if (mounted) {
52+
setState(() => _subtitle = 'Loading all exercises from server...');
53+
}
54+
await exerciseProvider.fetchAndSetAllExercises();
55+
56+
if (mounted) {
57+
setState(() => _subtitle = '');
58+
}
59+
} finally {
60+
if (mounted) {
61+
setState(() => _isRefreshLoading = false);
62+
}
63+
if (context.mounted) {
64+
ScaffoldMessenger.of(context).showSnackBar(
65+
SnackBar(content: Text(i18n.success)),
66+
);
67+
}
68+
}
69+
},
70+
),
71+
IconButton(
72+
key: const ValueKey('cacheIconExercisesDelete'),
73+
icon: const Icon(Icons.delete),
74+
onPressed: () async {
75+
await exerciseProvider.clearAllCachesAndPrefs();
76+
77+
if (context.mounted) {
78+
final snackBar = SnackBar(
79+
content: Text(i18n.settingsCacheDeletedSnackbar),
80+
);
81+
82+
ScaffoldMessenger.of(context).showSnackBar(snackBar);
83+
}
84+
},
85+
)
86+
]),
87+
);
88+
}
89+
}

test/core/settings_test.dart

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import 'settings_test.mocks.dart';
3939
WgerBaseProvider,
4040
SharedPreferencesAsync,
4141
])
42-
void main() async {
42+
void main() {
4343
final mockExerciseProvider = MockExercisesProvider();
4444
final mockNutritionProvider = MockNutritionPlansProvider();
4545
final mockSharedPreferences = MockSharedPreferencesAsync();
@@ -68,12 +68,22 @@ void main() async {
6868
group('Cache', () {
6969
testWidgets('Test resetting the exercise cache', (WidgetTester tester) async {
7070
await tester.pumpWidget(createSettingsScreen());
71-
await tester.tap(find.byKey(const ValueKey('cacheIconExercises')));
71+
await tester.tap(find.byKey(const ValueKey('cacheIconExercisesDelete')));
7272
await tester.pumpAndSettle();
7373

7474
verify(mockExerciseProvider.clearAllCachesAndPrefs());
7575
});
7676

77+
testWidgets('Test refreshing the exercise cache', (WidgetTester tester) async {
78+
await tester.pumpWidget(createSettingsScreen());
79+
await tester.tap(find.byKey(const ValueKey('cacheIconExercisesRefresh')));
80+
await tester.pumpAndSettle();
81+
82+
verify(mockExerciseProvider.clearAllCachesAndPrefs());
83+
verify(mockExerciseProvider.fetchAndSetInitialData());
84+
verify(mockExerciseProvider.fetchAndSetAllExercises());
85+
});
86+
7787
testWidgets('Test resetting the ingredient cache', (WidgetTester tester) async {
7888
await tester.pumpWidget(createSettingsScreen());
7989
await tester.tap(find.byKey(const ValueKey('cacheIconIngredients')));

test/core/settings_test.mocks.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,16 @@ class MockExercisesProvider extends _i1.Mock implements _i17.ExercisesProvider {
490490
returnValueForMissingStub: _i18.Future<void>.value(),
491491
) as _i18.Future<void>);
492492

493+
@override
494+
_i18.Future<void> fetchAndSetAllExercises() => (super.noSuchMethod(
495+
Invocation.method(
496+
#fetchAndSetAllExercises,
497+
[],
498+
),
499+
returnValue: _i18.Future<void>.value(),
500+
returnValueForMissingStub: _i18.Future<void>.value(),
501+
) as _i18.Future<void>);
502+
493503
@override
494504
_i18.Future<_i4.Exercise?> fetchAndSetExercise(int? exerciseId) => (super.noSuchMethod(
495505
Invocation.method(

test/exercises/contribute_exercise_test.mocks.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -945,6 +945,16 @@ class MockExercisesProvider extends _i1.Mock implements _i20.ExercisesProvider {
945945
returnValueForMissingStub: _i15.Future<void>.value(),
946946
) as _i15.Future<void>);
947947

948+
@override
949+
_i15.Future<void> fetchAndSetAllExercises() => (super.noSuchMethod(
950+
Invocation.method(
951+
#fetchAndSetAllExercises,
952+
[],
953+
),
954+
returnValue: _i15.Future<void>.value(),
955+
returnValueForMissingStub: _i15.Future<void>.value(),
956+
) as _i15.Future<void>);
957+
948958
@override
949959
_i15.Future<_i3.Exercise?> fetchAndSetExercise(int? exerciseId) => (super.noSuchMethod(
950960
Invocation.method(

test/exercises/exercises_detail_widget_test.mocks.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,16 @@ class MockExercisesProvider extends _i1.Mock implements _i9.ExercisesProvider {
377377
returnValueForMissingStub: _i10.Future<void>.value(),
378378
) as _i10.Future<void>);
379379

380+
@override
381+
_i10.Future<void> fetchAndSetAllExercises() => (super.noSuchMethod(
382+
Invocation.method(
383+
#fetchAndSetAllExercises,
384+
[],
385+
),
386+
returnValue: _i10.Future<void>.value(),
387+
returnValueForMissingStub: _i10.Future<void>.value(),
388+
) as _i10.Future<void>);
389+
380390
@override
381391
_i10.Future<_i4.Exercise?> fetchAndSetExercise(int? exerciseId) => (super.noSuchMethod(
382392
Invocation.method(

test/routine/gym_mode_screen_test.mocks.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,16 @@ class MockExercisesProvider extends _i1.Mock implements _i12.ExercisesProvider {
580580
returnValueForMissingStub: _i11.Future<void>.value(),
581581
) as _i11.Future<void>);
582582

583+
@override
584+
_i11.Future<void> fetchAndSetAllExercises() => (super.noSuchMethod(
585+
Invocation.method(
586+
#fetchAndSetAllExercises,
587+
[],
588+
),
589+
returnValue: _i11.Future<void>.value(),
590+
returnValueForMissingStub: _i11.Future<void>.value(),
591+
) as _i11.Future<void>);
592+
583593
@override
584594
_i11.Future<_i6.Exercise?> fetchAndSetExercise(int? exerciseId) => (super.noSuchMethod(
585595
Invocation.method(

test/routine/routines_provider_test.mocks.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,16 @@ class MockExercisesProvider extends _i1.Mock implements _i12.ExercisesProvider {
580580
returnValueForMissingStub: _i11.Future<void>.value(),
581581
) as _i11.Future<void>);
582582

583+
@override
584+
_i11.Future<void> fetchAndSetAllExercises() => (super.noSuchMethod(
585+
Invocation.method(
586+
#fetchAndSetAllExercises,
587+
[],
588+
),
589+
returnValue: _i11.Future<void>.value(),
590+
returnValueForMissingStub: _i11.Future<void>.value(),
591+
) as _i11.Future<void>);
592+
583593
@override
584594
_i11.Future<_i6.Exercise?> fetchAndSetExercise(int? exerciseId) => (super.noSuchMethod(
585595
Invocation.method(

0 commit comments

Comments
 (0)