Make the app work better on wider screens

This commit is contained in:
Roland Geider
2025-12-01 11:52:11 +01:00
parent c8b17ed9e7
commit 559eb26631
23 changed files with 490 additions and 309 deletions

View File

@@ -0,0 +1,36 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* Copyright (c) 2020, 2025 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 <http://www.gnu.org/licenses/>.
*/
import 'package:flutter/material.dart';
import 'package:wger/helpers/material.dart';
class WidescreenWrapper extends StatelessWidget {
final Widget child;
const WidescreenWrapper({required this.child, super.key});
@override
Widget build(BuildContext context) {
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: MATERIAL_MD_BREAKPOINT),
child: child,
),
);
}
}

23
lib/helpers/material.dart Normal file
View File

@@ -0,0 +1,23 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* Copyright (c) 2020, 2025 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 <http://www.gnu.org/licenses/>.
*/
// From https://m3.material.io/foundations/layout/applying-layout/window-size-classes
const MATERIAL_XS_BREAKPOINT = 600.0;
const MATERIAL_MD_BREAKPOINT = 840.0;
const MATERIAL_LG_BREAKPOINT = 1200.0;
const MATERIAL_XL_BREAKPOINT = 1600.0;

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:wger/core/wide_screen_wrapper.dart';
import 'package:wger/exceptions/http_exception.dart';
import 'package:wger/helpers/consts.dart';
import 'package:wger/helpers/errors.dart';
@@ -151,7 +152,8 @@ class _AddExerciseStepperState extends State<AddExerciseStepper> {
Widget build(BuildContext context) {
return Scaffold(
appBar: EmptyAppBar(AppLocalizations.of(context).contributeExercise),
body: Stepper(
body: WidescreenWrapper(
child: Stepper(
controlsBuilder: _controlsBuilder,
steps: [
Step(
@@ -201,6 +203,7 @@ class _AddExerciseStepperState extends State<AddExerciseStepper> {
},
*/
),
),
);
}
}

View File

@@ -1,4 +1,5 @@
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/routines/plate_calculator.dart';
@@ -13,7 +14,7 @@ class ConfigurePlatesScreen extends StatelessWidget {
return Scaffold(
appBar: AppBar(title: Text(i18n.selectAvailablePlates)),
body: const ConfigureAvailablePlates(),
body: const WidescreenWrapper(child: ConfigureAvailablePlates()),
);
}
}

View File

@@ -17,6 +17,7 @@
*/
import 'package:flutter/material.dart';
import 'package:wger/helpers/material.dart';
import 'package:wger/l10n/generated/app_localizations.dart';
import 'package:wger/widgets/core/app_bar.dart';
import 'package:wger/widgets/dashboard/calendar.dart';
@@ -32,18 +33,48 @@ 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(),
];
return Scaffold(
appBar: MainAppBar(AppLocalizations.of(context).labelDashboard),
body: const SingleChildScrollView(
padding: EdgeInsets.all(10),
child: Column(
children: [
DashboardRoutineWidget(),
DashboardNutritionWidget(),
DashboardWeightWidget(),
DashboardMeasurementWidget(),
DashboardCalendarWidget(),
],
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,
),
),
),
),
);

View File

