Also allow changing the order of the dashboard widgets

This makes the behaviour more consistent and allows users to remove widgets
they won't be using, like the trophies
This commit is contained in:
Roland Geider
2026-01-17 13:23:55 +01:00
parent 5574f74a2f
commit b81331609a
12 changed files with 735 additions and 134 deletions

View File

@@ -19,6 +19,7 @@
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';
@@ -28,44 +29,48 @@ import 'package:wger/providers/base_provider.dart';
enum DashboardWidget {
routines('routines'),
nutrition('nutrition'),
weight('weight'),
measurements('measurements'),
calendar('calendar'),
nutrition('nutrition');
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;
if (e.value == s) {
return e;
}
}
return null;
}
}
class DashboardItem {
final DashboardWidget widget;
bool isVisible;
DashboardItem(this.widget, {this.isVisible = true});
Map<String, dynamic> toJson() => {
'widget': widget.value,
'visible': isVisible,
};
}
class UserProvider with ChangeNotifier {
ThemeMode themeMode = ThemeMode.system;
final WgerBaseProvider baseProvider;
late SharedPreferencesAsync prefs;
// New: visibility state for dashboard widgets
Map<DashboardWidget, bool> dashboardWidgetVisibility = {
DashboardWidget.routines: true,
DashboardWidget.weight: true,
DashboardWidget.measurements: true,
DashboardWidget.calendar: true,
DashboardWidget.nutrition: true,
};
static const String PREFS_DASHBOARD_VISIBILITY = 'dashboardWidgetVisibility';
UserProvider(this.baseProvider, {SharedPreferencesAsync? prefs}) {
this.prefs = prefs ?? PreferenceHelper.asyncPref;
_loadThemeMode();
_loadDashboardVisibility();
_loadDashboardConfig();
}
static const String PREFS_DASHBOARD_CONFIG = 'dashboardConfig';
static const PROFILE_URL = 'userprofile';
static const VERIFY_EMAIL = 'verify-email';
@@ -76,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<void> _loadThemeMode() async {
final prefsDarkMode = await prefs.getBool(PREFS_USER_DARK_THEME);
@@ -98,47 +94,76 @@ class UserProvider with ChangeNotifier {
notifyListeners();
}
Future<void> _loadDashboardVisibility() async {
final jsonString = await prefs.getString(PREFS_DASHBOARD_VISIBILITY);
// Dashboard configuration
List<DashboardItem> _dashboardItems = DashboardWidget.values
.map((w) => DashboardItem(w))
.toList();
List<DashboardWidget> get dashboardOrder => _dashboardItems.map((e) => e.widget).toList();
Future<void> _loadDashboardConfig() async {
final jsonString = await prefs.getString(PREFS_DASHBOARD_CONFIG);
if (jsonString == null) {
notifyListeners();
return;
}
try {
final decoded = jsonDecode(jsonString) as Map<String, dynamic>;
final Map<DashboardWidget, bool> loaded = {};
final List<dynamic> decoded = jsonDecode(jsonString);
final List<DashboardItem> loaded = [];
for (final entry in decoded.entries) {
final widget = DashboardWidget.fromString(entry.key);
for (final item in decoded) {
final widget = DashboardWidget.fromString(item['widget']);
if (widget != null) {
loaded[widget] = entry.value as bool;
loaded.add(
DashboardItem(widget, isVisible: item['visible'] as bool),
);
}
}
if (loaded.isNotEmpty) {
dashboardWidgetVisibility = loaded;
// 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<void> _saveDashboardConfig() async {
final serializable = _dashboardItems.map((e) => e.toJson()).toList();
await prefs.setString(PREFS_DASHBOARD_CONFIG, jsonEncode(serializable));
}
bool isDashboardWidgetVisible(DashboardWidget key) {
return dashboardWidgetVisibility[key] ?? true;
final widget = _dashboardItems.firstWhereOrNull((e) => e.widget == key);
return widget == null || widget.isVisible;
}
Future<void> setDashboardWidgetVisible(DashboardWidget key, bool visible) async {
dashboardWidgetVisibility[key] = visible;
final Map<String, bool> serializable = {
for (final e in dashboardWidgetVisibility.entries) e.key.value: e.value,
};
final item = _dashboardItems.firstWhereOrNull((e) => e.widget == key);
if (item == null) {
return;
}
await prefs.setString(
PREFS_DASHBOARD_VISIBILITY,
jsonEncode(serializable),
);
item.isVisible = visible;
await _saveDashboardConfig();
notifyListeners();
}
Future<void> setDashboardOrder(int oldIndex, int newIndex) async {
if (oldIndex < newIndex) {
newIndex -= 1;
}
final item = _dashboardItems.removeAt(oldIndex);
_dashboardItems.insert(newIndex, item);
await _saveDashboardConfig();
notifyListeners();
}

View File

@@ -1,6 +1,6 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* 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
@@ -32,6 +32,21 @@ 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();
}
}
@override
Widget build(BuildContext context) {
final user = Provider.of<UserProvider>(context);
@@ -41,18 +56,10 @@ class DashboardScreen extends StatelessWidget {
body: SingleChildScrollView(
padding: const EdgeInsets.all(10),
child: Column(
children: [
if (user.isDashboardWidgetVisible(DashboardWidget.routines))
const DashboardRoutineWidget(),
if (user.isDashboardWidgetVisible(DashboardWidget.weight))
const DashboardWeightWidget(),
if (user.isDashboardWidgetVisible(DashboardWidget.measurements))
const DashboardMeasurementWidget(),
if (user.isDashboardWidgetVisible(DashboardWidget.calendar))
const DashboardCalendarWidget(),
if (user.isDashboardWidgetVisible(DashboardWidget.nutrition))
const DashboardNutritionWidget(),
],
children: user.dashboardOrder
.where((w) => user.isDashboardWidgetVisible(w))
.map(_getDashboardWidget)
.toList(),
),
),
);

View File

@@ -18,7 +18,6 @@
import 'package:flutter/material.dart';
import 'package:wger/l10n/generated/app_localizations.dart';
import 'package:wger/screens/settings_dashboard_widgets_screen.dart';
import 'package:wger/screens/settings_plates_screen.dart';
import './settings/exercise_cache.dart';
@@ -52,13 +51,6 @@ class SettingsPage extends StatelessWidget {
},
trailing: const Icon(Icons.chevron_right),
),
ListTile(
title: Text(i18n.dashboardWidgets),
onTap: () {
Navigator.of(context).pushNamed(ConfigureDashboardWidgetsScreen.routeName);
},
trailing: const Icon(Icons.chevron_right),
),
],
),
);

View File

@@ -28,36 +28,49 @@ class SettingsDashboardVisibility extends StatelessWidget {
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<UserProvider>(
builder: (context, user, _) {
return Column(
children: [
SwitchListTile(
title: Text(i18n.routines),
value: user.isDashboardWidgetVisible(DashboardWidget.routines),
onChanged: (v) => user.setDashboardWidgetVisible(DashboardWidget.routines, v),
),
SwitchListTile(
title: Text(i18n.weight),
value: user.isDashboardWidgetVisible(DashboardWidget.weight),
onChanged: (v) => user.setDashboardWidgetVisible(DashboardWidget.weight, v),
),
SwitchListTile(
title: Text(i18n.measurements),
value: user.isDashboardWidgetVisible(DashboardWidget.measurements),
onChanged: (v) => user.setDashboardWidgetVisible(DashboardWidget.measurements, v),
),
SwitchListTile(
title: Text(i18n.calendar),
value: user.isDashboardWidgetVisible(DashboardWidget.calendar),
onChanged: (v) => user.setDashboardWidgetVisible(DashboardWidget.calendar, v),
),
SwitchListTile(
title: Text(i18n.nutritionalPlans),
value: user.isDashboardWidgetVisible(DashboardWidget.nutrition),
onChanged: (v) => user.setDashboardWidgetVisible(DashboardWidget.nutrition, v),
),
],
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(),
);
},
);