Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions ios/Flutter/ephemeral/flutter_lldb_helper.py
Original file line number Diff line number Diff line change
@@ -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 --")
5 changes: 5 additions & 0 deletions ios/Flutter/ephemeral/flutter_lldbinit
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#
# Generated file, do not edit.
#

command script import --relative-to-command-file flutter_lldb_helper.py
43 changes: 43 additions & 0 deletions lib/providers/user.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
*/

import 'dart:async';
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
Expand All @@ -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<String, bool> 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';
Expand Down Expand Up @@ -66,6 +80,34 @@ class UserProvider with ChangeNotifier {

notifyListeners();
}
Future<void> _loadDashboardVisibility() async {
final jsonString = await prefs.getString(PREFS_DASHBOARD_VISIBILITY);

if (jsonString != null) {
try {
final decoded = jsonDecode(jsonString) as Map<String, dynamic>;
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<void> 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 {
Expand All @@ -91,6 +133,7 @@ class UserProvider with ChangeNotifier {
}
}


/// Save the user's profile to the server
Future<void> saveProfile() async {
await baseProvider.post(
Expand Down
60 changes: 19 additions & 41 deletions lib/screens/dashboard.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@
*/

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';
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});
Expand All @@ -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<UserProvider>(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(),
],
),
),
);
}
}
}
80 changes: 58 additions & 22 deletions lib/widgets/core/settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<UserProvider>(
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),
),
],
),
);
}
Expand Down
11 changes: 11 additions & 0 deletions test/core/settings_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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'}) {
Expand Down Expand Up @@ -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);
Expand Down