@@ -17,6 +17,7 @@
*/
import 'package:flutter/material.dart';
import 'package:wger/core/wide_screen_wrapper.dart';
import 'package:wger/models/exercises/exercise.dart';
import 'package:wger/widgets/exercises/exercises.dart';
@@ -33,10 +34,12 @@ class ExerciseDetailScreen extends StatelessWidget {
appBar: AppBar(
title: Text(exercise.getTranslation(Localizations.localeOf(context).languageCode).name),
),
body: Padding(
body: WidescreenWrapper(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: ExerciseDetail(exercise),
),
),
);
}
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:wger/core/wide_screen_wrapper.dart';
import 'package:wger/l10n/generated/app_localizations.dart';
import 'package:wger/models/exercises/exercise.dart';
import 'package:wger/providers/exercises.dart';
@@ -23,7 +24,8 @@ class _ExercisesScreenState extends State<ExercisesScreen> {
return Scaffold(
appBar: EmptyAppBar(AppLocalizations.of(context).exercises),
body: Column(
body: WidescreenWrapper(
child: Column(
children: [
const FilterRow(),
Expanded(
@@ -39,6 +41,7 @@ class _ExercisesScreenState extends State<ExercisesScreen> {
),
],
),
),
);
}
}

View File

@@ -18,6 +18,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:wger/core/wide_screen_wrapper.dart';
/// Arguments passed to the form screen
class FormScreenArguments {
@@ -54,7 +55,8 @@ class FormScreen extends StatelessWidget {
return Scaffold(
appBar: AppBar(title: Text(args.title)),
body: args.hasListView
body: WidescreenWrapper(
child: args.hasListView
? Scrollable(
viewportBuilder: (BuildContext context, ViewportOffset position) => Padding(
padding: args.padding,
@@ -66,6 +68,7 @@ class FormScreen extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.end,
children: [Padding(padding: args.padding, child: args.widget)],
),
),
);
}
}

View File

@@ -18,6 +18,7 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:wger/core/wide_screen_wrapper.dart';
import 'package:wger/helpers/platform.dart';
import 'package:wger/l10n/generated/app_localizations.dart';
import 'package:wger/providers/gallery.dart';
@@ -52,9 +53,11 @@ class GalleryScreen extends StatelessWidget {
);
},
),
body: Consumer<GalleryProvider>(
body: WidescreenWrapper(
child: Consumer<GalleryProvider>(
builder: (context, workoutProvider, child) => const Gallery(),
),
),
);
}
}

View File

@@ -18,6 +18,7 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:wger/core/wide_screen_wrapper.dart';
import 'package:wger/providers/routines.dart';
import 'package:wger/widgets/routines/gym_mode/gym_mode.dart';
@@ -51,10 +52,12 @@ class GymModeScreen extends StatelessWidget {
// backgroundColor: Theme.of(context).cardColor,
// primary: false,
body: SafeArea(
child: WidescreenWrapper(
child: Consumer<RoutinesProvider>(
builder: (context, value, child) => GymMode(dayDataGym, dayDataDisplay, args.iteration),
),
),
),
);
}
}

View File

@@ -21,6 +21,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:logging/logging.dart';
import 'package:provider/provider.dart';
import 'package:rive/rive.dart';
import 'package:wger/helpers/material.dart';
import 'package:wger/l10n/generated/app_localizations.dart';
import 'package:wger/providers/auth.dart';
import 'package:wger/providers/body_weight.dart';
@@ -51,6 +52,7 @@ class _HomeTabsScreenState extends State<HomeTabsScreen> with SingleTickerProvid
late Future<void> _initialData;
bool _errorHandled = false;
int _selectedIndex = 0;
bool _isWideScreen = false;
@override
void initState() {
@@ -59,6 +61,14 @@ class _HomeTabsScreenState extends State<HomeTabsScreen> with SingleTickerProvid
_initialData = _loadEntries();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final double width = MediaQuery.of(context).size.width;
_isWideScreen = width > MATERIAL_XS_BREAKPOINT;
}
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
@@ -141,6 +151,57 @@ class _HomeTabsScreenState extends State<HomeTabsScreen> with SingleTickerProvid
@override
Widget build(BuildContext context) {
final destinations = [
NavigationDestination(
icon: const Icon(Icons.home),
label: AppLocalizations.of(context).labelDashboard,
),
NavigationDestination(
icon: const Icon(Icons.fitness_center),
label: AppLocalizations.of(context).labelBottomNavWorkout,
),
NavigationDestination(
icon: const Icon(Icons.restaurant),
label: AppLocalizations.of(context).labelBottomNavNutrition,
),
NavigationDestination(
icon: const FaIcon(FontAwesomeIcons.weightScale, size: 20),
label: AppLocalizations.of(context).weight,
),
NavigationDestination(
icon: const Icon(Icons.photo_library),
label: AppLocalizations.of(context).gallery,
),
];
/// Navigation bar for narrow screens
Widget getNavigationBar() {
return NavigationBar(
destinations: destinations,
onDestinationSelected: _onItemTapped,
selectedIndex: _selectedIndex,
labelBehavior: NavigationDestinationLabelBehavior.alwaysHide,
);
}
/// Navigation rail for wide screens
Widget getNavigationRail() {
return NavigationRail(
selectedIndex: _selectedIndex,
onDestinationSelected: _onItemTapped,
labelType: NavigationRailLabelType.all,
scrollable: true,
destinations: destinations
.map(
(d) => NavigationRailDestination(
icon: d.icon,
label: Text(d.label),
),
)
.toList(),
);
}
return FutureBuilder<void>(
future: _initialData,
builder: (context, snapshot) {
@@ -173,34 +234,13 @@ class _HomeTabsScreenState extends State<HomeTabsScreen> with SingleTickerProvid
}
return Scaffold(
body: _screenList.elementAt(_selectedIndex),
bottomNavigationBar: NavigationBar(
destinations: [
NavigationDestination(
icon: const Icon(Icons.home),
label: AppLocalizations.of(context).labelDashboard,
),
NavigationDestination(
icon: const Icon(Icons.fitness_center),
label: AppLocalizations.of(context).labelBottomNavWorkout,
),
NavigationDestination(
icon: const Icon(Icons.restaurant),
label: AppLocalizations.of(context).labelBottomNavNutrition,
),
NavigationDestination(
icon: const FaIcon(FontAwesomeIcons.weightScale, size: 20),
label: AppLocalizations.of(context).weight,
),
NavigationDestination(
icon: const Icon(Icons.photo_library),
label: AppLocalizations.of(context).gallery,
),
body: Row(
children: [
if (_isWideScreen) getNavigationRail(),
Expanded(child: _screenList.elementAt(_selectedIndex)),
],
onDestinationSelected: _onItemTapped,
selectedIndex: _selectedIndex,
labelBehavior: NavigationDestinationLabelBehavior.alwaysHide,
),
bottomNavigationBar: _isWideScreen ? null : getNavigationBar(),
);
},
);

View File

@@ -18,6 +18,7 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:wger/core/wide_screen_wrapper.dart';
import 'package:wger/l10n/generated/app_localizations.dart';
import 'package:wger/providers/measurement.dart';
import 'package:wger/screens/form_screen.dart';
@@ -46,9 +47,11 @@ class MeasurementCategoriesScreen extends StatelessWidget {
);
},
),
body: Consumer<MeasurementProvider>(
body: WidescreenWrapper(
child: Consumer<MeasurementProvider>(
builder: (context, provider, child) => const CategoriesList(),
),
),
);
}
}

