From f5a591a3b7e8f98c66a9a20e2245957bf11610fa Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Sat, 17 Jan 2026 14:01:17 +0100 Subject: [PATCH] Add trophies to configurable dashboard lists --- lib/main.dart | 2 +- lib/providers/user.dart | 9 +- lib/screens/dashboard.dart | 17 +- .../core/settings/dashboard_visibility.dart | 2 + .../settings_dashboard_visibility_test.dart | 24 +- ...tings_dashboard_visibility_test.mocks.dart | 267 ++++-------------- test/core/validators_test.mocks.dart | 22 ++ test/user/provider_test.dart | 27 +- 8 files changed, 119 insertions(+), 251 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 4f2f1394..956c4874 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -266,7 +266,7 @@ class MainApp extends StatelessWidget { LogOverviewPage.routeName: (ctx) => const LogOverviewPage(), ConfigurePlatesScreen.routeName: (ctx) => const ConfigurePlatesScreen(), ConfigureDashboardWidgetsScreen.routeName: (ctx) => - const ConfigureDashboardWidgetsScreen(), + const ConfigureDashboardWidgetsScreen(), TrophyScreen.routeName: (ctx) => const TrophyScreen(), }, localizationsDelegates: AppLocalizations.localizationsDelegates, diff --git a/lib/providers/user.dart b/lib/providers/user.dart index e2c4c0a8..5e4004e7 100644 --- a/lib/providers/user.dart +++ b/lib/providers/user.dart @@ -28,6 +28,7 @@ import 'package:wger/models/user/profile.dart'; import 'package:wger/providers/base_provider.dart'; enum DashboardWidget { + trophies('trophies'), routines('routines'), nutrition('nutrition'), weight('weight'), @@ -124,7 +125,13 @@ class UserProvider with ChangeNotifier { // Add any missing widgets (e.g. newly added features) for (final widget in DashboardWidget.values) { if (!loaded.any((item) => item.widget == widget)) { - loaded.add(DashboardItem(widget)); + // Try to insert at the original position defined in the enum + // taking into account the current size of the list + var index = DashboardWidget.values.indexOf(widget); + if (index > loaded.length) { + index = loaded.length; + } + loaded.insert(index, DashboardItem(widget)); } } diff --git a/lib/screens/dashboard.dart b/lib/screens/dashboard.dart index 26a9aec2..51319722 100644 --- a/lib/screens/dashboard.dart +++ b/lib/screens/dashboard.dart @@ -18,6 +18,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:wger/helpers/material.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/providers/user.dart'; import 'package:wger/widgets/core/app_bar.dart'; @@ -25,10 +26,9 @@ import 'package:wger/widgets/dashboard/calendar.dart'; import 'package:wger/widgets/dashboard/widgets/measurements.dart'; import 'package:wger/widgets/dashboard/widgets/nutrition.dart'; import 'package:wger/widgets/dashboard/widgets/routines.dart'; +import 'package:wger/widgets/dashboard/widgets/trophies.dart'; import 'package:wger/widgets/dashboard/widgets/weight.dart'; -import '../widgets/dashboard/widgets/trophies.dart'; - class DashboardScreen extends StatelessWidget { const DashboardScreen({super.key}); @@ -46,6 +46,8 @@ class DashboardScreen extends StatelessWidget { return const DashboardCalendarWidget(); case DashboardWidget.nutrition: return const DashboardNutritionWidget(); + case DashboardWidget.trophies: + return const DashboardTrophiesWidget(); } /* child: Column( @@ -60,7 +62,7 @@ class DashboardScreen extends StatelessWidget { Widget build(BuildContext context) { final width = MediaQuery.sizeOf(context).width; final isMobile = width < MATERIAL_XS_BREAKPOINT; - inal user = Provider.of(context); + final user = Provider.of(context); late final int crossAxisCount; if (width < MATERIAL_XS_BREAKPOINT) { @@ -73,22 +75,21 @@ class DashboardScreen extends StatelessWidget { crossAxisCount = 4; } - - return Scaffold( appBar: MainAppBar(AppLocalizations.of(context).labelDashboard), body: Center( child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: MATERIAL_LG_BREAKPOINT.toDouble()), + constraints: const BoxConstraints(maxWidth: MATERIAL_LG_BREAKPOINT), child: isMobile ? ListView.builder( padding: const EdgeInsets.all(10), - itemBuilder: (context, index) => _getDashboardWidget(index), + itemBuilder: (context, index) => _getDashboardWidget(user.dashboardOrder[index]), itemCount: user.dashboardOrder.length, ) : GridView.builder( padding: const EdgeInsets.all(10), - itemBuilder: (context, index) => SingleChildScrollView(child: _getDashboardWidget(index)), + itemBuilder: (context, index) => + SingleChildScrollView(child: _getDashboardWidget(user.dashboardOrder[index])), itemCount: user.dashboardOrder.length, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: crossAxisCount, diff --git a/lib/widgets/core/settings/dashboard_visibility.dart b/lib/widgets/core/settings/dashboard_visibility.dart index b7d110b8..a86b8d5f 100644 --- a/lib/widgets/core/settings/dashboard_visibility.dart +++ b/lib/widgets/core/settings/dashboard_visibility.dart @@ -40,6 +40,8 @@ class SettingsDashboardVisibility extends StatelessWidget { return i18n.calendar; case DashboardWidget.nutrition: return i18n.nutritionalPlans; + case DashboardWidget.trophies: + return i18n.trophies; } } diff --git a/test/core/settings_dashboard_visibility_test.dart b/test/core/settings_dashboard_visibility_test.dart index a171bbd4..1aedab97 100644 --- a/test/core/settings_dashboard_visibility_test.dart +++ b/test/core/settings_dashboard_visibility_test.dart @@ -29,10 +29,7 @@ import 'package:wger/widgets/core/settings/dashboard_visibility.dart'; import 'settings_dashboard_visibility_test.mocks.dart'; -@GenerateMocks([ - UserProvider, - WgerBaseProvider, -]) +@GenerateMocks([WgerBaseProvider]) void main() { late UserProvider userProvider; late MockWgerBaseProvider mockBaseProvider; @@ -98,24 +95,23 @@ void main() { await tester.pumpWidget(createWidget()); await tester.pumpAndSettle(); - // Initial order: routines, nutrition, weight... - expect(userProvider.dashboardOrder[0], DashboardWidget.routines); - expect(userProvider.dashboardOrder[1], DashboardWidget.nutrition); + // Initial order: trophies, routines, nutrition, weight... + expect(userProvider.dashboardOrder[0], DashboardWidget.trophies); + expect(userProvider.dashboardOrder[1], DashboardWidget.routines); - // Find drag handle for Routines (index 0) + // Find drag handle for Trophies (index 0) final handleFinder = find.byIcon(Icons.drag_handle); final firstHandle = handleFinder.at(0); - // final secondHandle = handleFinder.at(1); // Drag first item down await tester.drag(firstHandle, const Offset(0, 100)); // Drag down enough to swap await tester.pumpAndSettle(); // Verify order changed - // If swapped with second item (nutrition) and maybe third (weight) depending on height - // Based on running test: index 0 is nutrition, index 1 is weight. - expect(userProvider.dashboardOrder[0], DashboardWidget.nutrition); - expect(userProvider.dashboardOrder[1], DashboardWidget.weight); - expect(userProvider.dashboardOrder[2], DashboardWidget.routines); + // 100px drag seems to skip 2 items (trophies moves to index 2) + // [routines, nutrition, trophies, ...] + expect(userProvider.dashboardOrder[0], DashboardWidget.routines); + expect(userProvider.dashboardOrder[1], DashboardWidget.nutrition); + expect(userProvider.dashboardOrder[2], DashboardWidget.trophies); }); } diff --git a/test/core/settings_dashboard_visibility_test.mocks.dart b/test/core/settings_dashboard_visibility_test.mocks.dart index 01803e1b..bc9ac783 100644 --- a/test/core/settings_dashboard_visibility_test.mocks.dart +++ b/test/core/settings_dashboard_visibility_test.mocks.dart @@ -3,17 +3,12 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i9; -import 'dart:ui' as _i10; +import 'dart:async' as _i5; -import 'package:flutter/material.dart' as _i7; -import 'package:http/http.dart' as _i5; +import 'package:http/http.dart' as _i3; import 'package:mockito/mockito.dart' as _i1; -import 'package:shared_preferences/shared_preferences.dart' as _i3; -import 'package:wger/models/user/profile.dart' as _i8; -import 'package:wger/providers/auth.dart' as _i4; -import 'package:wger/providers/base_provider.dart' as _i2; -import 'package:wger/providers/user.dart' as _i6; +import 'package:wger/providers/auth.dart' as _i2; +import 'package:wger/providers/base_provider.dart' as _i4; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -30,234 +25,67 @@ import 'package:wger/providers/user.dart' as _i6; // ignore_for_file: subtype_of_sealed_class // ignore_for_file: invalid_use_of_internal_member -class _FakeWgerBaseProvider_0 extends _i1.SmartFake implements _i2.WgerBaseProvider { - _FakeWgerBaseProvider_0(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); +class _FakeAuthProvider_0 extends _i1.SmartFake implements _i2.AuthProvider { + _FakeAuthProvider_0(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); } -class _FakeSharedPreferencesAsync_1 extends _i1.SmartFake implements _i3.SharedPreferencesAsync { - _FakeSharedPreferencesAsync_1(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); +class _FakeClient_1 extends _i1.SmartFake implements _i3.Client { + _FakeClient_1(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); } -class _FakeAuthProvider_2 extends _i1.SmartFake implements _i4.AuthProvider { - _FakeAuthProvider_2(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); +class _FakeUri_2 extends _i1.SmartFake implements Uri { + _FakeUri_2(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); } -class _FakeClient_3 extends _i1.SmartFake implements _i5.Client { - _FakeClient_3(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); -} - -class _FakeUri_4 extends _i1.SmartFake implements Uri { - _FakeUri_4(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); -} - -class _FakeResponse_5 extends _i1.SmartFake implements _i5.Response { - _FakeResponse_5(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); -} - -/// A class which mocks [UserProvider]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockUserProvider extends _i1.Mock implements _i6.UserProvider { - MockUserProvider() { - _i1.throwOnMissingStub(this); - } - - @override - _i7.ThemeMode get themeMode => - (super.noSuchMethod( - Invocation.getter(#themeMode), - returnValue: _i7.ThemeMode.system, - ) - as _i7.ThemeMode); - - @override - _i2.WgerBaseProvider get baseProvider => - (super.noSuchMethod( - Invocation.getter(#baseProvider), - returnValue: _FakeWgerBaseProvider_0( - this, - Invocation.getter(#baseProvider), - ), - ) - as _i2.WgerBaseProvider); - - @override - _i3.SharedPreferencesAsync get prefs => - (super.noSuchMethod( - Invocation.getter(#prefs), - returnValue: _FakeSharedPreferencesAsync_1( - this, - Invocation.getter(#prefs), - ), - ) - as _i3.SharedPreferencesAsync); - - @override - List<_i6.DashboardWidget> get dashboardOrder => - (super.noSuchMethod( - Invocation.getter(#dashboardOrder), - returnValue: <_i6.DashboardWidget>[], - ) - as List<_i6.DashboardWidget>); - - @override - set themeMode(_i7.ThemeMode? value) => super.noSuchMethod( - Invocation.setter(#themeMode, value), - returnValueForMissingStub: null, - ); - - @override - set prefs(_i3.SharedPreferencesAsync? value) => super.noSuchMethod( - Invocation.setter(#prefs, value), - returnValueForMissingStub: null, - ); - - @override - set profile(_i8.Profile? value) => super.noSuchMethod( - Invocation.setter(#profile, value), - returnValueForMissingStub: null, - ); - - @override - bool get hasListeners => - (super.noSuchMethod(Invocation.getter(#hasListeners), returnValue: false) as bool); - - @override - void clear() => super.noSuchMethod( - Invocation.method(#clear, []), - returnValueForMissingStub: null, - ); - - @override - bool isDashboardWidgetVisible(_i6.DashboardWidget? key) => - (super.noSuchMethod( - Invocation.method(#isDashboardWidgetVisible, [key]), - returnValue: false, - ) - as bool); - - @override - _i9.Future setDashboardWidgetVisible( - _i6.DashboardWidget? key, - bool? visible, - ) => - (super.noSuchMethod( - Invocation.method(#setDashboardWidgetVisible, [key, visible]), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); - - @override - _i9.Future setDashboardOrder(int? oldIndex, int? newIndex) => - (super.noSuchMethod( - Invocation.method(#setDashboardOrder, [oldIndex, newIndex]), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); - - @override - void setThemeMode(_i7.ThemeMode? mode) => super.noSuchMethod( - Invocation.method(#setThemeMode, [mode]), - returnValueForMissingStub: null, - ); - - @override - _i9.Future fetchAndSetProfile() => - (super.noSuchMethod( - Invocation.method(#fetchAndSetProfile, []), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); - - @override - _i9.Future saveProfile() => - (super.noSuchMethod( - Invocation.method(#saveProfile, []), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); - - @override - _i9.Future verifyEmail() => - (super.noSuchMethod( - Invocation.method(#verifyEmail, []), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); - - @override - void addListener(_i10.VoidCallback? listener) => super.noSuchMethod( - Invocation.method(#addListener, [listener]), - returnValueForMissingStub: null, - ); - - @override - void removeListener(_i10.VoidCallback? listener) => super.noSuchMethod( - Invocation.method(#removeListener, [listener]), - returnValueForMissingStub: null, - ); - - @override - void dispose() => super.noSuchMethod( - Invocation.method(#dispose, []), - returnValueForMissingStub: null, - ); - - @override - void notifyListeners() => super.noSuchMethod( - Invocation.method(#notifyListeners, []), - returnValueForMissingStub: null, - ); +class _FakeResponse_3 extends _i1.SmartFake implements _i3.Response { + _FakeResponse_3(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); } /// A class which mocks [WgerBaseProvider]. /// /// See the documentation for Mockito's code generation for more information. -class MockWgerBaseProvider extends _i1.Mock implements _i2.WgerBaseProvider { +class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { MockWgerBaseProvider() { _i1.throwOnMissingStub(this); } @override - _i4.AuthProvider get auth => + _i2.AuthProvider get auth => (super.noSuchMethod( Invocation.getter(#auth), - returnValue: _FakeAuthProvider_2(this, Invocation.getter(#auth)), + returnValue: _FakeAuthProvider_0(this, Invocation.getter(#auth)), ) - as _i4.AuthProvider); + as _i2.AuthProvider); @override - _i5.Client get client => + _i3.Client get client => (super.noSuchMethod( Invocation.getter(#client), - returnValue: _FakeClient_3(this, Invocation.getter(#client)), + returnValue: _FakeClient_1(this, Invocation.getter(#client)), ) - as _i5.Client); + as _i3.Client); @override - set auth(_i4.AuthProvider? value) => super.noSuchMethod( + set auth(_i2.AuthProvider? value) => super.noSuchMethod( Invocation.setter(#auth, value), returnValueForMissingStub: null, ); @override - set client(_i5.Client? value) => super.noSuchMethod( + set client(_i3.Client? value) => super.noSuchMethod( Invocation.setter(#client, value), returnValueForMissingStub: null, ); @override - Map getDefaultHeaders({bool? includeAuth = false}) => + Map getDefaultHeaders({ + bool? includeAuth = false, + String? language, + }) => (super.noSuchMethod( Invocation.method(#getDefaultHeaders, [], { #includeAuth: includeAuth, + #language: language, }), returnValue: {}, ) @@ -276,7 +104,7 @@ class MockWgerBaseProvider extends _i1.Mock implements _i2.WgerBaseProvider { [path], {#id: id, #objectMethod: objectMethod, #query: query}, ), - returnValue: _FakeUri_4( + returnValue: _FakeUri_2( this, Invocation.method( #makeUrl, @@ -288,62 +116,67 @@ class MockWgerBaseProvider extends _i1.Mock implements _i2.WgerBaseProvider { as Uri); @override - _i9.Future fetch( + _i5.Future fetch( Uri? uri, { int? maxRetries = 3, Duration? initialDelay = const Duration(milliseconds: 250), + String? language, }) => (super.noSuchMethod( Invocation.method( #fetch, [uri], - {#maxRetries: maxRetries, #initialDelay: initialDelay}, + { + #maxRetries: maxRetries, + #initialDelay: initialDelay, + #language: language, + }, ), - returnValue: _i9.Future.value(), + returnValue: _i5.Future.value(), ) - as _i9.Future); + as _i5.Future); @override - _i9.Future> fetchPaginated(Uri? uri) => + _i5.Future> fetchPaginated(Uri? uri, {String? language}) => (super.noSuchMethod( - Invocation.method(#fetchPaginated, [uri]), - returnValue: _i9.Future>.value([]), + Invocation.method(#fetchPaginated, [uri], {#language: language}), + returnValue: _i5.Future>.value([]), ) - as _i9.Future>); + as _i5.Future>); @override - _i9.Future> post(Map? data, Uri? uri) => + _i5.Future> post(Map? data, Uri? uri) => (super.noSuchMethod( Invocation.method(#post, [data, uri]), - returnValue: _i9.Future>.value( + returnValue: _i5.Future>.value( {}, ), ) - as _i9.Future>); + as _i5.Future>); @override - _i9.Future> patch( + _i5.Future> patch( Map? data, Uri? uri, ) => (super.noSuchMethod( Invocation.method(#patch, [data, uri]), - returnValue: _i9.Future>.value( + returnValue: _i5.Future>.value( {}, ), ) - as _i9.Future>); + as _i5.Future>); @override - _i9.Future<_i5.Response> deleteRequest(String? url, int? id) => + _i5.Future<_i3.Response> deleteRequest(String? url, int? id) => (super.noSuchMethod( Invocation.method(#deleteRequest, [url, id]), - returnValue: _i9.Future<_i5.Response>.value( - _FakeResponse_5( + returnValue: _i5.Future<_i3.Response>.value( + _FakeResponse_3( this, Invocation.method(#deleteRequest, [url, id]), ), ), ) - as _i9.Future<_i5.Response>); + as _i5.Future<_i3.Response>); } diff --git a/test/core/validators_test.mocks.dart b/test/core/validators_test.mocks.dart index f79e0354..b89be452 100644 --- a/test/core/validators_test.mocks.dart +++ b/test/core/validators_test.mocks.dart @@ -2207,6 +2207,17 @@ class MockAppLocalizations extends _i1.Mock implements _i2.AppLocalizations { ) as String); + @override + String get noTrophies => + (super.noSuchMethod( + Invocation.getter(#noTrophies), + returnValue: _i3.dummyValue( + this, + Invocation.getter(#noTrophies), + ), + ) + as String); + @override String get noWeightEntries => (super.noSuchMethod( @@ -3843,6 +3854,17 @@ class MockAppLocalizations extends _i1.Mock implements _i2.AppLocalizations { ) as String); + @override + String topSet(String? value) => + (super.noSuchMethod( + Invocation.method(#topSet, [value]), + returnValue: _i3.dummyValue( + this, + Invocation.method(#topSet, [value]), + ), + ) + as String); + @override String durationHoursMinutes(int? hours, int? minutes) => (super.noSuchMethod( diff --git a/test/user/provider_test.dart b/test/user/provider_test.dart index e12951f8..c5a9206e 100644 --- a/test/user/provider_test.dart +++ b/test/user/provider_test.dart @@ -106,11 +106,12 @@ void main() { group('dashboard config', () { test('initial config should be default (all visible, default order)', () { - expect(userProvider.dashboardOrder.length, 5); + expect(userProvider.dashboardOrder.length, 6); expect( userProvider.dashboardOrder, orderedEquals([ + DashboardWidget.trophies, DashboardWidget.routines, DashboardWidget.nutrition, DashboardWidget.weight, @@ -161,22 +162,28 @@ void main() { // act final newProvider = UserProvider(mockWgerBaseProvider, prefs: prefs); - await Future.delayed(const Duration(milliseconds: 50)); // wait for async prefs load + await Future.delayed(const Duration(milliseconds: 100)); // wait for async prefs load // assert - // The loaded ones come first - expect(newProvider.dashboardOrder[0], DashboardWidget.nutrition); - expect(newProvider.dashboardOrder[1], DashboardWidget.routines); + // Loaded: [nutrition, routines] + // Missing: trophies (0), weight (3), measurements (4), calendar (5) + // 1. trophies (index 0) inserted at 0 -> [trophies, nutrition, routines] + // 2. weight (index 3) inserted at 3 -> [trophies, nutrition, routines, weight] + + expect(newProvider.dashboardOrder[0], DashboardWidget.trophies); + expect(newProvider.dashboardOrder[1], DashboardWidget.nutrition); + expect(newProvider.dashboardOrder[2], DashboardWidget.routines); + expect(newProvider.dashboardOrder[3], DashboardWidget.weight); // Check visibility expect(newProvider.isDashboardWidgetVisible(DashboardWidget.nutrition), true); expect(newProvider.isDashboardWidgetVisible(DashboardWidget.routines), false); - // Remaining items are added after - expect(newProvider.dashboardOrder.length, 5); - - // Items not in the prefs are visible by default - expect(newProvider.isDashboardWidgetVisible(DashboardWidget.weight), true); + // Missing items should be visible by default + expect( + newProvider.isDashboardWidgetVisible(DashboardWidget.weight), + true, + ); }); }); }