diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 7c116d7e..81ac6e4c 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -100,6 +100,7 @@ "@labelWorkoutPlan": { "description": "Title for screen workout plan" }, + "dashboardWidgets": "Dashboard widgets", "labelDashboard": "Dashboard", "@labelDashboard": { "description": "Title for screen dashboard" diff --git a/lib/main.dart b/lib/main.dart index 0eda236e..4f2f1394 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (c) 2025 wger Team + * Copyright (c) 2020 - 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -38,7 +38,6 @@ import 'package:wger/providers/user.dart'; import 'package:wger/providers/wger_base_riverpod.dart'; import 'package:wger/screens/add_exercise_screen.dart'; import 'package:wger/screens/auth_screen.dart'; -import 'package:wger/screens/configure_plates_screen.dart'; import 'package:wger/screens/dashboard.dart'; import 'package:wger/screens/exercise_screen.dart'; import 'package:wger/screens/exercises_screen.dart'; @@ -57,6 +56,8 @@ import 'package:wger/screens/routine_edit_screen.dart'; import 'package:wger/screens/routine_list_screen.dart'; import 'package:wger/screens/routine_logs_screen.dart'; import 'package:wger/screens/routine_screen.dart'; +import 'package:wger/screens/settings_dashboard_widgets_screen.dart'; +import 'package:wger/screens/settings_plates_screen.dart'; import 'package:wger/screens/splash_screen.dart'; import 'package:wger/screens/trophy_screen.dart'; import 'package:wger/screens/update_app_screen.dart'; @@ -264,6 +265,8 @@ class MainApp extends StatelessWidget { SettingsPage.routeName: (ctx) => const SettingsPage(), LogOverviewPage.routeName: (ctx) => const LogOverviewPage(), ConfigurePlatesScreen.routeName: (ctx) => const ConfigurePlatesScreen(), + ConfigureDashboardWidgetsScreen.routeName: (ctx) => + const ConfigureDashboardWidgetsScreen(), TrophyScreen.routeName: (ctx) => const TrophyScreen(), }, localizationsDelegates: AppLocalizations.localizationsDelegates, diff --git a/lib/providers/user.dart b/lib/providers/user.dart index 3765bfe2..e2c4c0a8 100644 --- a/lib/providers/user.dart +++ b/lib/providers/user.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -17,7 +17,9 @@ */ import 'dart:async'; +import 'dart:convert'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:wger/helpers/consts.dart'; @@ -25,6 +27,38 @@ import 'package:wger/helpers/shared_preferences.dart'; import 'package:wger/models/user/profile.dart'; import 'package:wger/providers/base_provider.dart'; +enum DashboardWidget { + routines('routines'), + nutrition('nutrition'), + weight('weight'), + measurements('measurements'), + calendar('calendar'); + + final String value; + const DashboardWidget(this.value); + + static DashboardWidget? fromString(String s) { + for (final e in DashboardWidget.values) { + if (e.value == s) { + return e; + } + } + return null; + } +} + +class DashboardItem { + final DashboardWidget widget; + bool isVisible; + + DashboardItem(this.widget, {this.isVisible = true}); + + Map toJson() => { + 'widget': widget.value, + 'visible': isVisible, + }; +} + class UserProvider with ChangeNotifier { ThemeMode themeMode = ThemeMode.system; final WgerBaseProvider baseProvider; @@ -33,8 +67,10 @@ class UserProvider with ChangeNotifier { UserProvider(this.baseProvider, {SharedPreferencesAsync? prefs}) { this.prefs = prefs ?? PreferenceHelper.asyncPref; _loadThemeMode(); + _loadDashboardConfig(); } + static const String PREFS_DASHBOARD_CONFIG = 'dashboardConfig'; static const PROFILE_URL = 'userprofile'; static const VERIFY_EMAIL = 'verify-email'; @@ -45,15 +81,6 @@ class UserProvider with ChangeNotifier { profile = null; } - // // change the unit of plates - // void changeUnit({changeTo = 'kg'}) { - // if (changeTo == 'kg') { - // profile?.weightUnitStr = 'lb'; - // } else { - // profile?.weightUnitStr = 'kg'; - // } - // } - // Load theme mode from SharedPreferences Future _loadThemeMode() async { final prefsDarkMode = await prefs.getBool(PREFS_USER_DARK_THEME); @@ -67,6 +94,79 @@ class UserProvider with ChangeNotifier { notifyListeners(); } + // Dashboard configuration + List _dashboardItems = DashboardWidget.values + .map((w) => DashboardItem(w)) + .toList(); + + List get dashboardOrder => _dashboardItems.map((e) => e.widget).toList(); + + Future _loadDashboardConfig() async { + final jsonString = await prefs.getString(PREFS_DASHBOARD_CONFIG); + if (jsonString == null) { + notifyListeners(); + return; + } + + try { + final List decoded = jsonDecode(jsonString); + final List loaded = []; + + for (final item in decoded) { + final widget = DashboardWidget.fromString(item['widget']); + if (widget != null) { + loaded.add( + DashboardItem(widget, isVisible: item['visible'] as bool), + ); + } + } + + // 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)); + } + } + + _dashboardItems = loaded; + } catch (_) { + // parsing failed -> keep defaults + } + notifyListeners(); + } + + Future _saveDashboardConfig() async { + final serializable = _dashboardItems.map((e) => e.toJson()).toList(); + await prefs.setString(PREFS_DASHBOARD_CONFIG, jsonEncode(serializable)); + } + + bool isDashboardWidgetVisible(DashboardWidget key) { + final widget = _dashboardItems.firstWhereOrNull((e) => e.widget == key); + return widget == null || widget.isVisible; + } + + Future setDashboardWidgetVisible(DashboardWidget key, bool visible) async { + final item = _dashboardItems.firstWhereOrNull((e) => e.widget == key); + if (item == null) { + return; + } + + item.isVisible = visible; + await _saveDashboardConfig(); + notifyListeners(); + } + + Future setDashboardOrder(int oldIndex, int newIndex) async { + if (oldIndex < newIndex) { + newIndex -= 1; + } + final item = _dashboardItems.removeAt(oldIndex); + _dashboardItems.insert(newIndex, item); + + await _saveDashboardConfig(); + notifyListeners(); + } + // Change mode on switch button click void setThemeMode(ThemeMode mode) async { themeMode = mode; diff --git a/lib/screens/dashboard.dart b/lib/screens/dashboard.dart index 0a714308..26a9aec2 100644 --- a/lib/screens/dashboard.dart +++ b/lib/screens/dashboard.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (c) 2020 - 2025 wger Team + * Copyright (c) 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -17,8 +17,9 @@ */ import 'package:flutter/material.dart'; -import 'package:wger/helpers/material.dart'; +import 'package:provider/provider.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; +import 'package:wger/providers/user.dart'; import 'package:wger/widgets/core/app_bar.dart'; import 'package:wger/widgets/dashboard/calendar.dart'; import 'package:wger/widgets/dashboard/widgets/measurements.dart'; @@ -33,10 +34,33 @@ class DashboardScreen extends StatelessWidget { static const routeName = '/dashboard'; + Widget _getDashboardWidget(DashboardWidget widget) { + switch (widget) { + case DashboardWidget.routines: + return const DashboardRoutineWidget(); + case DashboardWidget.weight: + return const DashboardWeightWidget(); + case DashboardWidget.measurements: + return const DashboardMeasurementWidget(); + case DashboardWidget.calendar: + return const DashboardCalendarWidget(); + case DashboardWidget.nutrition: + return const DashboardNutritionWidget(); + } + /* + child: Column( + children: user.dashboardOrder + .where((w) => user.isDashboardWidgetVisible(w)) + .map(_getDashboardWidget) + .toList(), + */ + } + @override Widget build(BuildContext context) { final width = MediaQuery.sizeOf(context).width; final isMobile = width < MATERIAL_XS_BREAKPOINT; + inal user = Provider.of(context); late final int crossAxisCount; if (width < MATERIAL_XS_BREAKPOINT) { @@ -49,14 +73,7 @@ class DashboardScreen extends StatelessWidget { crossAxisCount = 4; } - final items = [ - const DashboardTrophiesWidget(), - const DashboardRoutineWidget(), - const DashboardNutritionWidget(), - const DashboardWeightWidget(), - const DashboardMeasurementWidget(), - const DashboardCalendarWidget(), - ]; + return Scaffold( appBar: MainAppBar(AppLocalizations.of(context).labelDashboard), @@ -66,13 +83,13 @@ class DashboardScreen extends StatelessWidget { child: isMobile ? ListView.builder( padding: const EdgeInsets.all(10), - itemBuilder: (context, index) => items[index], - itemCount: items.length, + itemBuilder: (context, index) => _getDashboardWidget(index), + itemCount: user.dashboardOrder.length, ) : GridView.builder( padding: const EdgeInsets.all(10), - itemBuilder: (context, index) => SingleChildScrollView(child: items[index]), - itemCount: items.length, + itemBuilder: (context, index) => SingleChildScrollView(child: _getDashboardWidget(index)), + itemCount: user.dashboardOrder.length, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: crossAxisCount, childAspectRatio: 0.7, diff --git a/lib/screens/settings_dashboard_widgets_screen.dart b/lib/screens/settings_dashboard_widgets_screen.dart new file mode 100644 index 00000000..d3be7771 --- /dev/null +++ b/lib/screens/settings_dashboard_widgets_screen.dart @@ -0,0 +1,38 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2026 wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'package:flutter/material.dart'; +import 'package:wger/core/wide_screen_wrapper.dart'; +import 'package:wger/l10n/generated/app_localizations.dart'; +import 'package:wger/widgets/core/settings/dashboard_visibility.dart'; + +class ConfigureDashboardWidgetsScreen extends StatelessWidget { + static const routeName = '/ConfigureDashboardWidgetScreen'; + + const ConfigureDashboardWidgetsScreen({super.key}); + + @override + Widget build(BuildContext context) { + final i18n = AppLocalizations.of(context); + + return Scaffold( + appBar: AppBar(title: Text(i18n.dashboardWidgets)), + body: const WidescreenWrapper(child: SettingsDashboardVisibility()), + ); + } +} diff --git a/lib/screens/configure_plates_screen.dart b/lib/screens/settings_plates_screen.dart similarity index 100% rename from lib/screens/configure_plates_screen.dart rename to lib/screens/settings_plates_screen.dart diff --git a/lib/widgets/core/app_bar.dart b/lib/widgets/core/app_bar.dart index cf0948d5..be52f063 100644 --- a/lib/widgets/core/app_bar.dart +++ b/lib/widgets/core/app_bar.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -26,6 +26,7 @@ import 'package:wger/providers/nutrition.dart'; import 'package:wger/providers/routines.dart'; import 'package:wger/providers/user.dart'; import 'package:wger/screens/form_screen.dart'; +import 'package:wger/screens/settings_dashboard_widgets_screen.dart'; import 'package:wger/widgets/core/about.dart'; import 'package:wger/widgets/core/settings.dart'; import 'package:wger/widgets/user/forms.dart'; @@ -40,71 +41,19 @@ class MainAppBar extends StatelessWidget implements PreferredSizeWidget { return AppBar( title: Text(_title), actions: [ + IconButton( + icon: const Icon(Icons.widgets_outlined), + onPressed: () { + Navigator.of(context).pushNamed(ConfigureDashboardWidgetsScreen.routeName); + }, + ), IconButton( icon: const Icon(Icons.settings), onPressed: () async { return showDialog( context: context, builder: (BuildContext context) { - return AlertDialog( - title: Text(AppLocalizations.of(context).optionsLabel), - actions: [ - TextButton( - child: Text( - MaterialLocalizations.of(context).closeButtonLabel, - ), - onPressed: () => Navigator.of(context).pop(), - ), - ], - contentPadding: EdgeInsets.zero, - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - //dense: true, - leading: const Icon(Icons.person), - title: Text(AppLocalizations.of(context).userProfile), - onTap: () => Navigator.pushNamed( - context, - FormScreen.routeName, - arguments: FormScreenArguments( - AppLocalizations.of(context).userProfile, - UserProfileForm( - context.read().profile!, - ), - ), - ), - ), - ListTile( - leading: const Icon(Icons.settings), - onTap: () => Navigator.of(context).pushNamed(SettingsPage.routeName), - title: Text(AppLocalizations.of(context).settingsTitle), - ), - ListTile( - leading: const Icon(Icons.info), - onTap: () => Navigator.of(context).pushNamed(AboutPage.routeName), - title: Text(AppLocalizations.of(context).aboutPageTitle), - ), - const Divider(), - ListTile( - //dense: true, - leading: const Icon(Icons.exit_to_app), - title: Text(AppLocalizations.of(context).logout), - onTap: () { - context.read().logout(); - context.read().clear(); - context.read().clear(); - context.read().clear(); - context.read().clear(); - context.read().clear(); - - Navigator.of(context).pop(); - Navigator.of(context).pushReplacementNamed('/'); - }, - ), - ], - ), - ); + return const MainSettingsDialog(); }, ); }, @@ -117,6 +66,73 @@ class MainAppBar extends StatelessWidget implements PreferredSizeWidget { Size get preferredSize => const Size.fromHeight(kToolbarHeight); } +class MainSettingsDialog extends StatelessWidget { + const MainSettingsDialog({super.key}); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(AppLocalizations.of(context).optionsLabel), + actions: [ + TextButton( + child: Text( + MaterialLocalizations.of(context).closeButtonLabel, + ), + onPressed: () => Navigator.of(context).pop(), + ), + ], + contentPadding: EdgeInsets.zero, + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + //dense: true, + leading: const Icon(Icons.person), + title: Text(AppLocalizations.of(context).userProfile), + onTap: () => Navigator.pushNamed( + context, + FormScreen.routeName, + arguments: FormScreenArguments( + AppLocalizations.of(context).userProfile, + UserProfileForm( + context.read().profile!, + ), + ), + ), + ), + ListTile( + leading: const Icon(Icons.settings), + onTap: () => Navigator.of(context).pushNamed(SettingsPage.routeName), + title: Text(AppLocalizations.of(context).settingsTitle), + ), + ListTile( + leading: const Icon(Icons.info), + onTap: () => Navigator.of(context).pushNamed(AboutPage.routeName), + title: Text(AppLocalizations.of(context).aboutPageTitle), + ), + const Divider(), + ListTile( + //dense: true, + leading: const Icon(Icons.exit_to_app), + title: Text(AppLocalizations.of(context).logout), + onTap: () { + context.read().logout(); + context.read().clear(); + context.read().clear(); + context.read().clear(); + context.read().clear(); + context.read().clear(); + + Navigator.of(context).pop(); + Navigator.of(context).pushReplacementNamed('/'); + }, + ), + ], + ), + ); + } +} + /// App bar that only displays a title class EmptyAppBar extends StatelessWidget implements PreferredSizeWidget { final String _title; diff --git a/lib/widgets/core/settings.dart b/lib/widgets/core/settings.dart index 3562c545..014b281d 100644 --- a/lib/widgets/core/settings.dart +++ b/lib/widgets/core/settings.dart @@ -1,13 +1,13 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) wger Team + * Copyright (c) 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * wger Workout Manager is distributed in the hope that it will be useful, + * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. @@ -16,14 +16,13 @@ * along with this program. If not, see . */ -//import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; -import 'package:wger/core/wide_screen_wrapper.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; -import 'package:wger/screens/configure_plates_screen.dart'; -import 'package:wger/widgets/core/settings/exercise_cache.dart'; -import 'package:wger/widgets/core/settings/ingredient_cache.dart'; -import 'package:wger/widgets/core/settings/theme.dart'; +import 'package:wger/screens/settings_plates_screen.dart'; + +import './settings/exercise_cache.dart'; +import './settings/ingredient_cache.dart'; +import './settings/theme.dart'; class SettingsPage extends StatelessWidget { static String routeName = '/SettingsPage'; @@ -36,28 +35,23 @@ class SettingsPage extends StatelessWidget { return Scaffold( appBar: AppBar(title: Text(i18n.settingsTitle)), - body: WidescreenWrapper( - child: ListView( - children: [ - ListTile( - title: Text( - i18n.settingsCacheTitle, - style: Theme.of(context).textTheme.headlineSmall, - ), - ), - const SettingsExerciseCache(), - const SettingsIngredientCache(), - ListTile(title: Text(i18n.others, style: Theme.of(context).textTheme.headlineSmall)), - const SettingsTheme(), - ListTile( - title: Text(i18n.selectAvailablePlates), - onTap: () { - Navigator.of(context).pushNamed(ConfigurePlatesScreen.routeName); - }, - trailing: const Icon(Icons.chevron_right), - ), - ], - ), + body: ListView( + children: [ + ListTile( + title: Text(i18n.settingsCacheTitle, style: Theme.of(context).textTheme.headlineSmall), + ), + const SettingsExerciseCache(), + const SettingsIngredientCache(), + ListTile(title: Text(i18n.others, style: Theme.of(context).textTheme.headlineSmall)), + const SettingsTheme(), + ListTile( + title: Text(i18n.selectAvailablePlates), + onTap: () { + Navigator.of(context).pushNamed(ConfigurePlatesScreen.routeName); + }, + trailing: const Icon(Icons.chevron_right), + ), + ], ), ); } diff --git a/lib/widgets/core/settings/dashboard_visibility.dart b/lib/widgets/core/settings/dashboard_visibility.dart new file mode 100644 index 00000000..b7d110b8 --- /dev/null +++ b/lib/widgets/core/settings/dashboard_visibility.dart @@ -0,0 +1,78 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2026 wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:wger/l10n/generated/app_localizations.dart'; +import 'package:wger/providers/user.dart'; + +class SettingsDashboardVisibility extends StatelessWidget { + const SettingsDashboardVisibility({super.key}); + + @override + Widget build(BuildContext context) { + final i18n = AppLocalizations.of(context); + + String getTitle(DashboardWidget w) { + switch (w) { + case DashboardWidget.routines: + return i18n.routines; + case DashboardWidget.weight: + return i18n.weight; + case DashboardWidget.measurements: + return i18n.measurements; + case DashboardWidget.calendar: + return i18n.calendar; + case DashboardWidget.nutrition: + return i18n.nutritionalPlans; + } + } + + return Consumer( + builder: (context, user, _) { + return ReorderableListView( + physics: const NeverScrollableScrollPhysics(), + buildDefaultDragHandles: false, + onReorder: user.setDashboardOrder, + children: user.dashboardOrder.asMap().entries.map((entry) { + final index = entry.key; + final w = entry.value; + + return ListTile( + key: ValueKey(w), + title: Text(getTitle(w)), + leading: IconButton( + icon: user.isDashboardWidgetVisible(w) + ? const Icon(Icons.visibility) + : const Icon(Icons.visibility_off, color: Colors.grey), + onPressed: () => user.setDashboardWidgetVisible( + w, + !user.isDashboardWidgetVisible(w), + ), + ), + trailing: ReorderableDragStartListener( + index: index, + child: const Icon(Icons.drag_handle), + ), + ); + }).toList(), + ); + }, + ); + } +} diff --git a/lib/widgets/routines/gym_mode/log_page.dart b/lib/widgets/routines/gym_mode/log_page.dart index 088836d6..49865e7e 100644 --- a/lib/widgets/routines/gym_mode/log_page.dart +++ b/lib/widgets/routines/gym_mode/log_page.dart @@ -31,7 +31,7 @@ import 'package:wger/providers/gym_log_state.dart'; import 'package:wger/providers/gym_state.dart'; import 'package:wger/providers/plate_weights.dart'; import 'package:wger/providers/routines.dart'; -import 'package:wger/screens/configure_plates_screen.dart'; +import 'package:wger/screens/settings_plates_screen.dart'; import 'package:wger/widgets/core/core.dart'; import 'package:wger/widgets/core/progress_indicator.dart'; import 'package:wger/widgets/routines/forms/reps_unit.dart'; diff --git a/test/core/settings_dashboard_visibility_test.dart b/test/core/settings_dashboard_visibility_test.dart new file mode 100644 index 00000000..a171bbd4 --- /dev/null +++ b/test/core/settings_dashboard_visibility_test.dart @@ -0,0 +1,121 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2026 wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:provider/provider.dart'; +import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart'; +import 'package:wger/l10n/generated/app_localizations.dart'; +import 'package:wger/providers/base_provider.dart'; +import 'package:wger/providers/user.dart'; +import 'package:wger/widgets/core/settings/dashboard_visibility.dart'; + +import 'settings_dashboard_visibility_test.mocks.dart'; + +@GenerateMocks([ + UserProvider, + WgerBaseProvider, +]) +void main() { + late UserProvider userProvider; + late MockWgerBaseProvider mockBaseProvider; + + setUp(() { + SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty(); + mockBaseProvider = MockWgerBaseProvider(); + userProvider = UserProvider(mockBaseProvider); + }); + + Widget createWidget() { + return ChangeNotifierProvider.value( + value: userProvider, + child: const MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Scaffold( + body: SettingsDashboardVisibility(), + ), + ), + ); + } + + testWidgets('renders list of dashboard widgets', (tester) async { + await tester.pumpWidget(createWidget()); + await tester.pumpAndSettle(); + + // Verify all items are present + expect(find.byType(ListTile), findsNWidgets(DashboardWidget.values.length)); + expect(find.text('Routines'), findsOneWidget); + }); + + testWidgets('toggle visibility updates provider', (tester) async { + await tester.pumpWidget(createWidget()); + await tester.pumpAndSettle(); + + // Routines should be visible initally (default is true) + expect(userProvider.isDashboardWidgetVisible(DashboardWidget.routines), true); + + // Find the visibility icon for Routines + final routineTile = find.byKey(const ValueKey(DashboardWidget.routines)); + final iconBtn = find.descendant(of: routineTile, matching: find.byType(IconButton)); + + // Check icon is 'visibility' + expect(find.descendant(of: iconBtn, matching: find.byIcon(Icons.visibility)), findsOneWidget); + + // Tap to toggle + await tester.tap(iconBtn); + await tester.pump(); // re-render + + // Check provider state + expect(userProvider.isDashboardWidgetVisible(DashboardWidget.routines), false); + + // Check icon is 'visibility_off' + expect( + find.descendant(of: iconBtn, matching: find.byIcon(Icons.visibility_off)), + findsOneWidget, + ); + }); + + // Reordering test is a bit flaky without full drag setup, but we can try + testWidgets('dragging reorders items', (tester) async { + await tester.pumpWidget(createWidget()); + await tester.pumpAndSettle(); + + // Initial order: routines, nutrition, weight... + expect(userProvider.dashboardOrder[0], DashboardWidget.routines); + expect(userProvider.dashboardOrder[1], DashboardWidget.nutrition); + + // Find drag handle for Routines (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); + }); +} diff --git a/test/core/settings_dashboard_visibility_test.mocks.dart b/test/core/settings_dashboard_visibility_test.mocks.dart new file mode 100644 index 00000000..01803e1b --- /dev/null +++ b/test/core/settings_dashboard_visibility_test.mocks.dart @@ -0,0 +1,349 @@ +// Mocks generated by Mockito 5.4.6 from annotations +// in wger/test/core/settings_dashboard_visibility_test.dart. +// 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 'package:flutter/material.dart' as _i7; +import 'package:http/http.dart' as _i5; +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; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// 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 _FakeSharedPreferencesAsync_1 extends _i1.SmartFake implements _i3.SharedPreferencesAsync { + _FakeSharedPreferencesAsync_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 _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, + ); +} + +/// A class which mocks [WgerBaseProvider]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWgerBaseProvider extends _i1.Mock implements _i2.WgerBaseProvider { + MockWgerBaseProvider() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.AuthProvider get auth => + (super.noSuchMethod( + Invocation.getter(#auth), + returnValue: _FakeAuthProvider_2(this, Invocation.getter(#auth)), + ) + as _i4.AuthProvider); + + @override + _i5.Client get client => + (super.noSuchMethod( + Invocation.getter(#client), + returnValue: _FakeClient_3(this, Invocation.getter(#client)), + ) + as _i5.Client); + + @override + set auth(_i4.AuthProvider? value) => super.noSuchMethod( + Invocation.setter(#auth, value), + returnValueForMissingStub: null, + ); + + @override + set client(_i5.Client? value) => super.noSuchMethod( + Invocation.setter(#client, value), + returnValueForMissingStub: null, + ); + + @override + Map getDefaultHeaders({bool? includeAuth = false}) => + (super.noSuchMethod( + Invocation.method(#getDefaultHeaders, [], { + #includeAuth: includeAuth, + }), + returnValue: {}, + ) + as Map); + + @override + Uri makeUrl( + String? path, { + int? id, + String? objectMethod, + Map? query, + }) => + (super.noSuchMethod( + Invocation.method( + #makeUrl, + [path], + {#id: id, #objectMethod: objectMethod, #query: query}, + ), + returnValue: _FakeUri_4( + this, + Invocation.method( + #makeUrl, + [path], + {#id: id, #objectMethod: objectMethod, #query: query}, + ), + ), + ) + as Uri); + + @override + _i9.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 250), + }) => + (super.noSuchMethod( + Invocation.method( + #fetch, + [uri], + {#maxRetries: maxRetries, #initialDelay: initialDelay}, + ), + returnValue: _i9.Future.value(), + ) + as _i9.Future); + + @override + _i9.Future> fetchPaginated(Uri? uri) => + (super.noSuchMethod( + Invocation.method(#fetchPaginated, [uri]), + returnValue: _i9.Future>.value([]), + ) + as _i9.Future>); + + @override + _i9.Future> post(Map? data, Uri? uri) => + (super.noSuchMethod( + Invocation.method(#post, [data, uri]), + returnValue: _i9.Future>.value( + {}, + ), + ) + as _i9.Future>); + + @override + _i9.Future> patch( + Map? data, + Uri? uri, + ) => + (super.noSuchMethod( + Invocation.method(#patch, [data, uri]), + returnValue: _i9.Future>.value( + {}, + ), + ) + as _i9.Future>); + + @override + _i9.Future<_i5.Response> deleteRequest(String? url, int? id) => + (super.noSuchMethod( + Invocation.method(#deleteRequest, [url, id]), + returnValue: _i9.Future<_i5.Response>.value( + _FakeResponse_5( + this, + Invocation.method(#deleteRequest, [url, id]), + ), + ), + ) + as _i9.Future<_i5.Response>); +} diff --git a/test/core/settings_test.dart b/test/core/settings_test.dart index 474603c9..f4869b4e 100644 --- a/test/core/settings_test.dart +++ b/test/core/settings_test.dart @@ -1,13 +1,13 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * wger Workout Manager is distributed in the hope that it will be useful, + * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. @@ -49,6 +49,9 @@ void main() { setUp(() { when(mockUserProvider.themeMode).thenReturn(ThemeMode.system); + when( + mockSharedPreferences.getString(UserProvider.PREFS_DASHBOARD_CONFIG), + ).thenAnswer((_) async => null); when(mockExerciseProvider.exercises).thenReturn(getTestExercises()); when(mockNutritionProvider.ingredients).thenReturn([ingredient1, ingredient2]); }); @@ -100,18 +103,23 @@ void main() { group('Theme settings', () { test('Default theme is system', () async { when(mockSharedPreferences.getBool(PREFS_USER_DARK_THEME)).thenAnswer((_) async => null); - final userProvider = await UserProvider(MockWgerBaseProvider(), prefs: mockSharedPreferences); + final userProvider = UserProvider(MockWgerBaseProvider(), prefs: mockSharedPreferences); + await Future.delayed(const Duration(milliseconds: 50)); // wait for async prefs load expect(userProvider.themeMode, ThemeMode.system); }); test('Loads light theme', () async { when(mockSharedPreferences.getBool(PREFS_USER_DARK_THEME)).thenAnswer((_) async => false); - final userProvider = await UserProvider(MockWgerBaseProvider(), prefs: mockSharedPreferences); + final userProvider = UserProvider(MockWgerBaseProvider(), prefs: mockSharedPreferences); + await Future.delayed(const Duration(milliseconds: 50)); // wait for async prefs load expect(userProvider.themeMode, ThemeMode.light); }); test('Saves theme to prefs', () { when(mockSharedPreferences.getBool(any)).thenAnswer((_) async => null); + when( + mockSharedPreferences.getString('dashboardWidgetVisibility'), + ).thenAnswer((_) async => null); final userProvider = UserProvider(MockWgerBaseProvider(), prefs: mockSharedPreferences); userProvider.setThemeMode(ThemeMode.dark); verify(mockSharedPreferences.setBool(PREFS_USER_DARK_THEME, true)).called(1); diff --git a/test/core/settings_test.mocks.dart b/test/core/settings_test.mocks.dart index a6192cd8..0c0b6b32 100644 --- a/test/core/settings_test.mocks.dart +++ b/test/core/settings_test.mocks.dart @@ -943,6 +943,14 @@ class MockUserProvider extends _i1.Mock implements _i21.UserProvider { ) as _i14.SharedPreferencesAsync); + @override + List<_i21.DashboardWidget> get dashboardOrder => + (super.noSuchMethod( + Invocation.getter(#dashboardOrder), + returnValue: <_i21.DashboardWidget>[], + ) + as List<_i21.DashboardWidget>); + @override set themeMode(_i22.ThemeMode? value) => super.noSuchMethod( Invocation.setter(#themeMode, value), @@ -971,6 +979,35 @@ class MockUserProvider extends _i1.Mock implements _i21.UserProvider { returnValueForMissingStub: null, ); + @override + bool isDashboardWidgetVisible(_i21.DashboardWidget? key) => + (super.noSuchMethod( + Invocation.method(#isDashboardWidgetVisible, [key]), + returnValue: false, + ) + as bool); + + @override + _i18.Future setDashboardWidgetVisible( + _i21.DashboardWidget? key, + bool? visible, + ) => + (super.noSuchMethod( + Invocation.method(#setDashboardWidgetVisible, [key, visible]), + returnValue: _i18.Future.value(), + returnValueForMissingStub: _i18.Future.value(), + ) + as _i18.Future); + + @override + _i18.Future setDashboardOrder(int? oldIndex, int? newIndex) => + (super.noSuchMethod( + Invocation.method(#setDashboardOrder, [oldIndex, newIndex]), + returnValue: _i18.Future.value(), + returnValueForMissingStub: _i18.Future.value(), + ) + as _i18.Future); + @override void setThemeMode(_i22.ThemeMode? mode) => super.noSuchMethod( Invocation.method(#setThemeMode, [mode]), diff --git a/test/core/validators_test.mocks.dart b/test/core/validators_test.mocks.dart index fb63573c..f79e0354 100644 --- a/test/core/validators_test.mocks.dart +++ b/test/core/validators_test.mocks.dart @@ -404,6 +404,17 @@ class MockAppLocalizations extends _i1.Mock implements _i2.AppLocalizations { ) as String); + @override + String get dashboardWidgets => + (super.noSuchMethod( + Invocation.getter(#dashboardWidgets), + returnValue: _i3.dummyValue( + this, + Invocation.getter(#dashboardWidgets), + ), + ) + as String); + @override String get labelDashboard => (super.noSuchMethod( diff --git a/test/exercises/contribute_exercise_test.mocks.dart b/test/exercises/contribute_exercise_test.mocks.dart index 726b8a14..dd9f98e8 100644 --- a/test/exercises/contribute_exercise_test.mocks.dart +++ b/test/exercises/contribute_exercise_test.mocks.dart @@ -389,6 +389,14 @@ class MockUserProvider extends _i1.Mock implements _i17.UserProvider { ) as _i4.SharedPreferencesAsync); + @override + List<_i17.DashboardWidget> get dashboardOrder => + (super.noSuchMethod( + Invocation.getter(#dashboardOrder), + returnValue: <_i17.DashboardWidget>[], + ) + as List<_i17.DashboardWidget>); + @override set themeMode(_i18.ThemeMode? value) => super.noSuchMethod( Invocation.setter(#themeMode, value), @@ -417,6 +425,35 @@ class MockUserProvider extends _i1.Mock implements _i17.UserProvider { returnValueForMissingStub: null, ); + @override + bool isDashboardWidgetVisible(_i17.DashboardWidget? key) => + (super.noSuchMethod( + Invocation.method(#isDashboardWidgetVisible, [key]), + returnValue: false, + ) + as bool); + + @override + _i15.Future setDashboardWidgetVisible( + _i17.DashboardWidget? key, + bool? visible, + ) => + (super.noSuchMethod( + Invocation.method(#setDashboardWidgetVisible, [key, visible]), + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) + as _i15.Future); + + @override + _i15.Future setDashboardOrder(int? oldIndex, int? newIndex) => + (super.noSuchMethod( + Invocation.method(#setDashboardOrder, [oldIndex, newIndex]), + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) + as _i15.Future); + @override void setThemeMode(_i18.ThemeMode? mode) => super.noSuchMethod( Invocation.method(#setThemeMode, [mode]), diff --git a/test/user/provider_test.dart b/test/user/provider_test.dart index 8297b330..e12951f8 100644 --- a/test/user/provider_test.dart +++ b/test/user/provider_test.dart @@ -1,13 +1,13 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * wger Workout Manager is distributed in the hope that it will be useful, + * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. @@ -21,6 +21,7 @@ import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart'; import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart'; import 'package:wger/providers/base_provider.dart'; @@ -51,7 +52,6 @@ void main() { ); setUp(() { - /// Replacement for SharedPreferences.setMockInitialValues() SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty(); mockWgerBaseProvider = MockWgerBaseProvider(); userProvider = UserProvider(mockWgerBaseProvider); @@ -103,4 +103,80 @@ void main() { verify(userProvider.baseProvider.fetch(tEmailVerifyUri)); }); }); + + group('dashboard config', () { + test('initial config should be default (all visible, default order)', () { + expect(userProvider.dashboardOrder.length, 5); + + expect( + userProvider.dashboardOrder, + orderedEquals([ + DashboardWidget.routines, + DashboardWidget.nutrition, + DashboardWidget.weight, + DashboardWidget.measurements, + DashboardWidget.calendar, + ]), + ); + expect(userProvider.isDashboardWidgetVisible(DashboardWidget.routines), true); + }); + + test('toggling visibility should update state', () async { + // act + await userProvider.setDashboardWidgetVisible(DashboardWidget.routines, false); + + // assert + expect(userProvider.isDashboardWidgetVisible(DashboardWidget.routines), false); + + // re-enable + await userProvider.setDashboardWidgetVisible(DashboardWidget.routines, true); + expect(userProvider.isDashboardWidgetVisible(DashboardWidget.routines), true); + }); + + test('reordering should update order', () async { + // arrange + final initialFirst = userProvider.dashboardOrder[0]; + final initialSecond = userProvider.dashboardOrder[1]; + + // act: move first to second position + // oldIndex: 0, newIndex: 2 (because insert is before index) + await userProvider.setDashboardOrder(0, 2); + + // assert + expect(userProvider.dashboardOrder[0], initialSecond); + expect(userProvider.dashboardOrder[1], initialFirst); + }); + + test('should load config from prefs', () async { + // arrange + final prefs = SharedPreferencesAsync(); + final customConfig = [ + {'widget': 'nutrition', 'visible': true}, + {'widget': 'routines', 'visible': false}, + ]; + await prefs.setString( + UserProvider.PREFS_DASHBOARD_CONFIG, + jsonEncode(customConfig), + ); + + // act + final newProvider = UserProvider(mockWgerBaseProvider, prefs: prefs); + await Future.delayed(const Duration(milliseconds: 50)); // wait for async prefs load + + // assert + // The loaded ones come first + expect(newProvider.dashboardOrder[0], DashboardWidget.nutrition); + expect(newProvider.dashboardOrder[1], DashboardWidget.routines); + + // 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); + }); + }); } diff --git a/test/weight/weight_screen_test.mocks.dart b/test/weight/weight_screen_test.mocks.dart index 8f508d55..ae2d83ee 100644 --- a/test/weight/weight_screen_test.mocks.dart +++ b/test/weight/weight_screen_test.mocks.dart @@ -231,6 +231,14 @@ class MockUserProvider extends _i1.Mock implements _i13.UserProvider { ) as _i4.SharedPreferencesAsync); + @override + List<_i13.DashboardWidget> get dashboardOrder => + (super.noSuchMethod( + Invocation.getter(#dashboardOrder), + returnValue: <_i13.DashboardWidget>[], + ) + as List<_i13.DashboardWidget>); + @override set themeMode(_i14.ThemeMode? value) => super.noSuchMethod( Invocation.setter(#themeMode, value), @@ -259,6 +267,35 @@ class MockUserProvider extends _i1.Mock implements _i13.UserProvider { returnValueForMissingStub: null, ); + @override + bool isDashboardWidgetVisible(_i13.DashboardWidget? key) => + (super.noSuchMethod( + Invocation.method(#isDashboardWidgetVisible, [key]), + returnValue: false, + ) + as bool); + + @override + _i11.Future setDashboardWidgetVisible( + _i13.DashboardWidget? key, + bool? visible, + ) => + (super.noSuchMethod( + Invocation.method(#setDashboardWidgetVisible, [key, visible]), + returnValue: _i11.Future.value(), + returnValueForMissingStub: _i11.Future.value(), + ) + as _i11.Future); + + @override + _i11.Future setDashboardOrder(int? oldIndex, int? newIndex) => + (super.noSuchMethod( + Invocation.method(#setDashboardOrder, [oldIndex, newIndex]), + returnValue: _i11.Future.value(), + returnValueForMissingStub: _i11.Future.value(), + ) + as _i11.Future); + @override void setThemeMode(_i14.ThemeMode? mode) => super.noSuchMethod( Invocation.method(#setThemeMode, [mode]),