View File

@@ -18,6 +18,7 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:wger/core/wide_screen_wrapper.dart';
import 'package:wger/l10n/generated/app_localizations.dart';
import 'package:wger/providers/measurement.dart';
import 'package:wger/screens/form_screen.dart';
@@ -134,11 +135,13 @@ class MeasurementEntriesScreen extends StatelessWidget {
);
},
),
body: SingleChildScrollView(
body: WidescreenWrapper(
child: SingleChildScrollView(
child: Consumer<MeasurementProvider>(
builder: (context, provider, child) => EntriesList(category),
),
),
),
);
}
}

View File

@@ -19,6 +19,7 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:wger/core/wide_screen_wrapper.dart';
import 'package:wger/models/nutrition/nutritional_plan.dart';
import 'package:wger/providers/nutrition.dart';
import 'package:wger/widgets/nutrition/nutritional_diary_detail.dart';
@@ -46,7 +47,8 @@ class NutritionalDiaryScreen extends StatelessWidget {
appBar: AppBar(
title: Text(DateFormat.yMd(Localizations.localeOf(context).languageCode).format(args.date)),
),
body: Consumer<NutritionPlansProvider>(
body: WidescreenWrapper(
child: Consumer<NutritionPlansProvider>(
builder: (context, nutritionProvider, child) => SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(8.0),
@@ -54,6 +56,7 @@ class NutritionalDiaryScreen extends StatelessWidget {
),
),
),
),
);
}
}

View File

@@ -18,6 +18,7 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:wger/core/wide_screen_wrapper.dart';
import 'package:wger/l10n/generated/app_localizations.dart';
import 'package:wger/providers/nutrition.dart';
import 'package:wger/screens/form_screen.dart';
@@ -48,9 +49,11 @@ class NutritionalPlansScreen extends StatelessWidget {
);
},
),
body: Consumer<NutritionPlansProvider>(
body: WidescreenWrapper(
child: Consumer<NutritionPlansProvider>(
builder: (context, nutritionProvider, child) => NutritionalPlansList(nutritionProvider),
),
),
);
}
}

View File

@@ -18,6 +18,7 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:wger/core/wide_screen_wrapper.dart';
import 'package:wger/providers/routines.dart';
import 'package:wger/widgets/core/app_bar.dart';
import 'package:wger/widgets/routines/routine_edit.dart';
@@ -34,7 +35,7 @@ class RoutineEditScreen extends StatelessWidget {
return Scaffold(
appBar: EmptyAppBar(routine.name),
body: RoutineEdit(routine),
body: WidescreenWrapper(child: RoutineEdit(routine)),
);
}
}

