diff --git a/ios/Flutter/ephemeral/flutter_lldb_helper.py b/ios/Flutter/ephemeral/flutter_lldb_helper.py new file mode 100644 index 000000000..a88caf99d --- /dev/null +++ b/ios/Flutter/ephemeral/flutter_lldb_helper.py @@ -0,0 +1,32 @@ +# +# Generated file, do not edit. +# + +import lldb + +def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict): + """Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages.""" + base = frame.register["x0"].GetValueAsAddress() + page_len = frame.register["x1"].GetValueAsUnsigned() + + # Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the + # first page to see if handled it correctly. This makes diagnosing + # misconfiguration (e.g. missing breakpoint) easier. + data = bytearray(page_len) + data[0:8] = b'IHELPED!' + + error = lldb.SBError() + frame.GetThread().GetProcess().WriteMemory(base, data, error) + if not error.Success(): + print(f'Failed to write into {base}[+{page_len}]', error) + return + +def __lldb_init_module(debugger: lldb.SBDebugger, _): + target = debugger.GetDummyTarget() + # Caveat: must use BreakpointCreateByRegEx here and not + # BreakpointCreateByName. For some reasons callback function does not + # get carried over from dummy target for the later. + bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$") + bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__)) + bp.SetAutoContinue(True) + print("-- LLDB integration loaded --") diff --git a/ios/Flutter/ephemeral/flutter_lldbinit b/ios/Flutter/ephemeral/flutter_lldbinit new file mode 100644 index 000000000..e3ba6fbed --- /dev/null +++ b/ios/Flutter/ephemeral/flutter_lldbinit @@ -0,0 +1,5 @@ +# +# Generated file, do not edit. +# + +command script import --relative-to-command-file flutter_lldb_helper.py diff --git a/lib/providers/user.dart b/lib/providers/user.dart index 3765bfe26..27f7516d5 100644 --- a/lib/providers/user.dart +++ b/lib/providers/user.dart @@ -17,6 +17,7 @@ */ import 'dart:async'; +import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -29,10 +30,23 @@ class UserProvider with ChangeNotifier { ThemeMode themeMode = ThemeMode.system; final WgerBaseProvider baseProvider; late SharedPreferencesAsync prefs; + // bool hideNutrition = false; + + // New: visibility state for dashboard widgets + Map dashboardWidgetVisibility = { + 'routines': true, + 'weight': true, + 'measurements': true, + 'calendar': true, + 'nutrition': true, + }; + + static const String PREFS_DASHBOARD_VISIBILITY = 'dashboardWidgetVisibility'; UserProvider(this.baseProvider, {SharedPreferencesAsync? prefs}) { this.prefs = prefs ?? PreferenceHelper.asyncPref; _loadThemeMode(); + _loadDashboardVisibility(); } static const PROFILE_URL = 'userprofile'; @@ -66,6 +80,34 @@ class UserProvider with ChangeNotifier { notifyListeners(); } + Future _loadDashboardVisibility() async { + final jsonString = await prefs.getString(PREFS_DASHBOARD_VISIBILITY); + + if (jsonString != null) { + try { + final decoded = jsonDecode(jsonString) as Map; + dashboardWidgetVisibility = + decoded.map((k, v) => MapEntry(k, v as bool)); + } catch (_) { + // If parsing fails, keep defaults + } + } + + notifyListeners(); + } + + bool isDashboardWidgetVisible(String key) { + return dashboardWidgetVisibility[key] ?? true; + } + + Future setDashboardWidgetVisible(String key, bool visible) async { + dashboardWidgetVisibility[key] = visible; + await prefs.setString( + PREFS_DASHBOARD_VISIBILITY, + jsonEncode(dashboardWidgetVisibility), + ); + notifyListeners(); + } // Change mode on switch button click void setThemeMode(ThemeMode mode) async { @@ -91,6 +133,7 @@ class UserProvider with ChangeNotifier { } } + /// Save the user's profile to the server Future saveProfile() async { await baseProvider.post( diff --git a/lib/screens/dashboard.dart b/lib/screens/dashboard.dart index 3be5a4b4c..ae41069ef 100644 --- a/lib/screens/dashboard.dart +++ b/lib/screens/dashboard.dart @@ -17,7 +17,7 @@ */ 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/widgets/core/app_bar.dart'; import 'package:wger/widgets/dashboard/calendar.dart'; @@ -25,6 +25,7 @@ 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/weight.dart'; +import 'package:wger/providers/user.dart'; class DashboardScreen extends StatelessWidget { const DashboardScreen({super.key}); @@ -33,50 +34,27 @@ class DashboardScreen extends StatelessWidget { @override Widget build(BuildContext context) { - final width = MediaQuery.sizeOf(context).width; - final isMobile = width < MATERIAL_XS_BREAKPOINT; - - late final int crossAxisCount; - if (width < MATERIAL_XS_BREAKPOINT) { - crossAxisCount = 1; - } else if (width < MATERIAL_MD_BREAKPOINT) { - crossAxisCount = 2; - } else if (width < MATERIAL_LG_BREAKPOINT) { - crossAxisCount = 3; - } else { - crossAxisCount = 4; - } - - final items = [ - const DashboardRoutineWidget(), - const DashboardNutritionWidget(), - const DashboardWeightWidget(), - const DashboardMeasurementWidget(), - const DashboardCalendarWidget(), - ]; + final user = Provider.of(context); return Scaffold( appBar: MainAppBar(AppLocalizations.of(context).labelDashboard), - body: Center( - child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: MATERIAL_LG_BREAKPOINT.toDouble()), - child: isMobile - ? ListView.builder( - padding: const EdgeInsets.all(10), - itemBuilder: (context, index) => items[index], - itemCount: items.length, - ) - : GridView.builder( - padding: const EdgeInsets.all(10), - itemBuilder: (context, index) => SingleChildScrollView(child: items[index]), - itemCount: items.length, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: crossAxisCount, - childAspectRatio: 0.7, - ), - ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(10), + child: Column( + children: [ + if (user.isDashboardWidgetVisible('routines')) + const DashboardRoutineWidget(), + if (user.isDashboardWidgetVisible('weight')) + const DashboardWeightWidget(), + if (user.isDashboardWidgetVisible('measurements')) + const DashboardMeasurementWidget(), + if (user.isDashboardWidgetVisible('calendar')) + const DashboardCalendarWidget(), + if (user.isDashboardWidgetVisible('nutrition')) + const DashboardNutritionWidget(), + ], ), ), ); } -} +} \ No newline at end of file diff --git a/lib/widgets/core/settings.dart b/lib/widgets/core/settings.dart index 3562c545d..336e13b81 100644 --- a/lib/widgets/core/settings.dart +++ b/lib/widgets/core/settings.dart @@ -24,6 +24,9 @@ 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:provider/provider.dart'; +import 'package:wger/providers/user.dart'; + class SettingsPage extends StatelessWidget { static String routeName = '/SettingsPage'; @@ -36,28 +39,61 @@ 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(), + Consumer( + builder: (context, user, _) { + return Column( + children: [ + SwitchListTile( + title: const Text('Show routines on dashboard'), + value: user.isDashboardWidgetVisible('routines'), + onChanged: (v) => + user.setDashboardWidgetVisible('routines', v), + ), + SwitchListTile( + title: const Text('Show weight on dashboard'), + value: user.isDashboardWidgetVisible('weight'), + onChanged: (v) => + user.setDashboardWidgetVisible('weight', v), + ), + SwitchListTile( + title: const Text('Show measurements on dashboard'), + value: user.isDashboardWidgetVisible('measurements'), + onChanged: (v) => + user.setDashboardWidgetVisible('measurements', v), + ), + SwitchListTile( + title: const Text('Show calendar on dashboard'), + value: user.isDashboardWidgetVisible('calendar'), + onChanged: (v) => + user.setDashboardWidgetVisible('calendar', v), + ), + SwitchListTile( + title: const Text('Show nutrition on dashboard'), + value: user.isDashboardWidgetVisible('nutrition'), + onChanged: (v) => + user.setDashboardWidgetVisible('nutrition', v), + ), + ], + ); + }, + ), + ListTile( + title: Text(i18n.selectAvailablePlates), + onTap: () { + Navigator.of(context).pushNamed(ConfigurePlatesScreen.routeName); + }, + trailing: const Icon(Icons.chevron_right), + ), + ], ), ); } diff --git a/test/core/settings_test.dart b/test/core/settings_test.dart index 474603c92..b29346827 100644 --- a/test/core/settings_test.dart +++ b/test/core/settings_test.dart @@ -51,6 +51,11 @@ void main() { when(mockUserProvider.themeMode).thenReturn(ThemeMode.system); when(mockExerciseProvider.exercises).thenReturn(getTestExercises()); when(mockNutritionProvider.ingredients).thenReturn([ingredient1, ingredient2]); + when(mockUserProvider.isDashboardWidgetVisible('routines')).thenReturn(true); + when(mockUserProvider.isDashboardWidgetVisible('weight')).thenReturn(true); + when(mockUserProvider.isDashboardWidgetVisible('measurements')).thenReturn(true); + when(mockUserProvider.isDashboardWidgetVisible('calendar')).thenReturn(true); + when(mockUserProvider.isDashboardWidgetVisible('nutrition')).thenReturn(true); }); Widget createSettingsScreen({locale = 'en'}) { @@ -100,18 +105,24 @@ void main() { group('Theme settings', () { test('Default theme is system', () async { when(mockSharedPreferences.getBool(PREFS_USER_DARK_THEME)).thenAnswer((_) async => null); + when(mockSharedPreferences.getString('dashboardWidgetVisibility')) + .thenAnswer((_) async => null); final userProvider = await UserProvider(MockWgerBaseProvider(), prefs: mockSharedPreferences); expect(userProvider.themeMode, ThemeMode.system); }); test('Loads light theme', () async { when(mockSharedPreferences.getBool(PREFS_USER_DARK_THEME)).thenAnswer((_) async => false); + when(mockSharedPreferences.getString('dashboardWidgetVisibility')) + .thenAnswer((_) async => null); final userProvider = await UserProvider(MockWgerBaseProvider(), prefs: mockSharedPreferences); 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);