View File

@@ -18,6 +18,7 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:wger/core/wide_screen_wrapper.dart';
import 'package:wger/l10n/generated/app_localizations.dart';
import 'package:wger/models/workouts/routine.dart';
import 'package:wger/providers/routines.dart';
@@ -49,9 +50,11 @@ class RoutineListScreen extends StatelessWidget {
},
child: const Icon(Icons.add, color: Colors.white),
),
body: Consumer<RoutinesProvider>(
body: WidescreenWrapper(
child: Consumer<RoutinesProvider>(
builder: (context, workoutProvider, child) => RoutinesList(workoutProvider),
),
),
);
}
}

View File

@@ -18,6 +18,7 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:wger/core/wide_screen_wrapper.dart';
import 'package:wger/providers/routines.dart';
import 'package:wger/widgets/core/app_bar.dart';
import 'package:wger/widgets/routines/workout_logs.dart';
@@ -34,7 +35,7 @@ class WorkoutLogsScreen extends StatelessWidget {
return Scaffold(
appBar: EmptyAppBar(routine.name),
body: WorkoutLogs(routine),
body: WidescreenWrapper(child: WorkoutLogs(routine)),
);
}
}

View File

@@ -18,6 +18,7 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:wger/core/wide_screen_wrapper.dart';
import 'package:wger/providers/routines.dart';
import 'package:wger/widgets/routines/app_bar.dart';
import 'package:wger/widgets/routines/routine_detail.dart';
@@ -36,11 +37,13 @@ class RoutineScreen extends StatelessWidget {
return Scaffold(
appBar: RoutineDetailAppBar(routine),
body: SingleChildScrollView(
body: WidescreenWrapper(
child: SingleChildScrollView(
child: Consumer<RoutinesProvider>(
builder: (context, value, child) => RoutineDetail(routine),
),
),
),
);
}
}

View File

@@ -18,6 +18,7 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:wger/core/wide_screen_wrapper.dart';
import 'package:wger/l10n/generated/app_localizations.dart';
import 'package:wger/providers/body_weight.dart';
import 'package:wger/screens/form_screen.dart';
@@ -47,11 +48,13 @@ class WeightScreen extends StatelessWidget {
);
},
),
body: SingleChildScrollView(
body: WidescreenWrapper(
child: SingleChildScrollView(
child: Consumer<BodyWeightProvider>(
builder: (context, provider, child) => WeightOverview(provider),
),
),
),
);
}
}

View File

@@ -19,6 +19,7 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.dart';
import 'package:wger/core/wide_screen_wrapper.dart';
import 'package:wger/helpers/consts.dart';
import 'package:wger/helpers/misc.dart';
import 'package:wger/l10n/generated/app_localizations.dart';
@@ -53,7 +54,8 @@ class AboutPage extends StatelessWidget {
return Scaffold(
appBar: AppBar(title: Text(i18n.aboutPageTitle)),
body: SingleChildScrollView(
body: WidescreenWrapper(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -213,6 +215,7 @@ class AboutPage extends StatelessWidget {
],
),
),
),
);
}
}

View File

@@ -18,6 +18,7 @@
//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';
@@ -35,10 +36,14 @@ class SettingsPage extends StatelessWidget {
return Scaffold(
appBar: AppBar(title: Text(i18n.settingsTitle)),
body: ListView(
body: WidescreenWrapper(
child: ListView(
children: [
ListTile(
title: Text(i18n.settingsCacheTitle, style: Theme.of(context).textTheme.headlineSmall),
title: Text(
i18n.settingsCacheTitle,
style: Theme.of(context).textTheme.headlineSmall,
),
),
const SettingsExerciseCache(),
const SettingsIngredientCache(),
@@ -53,6 +58,7 @@ class SettingsPage extends StatelessWidget {
),
],
),
),
);
}
}

View File

@@ -84,17 +84,17 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin
SPEC CHECKSUMS:
file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31
file_selector_macos: 9e9e068e90ebee155097d00e89ae91edb2374db7
flutter_zxing: 91e9d17c79c60860450e8879cced0ec54f6a2601
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
rive_common: ea79040f86acf053a2d5a75a2506175ee39796a5
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b
sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1
url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673
video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b
url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd
video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a
PODFILE CHECKSUM: 0d3963a09fc94f580682bd88480486da345dc3f0