Merge branch 'refs/heads/master' into nutrition-plan-stats

This commit is contained in:
Roland Geider
2025-09-12 14:05:38 +02:00
51 changed files with 1388 additions and 409 deletions

View File

@@ -9,7 +9,7 @@ runs:
uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version: 3.32.8
flutter-version: 3.35.2
cache: true
- name: Install Flutter dependencies

View File

@@ -23,8 +23,8 @@ if (keystorePropertiesFile.exists()) {
android {
namespace = "de.wger.flutter"
compileSdkVersion 35
ndkVersion "27.0.12077973"
compileSdkVersion 36
ndkVersion "28.2.13676358"
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
@@ -39,9 +39,7 @@ android {
defaultConfig {
// Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "de.wger.flutter"
// NOTE: manually setting the minSdk to 23 instead of "flutter.minSdkVersion"
// because flutter_zxing requires a higher minSdkVersion.
minSdk = 23
minSdkVersion = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip

View File

@@ -18,8 +18,8 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.3.2" apply false
id "org.jetbrains.kotlin.android" version "2.0.20" apply false
id "com.android.application" version "8.6.0" apply false
id "org.jetbrains.kotlin.android" version "2.1.20" apply false
}
include ":app"

View File

@@ -24,8 +24,7 @@ import '../test_data/routines.dart';
Widget createDashboardScreen({locale = 'en'}) {
final mockWorkoutProvider = MockRoutinesProvider();
when(mockWorkoutProvider.activeRoutine)
.thenReturn(getTestRoutine(exercises: getScreenshotExercises()));
when(mockWorkoutProvider.items).thenReturn([getTestRoutine(exercises: getScreenshotExercises())]);
when(mockWorkoutProvider.fetchSessionData()).thenAnswer((a) => Future.value([
WorkoutSession(

View File

@@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
<string>13.0</string>
</dict>
</plist>

View File

@@ -105,7 +105,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
camera_avfoundation: be3be85408cd4126f250386828e9b1dfa40ab436
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619
flutter_zxing: e8bcc43bd3056c70c271b732ed94e7a16fd62f93
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a

View File

@@ -344,7 +344,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -432,7 +432,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -481,7 +481,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;

View File

@@ -1,5 +1,3 @@
synthetic-package: false # see https://docs.flutter.dev/release/breaking-changes/flutter-generate-i10n-source
arb-dir: lib/l10n
output-dir: lib/l10n/generated
template-arb-file: app_en.arb

View File

@@ -135,3 +135,5 @@ enum WeightUnitEnum { kg, lb }
/// TextInputType for decimal numbers
const textInputTypeDecimal = TextInputType.numberWithOptions(decimal: true);
const String API_MAX_PAGE_SIZE = '999';

View File

@@ -49,7 +49,7 @@ void showHttpExceptionErrorDialog(WgerHttpException exception, {BuildContext? co
return;
}
final errorList = formatErrors(extractErrors(exception.errors));
final errorList = formatApiErrors(extractErrors(exception.errors));
showDialog(
context: dialogContext,
@@ -87,44 +87,51 @@ void showGeneralErrorDialog(dynamic error, StackTrace? stackTrace, {BuildContext
return;
}
final i18n = AppLocalizations.of(dialogContext);
// If possible, determine the error title and message based on the error type.
bool isNetworkError = false;
String errorTitle = 'An error occurred';
String errorMessage = error.toString();
// (Note that issue titles and error messages are not localized)
bool allowReportIssue = true;
String issueTitle = 'An error occurred';
String issueErrorMessage = error.toString();
String errorTitle = i18n.anErrorOccurred;
String errorDescription = i18n.errorInfoDescription;
var icon = Icons.error;
if (error is TimeoutException) {
errorTitle = 'Network Timeout';
errorMessage =
'The connection to the server timed out. Please check your internet connection and try again.';
issueTitle = 'Network Timeout';
issueErrorMessage = 'The connection to the server timed out. Please check your '
'internet connection and try again.';
} else if (error is FlutterErrorDetails) {
errorTitle = 'Application Error';
errorMessage = error.exceptionAsString();
issueTitle = 'Application Error';
issueErrorMessage = error.exceptionAsString();
} else if (error is MissingRequiredKeysException) {
errorTitle = 'Missing Required Key';
issueTitle = 'Missing Required Key';
} else if (error is SocketException) {
isNetworkError = true;
allowReportIssue = false;
icon = Icons.signal_wifi_connected_no_internet_4_outlined;
errorTitle = i18n.errorCouldNotConnectToServer;
errorDescription = i18n.errorCouldNotConnectToServerDetails;
}
final String fullStackTrace = stackTrace?.toString() ?? 'No stack trace available.';
final i18n = AppLocalizations.of(dialogContext);
showDialog(
context: dialogContext,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
title: Row(
spacing: 8,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
isNetworkError ? Icons.signal_wifi_connected_no_internet_4_outlined : Icons.error,
icon,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(width: 8),
Expanded(
child: Text(
isNetworkError ? i18n.errorCouldNotConnectToServer : i18n.anErrorOccurred,
errorTitle,
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
),
@@ -133,11 +140,7 @@ void showGeneralErrorDialog(dynamic error, StackTrace? stackTrace, {BuildContext
content: SingleChildScrollView(
child: ListBody(
children: [
Text(
isNetworkError
? i18n.errorCouldNotConnectToServerDetails
: i18n.errorInfoDescription,
),
Text(errorDescription),
const SizedBox(height: 8),
Text(i18n.errorInfoDescription2),
const SizedBox(height: 10),
@@ -146,7 +149,7 @@ void showGeneralErrorDialog(dynamic error, StackTrace? stackTrace, {BuildContext
title: Text(i18n.errorViewDetails),
children: [
Text(
errorMessage,
issueErrorMessage,
style: const TextStyle(fontWeight: FontWeight.bold),
),
Container(
@@ -169,14 +172,17 @@ void showGeneralErrorDialog(dynamic error, StackTrace? stackTrace, {BuildContext
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
onPressed: () {
final String clipboardText =
'Error Title: $errorTitle\nError Message: $errorMessage\n\nStack Trace:\n$fullStackTrace';
final String clipboardText = 'Error Title: $issueTitle\n'
'Error Message: $issueErrorMessage\n\n'
'Stack Trace:\n$fullStackTrace';
Clipboard.setData(ClipboardData(text: clipboardText)).then((_) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Error details copied to clipboard!')),
);
}).catchError((copyError) {
if (kDebugMode) logger.fine('Error copying to clipboard: $copyError');
if (kDebugMode) {
logger.fine('Error copying to clipboard: $copyError');
}
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Could not copy details.')),
);
@@ -189,7 +195,7 @@ void showGeneralErrorDialog(dynamic error, StackTrace? stackTrace, {BuildContext
),
),
actions: [
if (!isNetworkError)
if (allowReportIssue)
TextButton(
child: const Text('Report issue'),
onPressed: () async {
@@ -197,20 +203,22 @@ void showGeneralErrorDialog(dynamic error, StackTrace? stackTrace, {BuildContext
'## Description\n\n'
'[Please describe what you were doing when the error occurred.]\n\n'
'## Error details\n\n'
'Error title: $errorTitle\n'
'Error message: $errorMessage\n'
'Error title: $issueTitle\n'
'Error message: $issueErrorMessage\n'
'Stack trace:\n'
'```\n$stackTrace\n```',
);
final githubIssueUrl = '$GITHUB_ISSUES_BUG_URL'
'&title=$errorTitle'
'&title=$issueTitle'
'&description=$description';
final Uri reportUri = Uri.parse(githubIssueUrl);
try {
await launchUrl(reportUri, mode: LaunchMode.externalApplication);
} catch (e) {
if (kDebugMode) logger.warning('Error launching URL: $e');
if (kDebugMode) {
logger.warning('Error launching URL: $e');
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error opening issue tracker: $e')),
);
@@ -309,7 +317,7 @@ List<ApiError> extractErrors(Map<String, dynamic> errors) {
}
/// Processes the error messages from the server and returns a list of widgets
List<Widget> formatErrors(List<ApiError> errors, {Color? color}) {
List<Widget> formatApiErrors(List<ApiError> errors, {Color? color}) {
final textColor = color ?? Colors.black;
final List<Widget> errorList = [];
@@ -328,6 +336,26 @@ List<Widget> formatErrors(List<ApiError> errors, {Color? color}) {
return errorList;
}
/// Processes the error messages from the server and returns a list of widgets
List<Widget> formatTextErrors(List<String> errors, {String? title, Color? color}) {
final textColor = color ?? Colors.black;
final List<Widget> errorList = [];
if (title != null) {
errorList.add(
Text(title, style: TextStyle(fontWeight: FontWeight.bold, color: textColor)),
);
}
for (final message in errors) {
errorList.add(Text(message, style: TextStyle(color: textColor)));
}
errorList.add(const SizedBox(height: 8));
return errorList;
}
class FormHttpErrorsWidget extends StatelessWidget {
final WgerHttpException exception;
@@ -335,14 +363,45 @@ class FormHttpErrorsWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
Icon(Icons.error_outline, color: Theme.of(context).colorScheme.error),
...formatErrors(
extractErrors(exception.errors),
color: Theme.of(context).colorScheme.error,
return Container(
constraints: const BoxConstraints(maxHeight: 250),
child: SingleChildScrollView(
child: Column(
children: [
Icon(Icons.error_outline, color: Theme.of(context).colorScheme.error),
...formatApiErrors(
extractErrors(exception.errors),
color: Theme.of(context).colorScheme.error,
),
],
),
],
),
);
}
}
class GeneralErrorsWidget extends StatelessWidget {
final String? title;
final List<String> widgets;
const GeneralErrorsWidget(this.widgets, {this.title, super.key});
@override
Widget build(BuildContext context) {
return Container(
constraints: const BoxConstraints(maxHeight: 250),
child: SingleChildScrollView(
child: Column(
children: [
Icon(Icons.error_outline, color: Theme.of(context).colorScheme.error),
...formatTextErrors(
widgets,
title: title,
color: Theme.of(context).colorScheme.error,
),
],
),
),
);
}
}

View File

@@ -20,6 +20,7 @@ import 'dart:developer';
import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:logging/logging.dart';
import 'package:wger/helpers/consts.dart';
import 'package:wger/models/exercises/category.dart';
import 'package:wger/models/exercises/equipment.dart';
@@ -34,6 +35,8 @@ part 'exercise.g.dart';
@JsonSerializable(explicitToJson: true)
class Exercise extends Equatable {
final _logger = Logger('ExerciseModel');
@JsonKey(required: true)
late final int? id;
@@ -198,7 +201,13 @@ class Exercise extends Equatable {
(e) => e.languageObj.shortName == languageCode,
orElse: () => translations.firstWhere(
(e) => e.languageObj.shortName == LANGUAGE_SHORT_ENGLISH,
orElse: () => translations.first,
orElse: () {
_logger.info(
'Could not find fallback english translation for exercise-ID ${id}, returning '
'first language (${translations.first.languageObj.shortName}) instead.',
);
return translations.first;
},
),
);
}

View File

@@ -32,6 +32,8 @@ class Video {
@JsonKey(name: 'video', required: true)
final String url;
Uri get uri => Uri.parse(url);
@JsonKey(name: 'exercise', required: true)
final int exerciseId;

View File

@@ -19,6 +19,7 @@
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:wger/exceptions/http_exception.dart';
import 'package:wger/helpers/consts.dart';
import 'package:wger/models/body_weight/weight_entry.dart';
import 'package:wger/providers/base_provider.dart';
@@ -69,7 +70,7 @@ class BodyWeightProvider with ChangeNotifier {
// Process the response
final data = await baseProvider.fetchPaginated(baseProvider.makeUrl(
BODY_WEIGHT_URL,
query: {'ordering': '-date', 'limit': '100'},
query: {'ordering': '-date', 'limit': API_MAX_PAGE_SIZE},
));
_entries = [];
for (final entry in data) {

View File

@@ -47,6 +47,7 @@ class ExercisesProvider with ChangeNotifier {
static const EXERCISE_CACHE_DAYS = 7;
static const CACHE_VERSION = 4;
static const exerciseUrlPath = 'exercise';
static const exerciseInfoUrlPath = 'exerciseinfo';
static const exerciseSearchPath = 'exercise/search';
@@ -274,6 +275,18 @@ class ExercisesProvider with ChangeNotifier {
}
}
Future<void> fetchAndSetAllExercises() async {
_logger.info('Loading all exercises from API');
final exerciseData = await baseProvider.fetchPaginated(
baseProvider.makeUrl(exerciseUrlPath, query: {'limit': API_MAX_PAGE_SIZE}),
);
final exerciseIds = exerciseData.map<int>((e) => e['id'] as int).toSet();
for (final exerciseId in exerciseIds) {
await handleUpdateExerciseFromApi(database, exerciseId);
}
}
/// Returns the exercise with the given ID
///
/// If the exercise is not known locally, it is fetched from the server.
@@ -291,6 +304,7 @@ class ExercisesProvider with ChangeNotifier {
return exercise;
} on NoSuchEntryException {
// _logger.finer('Exercise not found locally, fetching from the API');
return handleUpdateExerciseFromApi(database, exerciseId);
}
}
@@ -460,7 +474,7 @@ class ExercisesProvider with ChangeNotifier {
/// Updates the exercise database with *all* the exercises from the server
Future<void> updateExerciseCache(ExerciseDatabase database) async {
final data = await baseProvider.fetchPaginated(
baseProvider.makeUrl(exerciseInfoUrlPath, query: {'limit': '999'}),
baseProvider.makeUrl(exerciseInfoUrlPath, query: {'limit': API_MAX_PAGE_SIZE}),
);
exercises = data.map((e) => Exercise.fromApiDataJson(e, _languages)).toList();

View File

@@ -20,6 +20,7 @@ import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:wger/exceptions/http_exception.dart';
import 'package:wger/exceptions/no_such_entry_exception.dart';
import 'package:wger/helpers/consts.dart';
import 'package:wger/models/measurements/measurement_category.dart';
import 'package:wger/models/measurements/measurement_entry.dart';
import 'package:wger/providers/base_provider.dart';
@@ -54,10 +55,10 @@ class MeasurementProvider with ChangeNotifier {
/// Fetches and sets the categories from the server (no entries)
Future<void> fetchAndSetCategories() async {
// Process the response
final requestUrl = baseProvider.makeUrl(_categoryUrl);
final data = await baseProvider.fetch(requestUrl);
final requestUrl = baseProvider.makeUrl(_categoryUrl, query: {'limit': API_MAX_PAGE_SIZE});
final data = await baseProvider.fetchPaginated(requestUrl);
final List<MeasurementCategory> loadedEntries = [];
for (final entry in data['results']) {
for (final entry in data) {
loadedEntries.add(MeasurementCategory.fromJson(entry));
}
@@ -71,10 +72,13 @@ class MeasurementProvider with ChangeNotifier {
final categoryIndex = _categories.indexOf(category);
// Process the response
final requestUrl = baseProvider.makeUrl(_entryUrl, query: {'category': category.id.toString()});
final data = await baseProvider.fetch(requestUrl);
final requestUrl = baseProvider.makeUrl(
_entryUrl,
query: {'category': category.id.toString(), 'limit': API_MAX_PAGE_SIZE},
);
final data = await baseProvider.fetchPaginated(requestUrl);
final List<MeasurementEntry> loadedEntries = [];
for (final entry in data['results']) {
for (final entry in data) {
loadedEntries.add(MeasurementEntry.fromJson(entry));
}
final MeasurementCategory editedCategory = category.copyWith(entries: loadedEntries);

View File

@@ -114,7 +114,7 @@ class NutritionPlansProvider with ChangeNotifier {
/// object itself and no child attributes
Future<void> fetchAndSetAllPlansSparse() async {
final data = await baseProvider.fetchPaginated(
baseProvider.makeUrl(_nutritionalPlansPath, query: {'limit': '1000'}),
baseProvider.makeUrl(_nutritionalPlansPath, query: {'limit': API_MAX_PAGE_SIZE}),
);
_plans = [];
for (final planData in data) {
@@ -127,7 +127,10 @@ class NutritionPlansProvider with ChangeNotifier {
/// Fetches and sets all plans fully, i.e. with all corresponding child objects
Future<void> fetchAndSetAllPlansFull() async {
final data = await baseProvider.fetchPaginated(baseProvider.makeUrl(_nutritionalPlansPath));
final data = await baseProvider.fetchPaginated(baseProvider.makeUrl(
_nutritionalPlansPath,
query: {'limit': API_MAX_PAGE_SIZE},
));
await Future.wait(data.map((e) => fetchAndSetPlanFull(e['id'])).toList());
}
@@ -466,7 +469,7 @@ class NutritionPlansProvider with ChangeNotifier {
_nutritionDiaryPath,
query: {
'plan': plan.id?.toString(),
'limit': '999',
'limit': API_MAX_PAGE_SIZE,
'ordering': 'datetime',
},
),

View File

@@ -83,6 +83,15 @@ class RoutinesProvider with ChangeNotifier {
_repetitionUnits = repetitionUnits ?? [];
}
/// Returns the current active nutritional plan. At the moment this is just
/// the latest, but this might change in the future.
Routine? get currentRoutine {
if (_routines.isNotEmpty) {
return _routines.first;
}
return null;
}
List<Routine> get items {
return [..._routines];
}
@@ -97,7 +106,6 @@ class RoutinesProvider with ChangeNotifier {
/// Clears all lists
void clear() {
activeRoutine = null;
_routines = [];
_weightUnits = [];
_repetitionUnits = [];
@@ -138,16 +146,6 @@ class RoutinesProvider with ChangeNotifier {
return _routines.indexWhere((routine) => routine.id == id);
}
/// Sets the current active routine. At the moment this is just the latest,
/// but this might change in the future.
void setActiveRoutine() {
if (_routines.isNotEmpty) {
activeRoutine = _routines.first;
} else {
activeRoutine = null;
}
}
/*
* Routines
*/
@@ -155,25 +153,29 @@ class RoutinesProvider with ChangeNotifier {
/// Fetches and sets all workout plans fully, i.e. with all corresponding child
/// attributes
Future<void> fetchAndSetAllRoutinesFull() async {
final data = await baseProvider.fetch(
_logger.fine('Fetching all routines fully');
final data = await baseProvider.fetchPaginated(
baseProvider.makeUrl(
_routinesUrlPath,
query: {'ordering': '-creation_date', 'limit': '1000', 'is_template': 'false'},
query: {'ordering': '-creation_date', 'limit': API_MAX_PAGE_SIZE, 'is_template': 'false'},
),
);
for (final entry in data['results']) {
for (final entry in data) {
await fetchAndSetRoutineFull(entry['id']);
}
setActiveRoutine();
notifyListeners();
}
/// Fetches all routines sparsely, i.e. only with the data on the object itself
/// and no child attributes
Future<void> fetchAndSetAllRoutinesSparse() async {
_logger.fine('Fetching all routines sparsely');
final data = await baseProvider.fetch(
baseProvider.makeUrl(_routinesUrlPath, query: {'limit': '1000', 'is_template': 'false'}),
baseProvider.makeUrl(
_routinesUrlPath,
query: {'limit': API_MAX_PAGE_SIZE, 'is_template': 'false'},
),
);
_routines = [];
for (final workoutPlanData in data['results']) {
@@ -181,7 +183,6 @@ class RoutinesProvider with ChangeNotifier {
_routines.add(plan);
}
setActiveRoutine();
notifyListeners();
}
@@ -214,13 +215,12 @@ class RoutinesProvider with ChangeNotifier {
/// and no child attributes
Future<Routine> fetchAndSetRoutineSparse(int planId) async {
final fullPlanData = await baseProvider.fetch(
baseProvider.makeUrl(_routinesUrlPath, id: planId),
baseProvider.makeUrl(_routinesUrlPath, id: planId, query: {'limit': API_MAX_PAGE_SIZE}),
);
final routine = Routine.fromJson(fullPlanData);
_routines.add(routine);
_routines.sort((a, b) => b.created.compareTo(a.created));
setActiveRoutine();
notifyListeners();
return routine;
}
@@ -338,7 +338,6 @@ class RoutinesProvider with ChangeNotifier {
_routines.add(routine);
}
setActiveRoutine();
notifyListeners();
return routine;
}
@@ -620,7 +619,7 @@ class RoutinesProvider with ChangeNotifier {
*/
Future<List<WorkoutSession>> fetchSessionData() async {
final data = await baseProvider.fetchPaginated(
baseProvider.makeUrl(_sessionUrlPath),
baseProvider.makeUrl(_sessionUrlPath, query: {'limit': API_MAX_PAGE_SIZE}),
);
final sessions = data.map((entry) => WorkoutSession.fromJson(entry)).toList();

View File

@@ -86,6 +86,7 @@ class _HomeTabsScreenState extends State<HomeTabsScreen> with SingleTickerProvid
final measurementProvider = context.read<MeasurementProvider>();
final userProvider = context.read<UserProvider>();
//
// Base data
widget._logger.info('Loading base data');
await Future.wait([
@@ -95,7 +96,18 @@ class _HomeTabsScreenState extends State<HomeTabsScreen> with SingleTickerProvid
nutritionPlansProvider.fetchIngredientsFromCache(),
exercisesProvider.fetchAndSetInitialData(),
]);
exercisesProvider.fetchAndSetAllExercises();
// Workaround for https://github.com/wger-project/flutter/issues/901
// It seems that it can happen that sometimes the units were not loaded properly
// so now we check and try again if necessary. We might need a better general
// solution since this could potentially happen with other data as well.
if (routinesProvider.repetitionUnits.isEmpty || routinesProvider.weightUnits.isEmpty) {
widget._logger.info('Routine units are empty, fetching again');
await routinesProvider.fetchAndSetUnits();
}
//
// Plans, weight and gallery
widget._logger.info('Loading routines, weight, measurements and gallery');
await Future.wait([
@@ -107,6 +119,7 @@ class _HomeTabsScreenState extends State<HomeTabsScreen> with SingleTickerProvid
measurementProvider.fetchAndSetAllCategoriesAndEntries(),
]);
//
// Current nutritional plan
widget._logger.info('Loading current nutritional plan');
if (nutritionPlansProvider.currentPlan != null) {
@@ -114,10 +127,11 @@ class _HomeTabsScreenState extends State<HomeTabsScreen> with SingleTickerProvid
await nutritionPlansProvider.fetchAndSetPlanFull(plan.id!);
}
//
// Current routine
widget._logger.info('Loading current routine');
if (routinesProvider.activeRoutine != null) {
final planId = routinesProvider.activeRoutine!.id!;
if (routinesProvider.currentRoutine != null) {
final planId = routinesProvider.currentRoutine!.id!;
await routinesProvider.fetchAndSetRoutineFull(planId);
}
}

View File

@@ -20,10 +20,10 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:wger/l10n/generated/app_localizations.dart';
import 'package:wger/providers/exercises.dart';
import 'package:wger/providers/nutrition.dart';
import 'package:wger/providers/user.dart';
import 'package:wger/screens/configure_plates_screen.dart';
import 'package:wger/widgets/core/settings/exercise_cache.dart';
class SettingsPage extends StatelessWidget {
static String routeName = '/SettingsPage';
@@ -33,7 +33,6 @@ class SettingsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context);
final exerciseProvider = Provider.of<ExercisesProvider>(context, listen: false);
final nutritionProvider = Provider.of<NutritionPlansProvider>(context, listen: false);
final userProvider = Provider.of<UserProvider>(context);
@@ -47,24 +46,7 @@ class SettingsPage extends StatelessWidget {
style: Theme.of(context).textTheme.headlineSmall,
),
),
ListTile(
title: Text(i18n.settingsExerciseCacheDescription),
trailing: IconButton(
key: const ValueKey('cacheIconExercises'),
icon: const Icon(Icons.delete),
onPressed: () async {
await exerciseProvider.clearAllCachesAndPrefs();
if (context.mounted) {
final snackBar = SnackBar(
content: Text(i18n.settingsCacheDeletedSnackbar),
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
},
),
),
const SettingsExerciseCache(),
ListTile(
title: Text(i18n.settingsIngredientCacheDescription),
trailing: IconButton(
@@ -83,6 +65,12 @@ class SettingsPage extends StatelessWidget {
},
),
),
ListTile(
title: Text(
i18n.others,
style: Theme.of(context).textTheme.headlineSmall,
),
),
ListTile(
title: Text(i18n.themeMode),
trailing: DropdownButton<ThemeMode>(

View File

@@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:wger/l10n/generated/app_localizations.dart';
import 'package:wger/providers/exercises.dart';
class SettingsExerciseCache extends StatefulWidget {
const SettingsExerciseCache({super.key});
@override
State<SettingsExerciseCache> createState() => _SettingsExerciseCacheState();
}
class _SettingsExerciseCacheState extends State<SettingsExerciseCache> {
bool _isRefreshLoading = false;
String _subtitle = '';
@override
Widget build(BuildContext context) {
final exerciseProvider = Provider.of<ExercisesProvider>(context, listen: false);
final i18n = AppLocalizations.of(context);
return ListTile(
enabled: !_isRefreshLoading,
title: Text(i18n.settingsExerciseCacheDescription),
subtitle: _subtitle.isNotEmpty ? Text(_subtitle) : null,
trailing: Row(mainAxisSize: MainAxisSize.min, children: [
IconButton(
key: const ValueKey('cacheIconExercisesRefresh'),
icon: _isRefreshLoading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.refresh),
onPressed: _isRefreshLoading
? null
: () async {
setState(() => _isRefreshLoading = true);
// Note: status messages are currently left in English on purpose
try {
setState(() => _subtitle = 'Clearing cache...');
await exerciseProvider.clearAllCachesAndPrefs();
if (mounted) {
setState(() => _subtitle = 'Loading languages and units...');
}
await exerciseProvider.fetchAndSetInitialData();
if (mounted) {
setState(() => _subtitle = 'Loading all exercises from server...');
}
await exerciseProvider.fetchAndSetAllExercises();
if (mounted) {
setState(() => _subtitle = '');
}
} finally {
if (mounted) {
setState(() => _isRefreshLoading = false);
}
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(i18n.success)),
);
}
}
},
),
IconButton(
key: const ValueKey('cacheIconExercisesDelete'),
icon: const Icon(Icons.delete),
onPressed: () async {
await exerciseProvider.clearAllCachesAndPrefs();
if (context.mounted) {
final snackBar = SnackBar(
content: Text(i18n.settingsCacheDeletedSnackbar),
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
},
)
]),
);
}
}

View File

@@ -87,6 +87,7 @@ class _DashboardCalendarWidgetState extends State<DashboardCalendarWidget>
void loadEvents() async {
final numberFormat = NumberFormat.decimalPattern(Localizations.localeOf(context).toString());
final i18n = AppLocalizations.of(context);
// Process weight entries
final weightProvider = context.read<BodyWeightProvider>();
@@ -98,7 +99,7 @@ class _DashboardCalendarWidgetState extends State<DashboardCalendarWidget>
}
// Add events to lists
_events[date]!.add(Event(EventType.weight, '${numberFormat.format(entry.weight)} kg'));
_events[date]?.add(Event(EventType.weight, '${numberFormat.format(entry.weight)} kg'));
}
// Process measurements
@@ -111,7 +112,7 @@ class _DashboardCalendarWidgetState extends State<DashboardCalendarWidget>
_events[date] = [];
}
_events[date]!.add(Event(
_events[date]?.add(Event(
EventType.measurement,
'${category.name}: ${numberFormat.format(entry.value)} ${category.unit}',
));
@@ -130,9 +131,9 @@ class _DashboardCalendarWidgetState extends State<DashboardCalendarWidget>
time = '(${timeToString(session.timeStart)} - ${timeToString(session.timeEnd)})';
// Add events to lists
_events[date]!.add(Event(
_events[date]?.add(Event(
EventType.session,
'${AppLocalizations.of(context).impression}: ${session.impressionAsString} $time',
'${i18n.impression}: ${session.impressionAsString} $time',
));
}
});
@@ -148,15 +149,15 @@ class _DashboardCalendarWidgetState extends State<DashboardCalendarWidget>
}
// Add events to lists
_events[date]!.add(Event(
_events[date]?.add(Event(
EventType.caloriesDiary,
AppLocalizations.of(context).kcalValue(entry.value.energy.toStringAsFixed(0)),
i18n.kcalValue(entry.value.energy.toStringAsFixed(0)),
));
}
}
// Add initial selected day to events list
_selectedEvents.value = _getEventsForDay(_selectedDay!);
_selectedEvents.value = _selectedDay != null ? _getEventsForDay(_selectedDay!) : [];
}
@override

View File

@@ -44,7 +44,7 @@ class _DashboardRoutineWidgetState extends State<DashboardRoutineWidget> {
@override
Widget build(BuildContext context) {
final routine = context.watch<RoutinesProvider>().activeRoutine;
final routine = context.watch<RoutinesProvider>().currentRoutine;
_hasContent = routine != null;
final dateFormat = DateFormat.yMd(Localizations.localeOf(context).languageCode);

View File

@@ -30,6 +30,7 @@ class _ExerciseAutocompleterState extends State<ExerciseAutocompleter> {
children: [
TypeAheadField<Exercise>(
key: const Key('field-typeahead'),
debounceDuration: const Duration(milliseconds: 500),
decorationBuilder: (context, child) {
return Material(
type: MaterialType.card,

View File

@@ -17,7 +17,10 @@
*/
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:logging/logging.dart';
import 'package:video_player/video_player.dart';
import 'package:wger/helpers/errors.dart';
import 'package:wger/models/exercises/video.dart';
class ExerciseVideoWidget extends StatefulWidget {
@@ -31,35 +34,56 @@ class ExerciseVideoWidget extends StatefulWidget {
class _ExerciseVideoWidgetState extends State<ExerciseVideoWidget> {
late VideoPlayerController _controller;
bool hasError = false;
final logger = Logger('ExerciseVideoWidgetState');
@override
void initState() {
super.initState();
_controller = VideoPlayerController.network(widget.video.url);
_controller.addListener(() {
_controller = VideoPlayerController.networkUrl(widget.video.uri);
_initializeVideo();
}
Future<void> _initializeVideo() async {
try {
await _controller.initialize();
setState(() {});
});
_controller.initialize().then((_) => setState(() {}));
} on PlatformException catch (e) {
if (mounted) {
setState(() => hasError = true);
}
logger.warning('PlatformException while initializing video: ${e.message}');
}
}
@override
void dispose() {
super.dispose();
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return _controller.value.isInitialized
? AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: Stack(alignment: Alignment.bottomCenter, children: [
VideoPlayer(_controller),
_ControlsOverlay(controller: _controller),
VideoProgressIndicator(_controller, allowScrubbing: true),
]),
return hasError
? const GeneralErrorsWidget(
[
'An error happened while loading the video. If you can, please check the application logs.'
],
)
: Container();
: _controller.value.isInitialized
? AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: Stack(
alignment: Alignment.bottomCenter,
children: [
VideoPlayer(_controller),
_ControlsOverlay(controller: _controller),
VideoProgressIndicator(_controller, allowScrubbing: true),
],
),
)
: Container();
}
}

View File

@@ -21,6 +21,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:flutter_zxing/flutter_zxing.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:logging/logging.dart';
import 'package:provider/provider.dart';
import 'package:wger/helpers/consts.dart';
import 'package:wger/helpers/misc.dart';
@@ -54,18 +55,20 @@ class ScanReader extends StatelessWidget {
}
class IngredientTypeahead extends StatefulWidget {
final _logger = Logger('IngredientTypeahead');
final TextEditingController _ingredientController;
final TextEditingController _ingredientIdController;
final String barcode;
final bool? test;
final bool test;
final bool showScanner;
final Function(int id, String name, num? amount) selectIngredient;
final Function() unSelectIngredient;
final Function(String query) updateSearchQuery;
const IngredientTypeahead(
IngredientTypeahead(
this._ingredientIdController,
this._ingredientController, {
this.showScanner = true,
@@ -90,19 +93,21 @@ class _IngredientTypeaheadState extends State<IngredientTypeahead> {
barcode = widget.barcode; // for unit tests
}
Future<String> readerscan(BuildContext context) async {
Future<String> openBarcodeScan(BuildContext context) async {
try {
final code = await Navigator.of(context)
.push<String?>(MaterialPageRoute(builder: (context) => const ScanReader()));
if (code == null) {
return '';
}
if (code.compareTo('-1') == 0) {
if (code == '-1') {
return '';
}
return code;
} on PlatformException {
} on PlatformException catch (e) {
widget._logger.warning('PlatformException during barcode scan: $e');
return '';
}
}
@@ -113,6 +118,7 @@ class _IngredientTypeaheadState extends State<IngredientTypeahead> {
children: [
TypeAheadField<IngredientApiSearchEntry>(
controller: widget._ingredientController,
debounceDuration: const Duration(milliseconds: 500),
builder: (context, controller, focusNode) {
return TextFormField(
controller: controller,
@@ -124,11 +130,6 @@ class _IngredientTypeaheadState extends State<IngredientTypeahead> {
}
return null;
},
onChanged: (value) {
widget.updateSearchQuery(value);
// unselect to start a new search
widget.unSelectIngredient();
},
decoration: InputDecoration(
prefixIcon: const Icon(Icons.search),
labelText: AppLocalizations.of(context).searchIngredient,
@@ -142,6 +143,10 @@ class _IngredientTypeaheadState extends State<IngredientTypeahead> {
return null;
}
widget.updateSearchQuery(pattern);
// unselect to start a new search
widget.unSelectIngredient();
return Provider.of<NutritionPlansProvider>(context, listen: false).searchIngredient(
pattern,
languageCode: Localizations.localeOf(context).languageCode,
@@ -202,8 +207,8 @@ class _IngredientTypeaheadState extends State<IngredientTypeahead> {
key: const Key('scan-button'),
icon: const FaIcon(FontAwesomeIcons.barcode),
onPressed: () async {
if (!widget.test!) {
barcode = await readerscan(context);
if (!widget.test) {
barcode = await openBarcodeScan(context);
}
if (!mounted) {

View File

@@ -18,6 +18,7 @@
import 'package:clock/clock.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:provider/provider.dart';
import 'package:wger/exceptions/http_exception.dart';
import 'package:wger/helpers/consts.dart';
@@ -28,6 +29,7 @@ import 'package:wger/models/workouts/session.dart';
import 'package:wger/providers/routines.dart';
class SessionForm extends StatefulWidget {
final _logger = Logger('SessionForm');
final WorkoutSession _session;
final int _routineId;
final Function()? _onSaved;
@@ -215,11 +217,18 @@ class _SessionFormState extends State<SessionForm> {
}
_form.currentState!.save();
// Reset any previous error message
setState(() {
errorMessage = const SizedBox.shrink();
});
// Save the entry on the server
try {
if (widget._session.id == null) {
widget._logger.fine('Adding new session');
await routinesProvider.addSession(widget._session, widget._routineId);
} else {
widget._logger.fine('Editing existing session with id ${widget._session.id}');
await routinesProvider.editSession(widget._session);
}
@@ -231,6 +240,7 @@ class _SessionFormState extends State<SessionForm> {
widget._onSaved!();
}
} on WgerHttpException catch (error) {
widget._logger.warning('Could not save session: $error');
if (context.mounted) {
setState(() {
errorMessage = FormHttpErrorsWidget(error);

View File

@@ -72,6 +72,14 @@ class _GymModeState extends ConsumerState<GymMode> {
}
Future<int> _loadGymState() async {
// Re-fetch the current routine data to ensure we have the latest session
// data since it is possible that the user created or deleted it from the
// web interface.
await context
.read<RoutinesProvider>()
.fetchAndSetRoutineFull(widget._dayDataGym.day!.routineId);
widget._logger.fine('Refreshed routine data');
final validUntil = ref.read(gymStateProvider).validUntil;
final currentPage = ref.read(gymStateProvider).currentPage;
final savedDayId = ref.read(gymStateProvider).dayId;

View File

@@ -53,8 +53,6 @@ class _RoutinesListState extends State<RoutinesList> {
return Card(
child: ListTile(
onTap: () async {
widget._routineProvider.activeRoutine = currentRoutine;
setState(() {
_loadingRoutine = currentRoutine.id;
});

View File

@@ -25,24 +25,11 @@ import 'package:wger/models/workouts/routine.dart';
import 'package:wger/theme/theme.dart';
import 'package:wger/widgets/routines/log.dart';
class WorkoutLogs extends StatefulWidget {
class WorkoutLogs extends StatelessWidget {
final Routine _routine;
const WorkoutLogs(this._routine);
@override
_WorkoutLogsState createState() => _WorkoutLogsState();
}
class _WorkoutLogsState extends State<WorkoutLogs> {
final dayController = TextEditingController();
@override
void dispose() {
dayController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ListView(
@@ -62,7 +49,7 @@ class _WorkoutLogsState extends State<WorkoutLogs> {
),
SizedBox(
width: double.infinity,
child: WorkoutLogCalendar(widget._routine),
child: WorkoutLogCalendar(_routine),
),
],
);

View File

@@ -53,10 +53,10 @@ packages:
dependency: transitive
description:
name: build
sha256: "6439a9c71a4e6eca8d9490c1b380a25b02675aa688137dfbe66d2062884a23ac"
sha256: ce76b1d48875e3233fde17717c23d1f60a91cc631597e49a400c89b475395b1d
url: "https://pub.dev"
source: hosted
version: "3.0.2"
version: "3.1.0"
build_config:
dependency: transitive
description:
@@ -77,26 +77,26 @@ packages:
dependency: transitive
description:
name: build_resolvers
sha256: "2b21a125d66a86b9511cc3fb6c668c42e9a1185083922bf60e46d483a81a9712"
sha256: d1d57f7807debd7349b4726a19fd32ec8bc177c71ad0febf91a20f84cd2d4b46
url: "https://pub.dev"
source: hosted
version: "3.0.2"
version: "3.0.3"
build_runner:
dependency: "direct dev"
description:
name: build_runner
sha256: fd3c09f4bbff7fa6e8d8ef688a0b2e8a6384e6483a25af0dac75fef362bcfe6f
sha256: b24597fceb695969d47025c958f3837f9f0122e237c6a22cb082a5ac66c3ca30
url: "https://pub.dev"
source: hosted
version: "2.7.0"
version: "2.7.1"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
sha256: ab27e46c8aa233e610cf6084ee6d8a22c6f873a0a9929241d8855b7a72978ae7
sha256: "066dda7f73d8eb48ba630a55acb50c4a84a2e6b453b1cb4567f581729e794f7b"
url: "https://pub.dev"
source: hosted
version: "9.3.0"
version: "9.3.1"
built_collection:
dependency: transitive
description:
@@ -293,10 +293,10 @@ packages:
dependency: "direct dev"
description:
name: drift_dev
sha256: "2fc05ad458a7c562755bf0cae11178dfc58387a416829b78d4da5155a61465fd"
sha256: d6646ee608b9f359b023ac329321bc9c63b098217291de079b8b2334a48abf39
url: "https://pub.dev"
source: hosted
version: "2.28.1"
version: "2.28.2"
equatable:
dependency: "direct main"
description:
@@ -373,18 +373,18 @@ packages:
dependency: "direct main"
description:
name: fl_chart
sha256: "577aeac8ca414c25333334d7c4bb246775234c0e44b38b10a82b559dd4d764e7"
sha256: d3f82f4a38e33ba23d05a08ff304d7d8b22d2a59a5503f20bd802966e915db89
url: "https://pub.dev"
source: hosted
version: "1.0.0"
version: "1.1.0"
flex_color_scheme:
dependency: "direct main"
description:
name: flex_color_scheme
sha256: "3344f8f6536c6ce0473b98e9f084ef80ca89024ad3b454f9c32cf840206f4387"
sha256: "034d5720747e6af39b2ad090d82dd92d33fde68e7964f1814b714c9d49ddbd64"
url: "https://pub.dev"
source: hosted
version: "8.2.0"
version: "8.3.0"
flex_seed_scheme:
dependency: "direct main"
description:
@@ -500,10 +500,10 @@ packages:
dependency: "direct main"
description:
name: flutter_svg
sha256: cd57f7969b4679317c17af6fd16ee233c1e60a82ed209d8a475c54fd6fd6f845
sha256: b9c2ad5872518a27507ab432d1fb97e8813b05f0fc693f9d40fad06d073e0678
url: "https://pub.dev"
source: hosted
version: "2.2.0"
version: "2.2.1"
flutter_svg_icons:
dependency: "direct main"
description:
@@ -542,10 +542,10 @@ packages:
dependency: "direct main"
description:
name: font_awesome_flutter
sha256: f50ce90dbe26d977415b9540400d6778bef00894aced6358ae578abd92b14b10
sha256: "27af5982e6c510dec1ba038eff634fa284676ee84e3fd807225c80c4ad869177"
url: "https://pub.dev"
source: hosted
version: "10.9.0"
version: "10.10.0"
freezed:
dependency: "direct dev"
description:
@@ -736,34 +736,34 @@ packages:
dependency: "direct dev"
description:
name: json_serializable
sha256: ce2cf974ccdee13be2a510832d7fba0b94b364e0b0395dee42abaa51b855be27
sha256: "33a040668b31b320aafa4822b7b1e177e163fc3c1e835c6750319d4ab23aa6fe"
url: "https://pub.dev"
source: hosted
version: "6.10.0"
version: "6.11.1"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0"
url: "https://pub.dev"
source: hosted
version: "10.0.9"
version: "11.0.1"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.9"
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
version: "3.0.2"
lints:
dependency: transitive
description:
@@ -1189,10 +1189,10 @@ packages:
dependency: transitive
description:
name: source_gen
sha256: fc787b1f89ceac9580c3616f899c9a447413cbdac1df071302127764c023a134
sha256: "7b19d6ba131c6eb98bfcbf8d56c1a7002eba438af2e7ae6f8398b2b0f4f381e3"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
version: "3.1.0"
source_helper:
dependency: transitive
description:
@@ -1229,10 +1229,10 @@ packages:
dependency: transitive
description:
name: sqlparser
sha256: "7c859c803cf7e9a84d6db918bac824545045692bbe94a6386bd3a45132235d09"
sha256: "57090342af1ce32bb499aa641f4ecdd2d6231b9403cea537ac059e803cc20d67"
url: "https://pub.dev"
source: hosted
version: "0.41.1"
version: "0.41.2"
stack_trace:
dependency: transitive
description:
@@ -1301,10 +1301,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
url: "https://pub.dev"
source: hosted
version: "0.7.4"
version: "0.7.6"
timing:
dependency: transitive
description:
@@ -1413,10 +1413,10 @@ packages:
dependency: transitive
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version: "2.2.0"
version:
dependency: "direct main"
description:
@@ -1554,5 +1554,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.8.0 <4.0.0"
flutter: ">=3.29.0"
dart: ">=3.9.0 <4.0.0"
flutter: ">=3.35.0"

View File

@@ -38,16 +38,16 @@ dependencies:
cupertino_icons: ^1.0.8
drift: ^2.28.1
equatable: ^2.0.7
fl_chart: ^1.0.0
flex_color_scheme: ^8.1.1
fl_chart: ^1.1.0
flex_color_scheme: ^8.3.0
flex_seed_scheme: ^3.5.1
flutter_html: ^3.0.0
flutter_staggered_grid_view: ^0.7.0
flutter_svg: ^2.2.0
flutter_svg: ^2.2.1
flutter_svg_icons: ^0.0.1
flutter_typeahead: ^5.2.0
flutter_zxing: ^2.2.1
font_awesome_flutter: ^10.9.0
font_awesome_flutter: ^10.10.0
freezed_annotation: ^3.0.0
get_it: ^8.2.0
http: ^1.5.0
@@ -74,12 +74,12 @@ dev_dependencies:
sdk: flutter
integration_test:
sdk: flutter
build_runner: ^2.7.0
build_runner: ^2.7.1
cider: ^0.2.7
drift_dev: ^2.28.1
drift_dev: ^2.28.2
flutter_lints: ^6.0.0
freezed: ^3.2.0
json_serializable: ^6.9.5
json_serializable: ^6.11.1
mockito: ^5.4.6
network_image_mock: ^2.1.1
shared_preferences_platform_interface: ^2.0.0

View File

@@ -39,7 +39,7 @@ import 'settings_test.mocks.dart';
WgerBaseProvider,
SharedPreferencesAsync,
])
void main() async {
void main() {
final mockExerciseProvider = MockExercisesProvider();
final mockNutritionProvider = MockNutritionPlansProvider();
final mockSharedPreferences = MockSharedPreferencesAsync();
@@ -68,12 +68,22 @@ void main() async {
group('Cache', () {
testWidgets('Test resetting the exercise cache', (WidgetTester tester) async {
await tester.pumpWidget(createSettingsScreen());
await tester.tap(find.byKey(const ValueKey('cacheIconExercises')));
await tester.tap(find.byKey(const ValueKey('cacheIconExercisesDelete')));
await tester.pumpAndSettle();
verify(mockExerciseProvider.clearAllCachesAndPrefs());
});
testWidgets('Test refreshing the exercise cache', (WidgetTester tester) async {
await tester.pumpWidget(createSettingsScreen());
await tester.tap(find.byKey(const ValueKey('cacheIconExercisesRefresh')));
await tester.pumpAndSettle();
verify(mockExerciseProvider.clearAllCachesAndPrefs());
verify(mockExerciseProvider.fetchAndSetInitialData());
verify(mockExerciseProvider.fetchAndSetAllExercises());
});
testWidgets('Test resetting the ingredient cache', (WidgetTester tester) async {
await tester.pumpWidget(createSettingsScreen());
await tester.tap(find.byKey(const ValueKey('cacheIconIngredients')));

View File

@@ -490,6 +490,16 @@ class MockExercisesProvider extends _i1.Mock implements _i17.ExercisesProvider {
returnValueForMissingStub: _i18.Future<void>.value(),
) as _i18.Future<void>);
@override
_i18.Future<void> fetchAndSetAllExercises() => (super.noSuchMethod(
Invocation.method(
#fetchAndSetAllExercises,
[],
),
returnValue: _i18.Future<void>.value(),
returnValueForMissingStub: _i18.Future<void>.value(),
) as _i18.Future<void>);
@override
_i18.Future<_i4.Exercise?> fetchAndSetExercise(int? exerciseId) => (super.noSuchMethod(
Invocation.method(

View File

@@ -945,6 +945,16 @@ class MockExercisesProvider extends _i1.Mock implements _i20.ExercisesProvider {
returnValueForMissingStub: _i15.Future<void>.value(),
) as _i15.Future<void>);
@override
_i15.Future<void> fetchAndSetAllExercises() => (super.noSuchMethod(
Invocation.method(
#fetchAndSetAllExercises,
[],
),
returnValue: _i15.Future<void>.value(),
returnValueForMissingStub: _i15.Future<void>.value(),
) as _i15.Future<void>);
@override
_i15.Future<_i3.Exercise?> fetchAndSetExercise(int? exerciseId) => (super.noSuchMethod(
Invocation.method(

View File

@@ -377,6 +377,16 @@ class MockExercisesProvider extends _i1.Mock implements _i9.ExercisesProvider {
returnValueForMissingStub: _i10.Future<void>.value(),
) as _i10.Future<void>);
@override
_i10.Future<void> fetchAndSetAllExercises() => (super.noSuchMethod(
Invocation.method(
#fetchAndSetAllExercises,
[],
),
returnValue: _i10.Future<void>.value(),
returnValueForMissingStub: _i10.Future<void>.value(),
) as _i10.Future<void>);
@override
_i10.Future<_i4.Exercise?> fetchAndSetExercise(int? exerciseId) => (super.noSuchMethod(
Invocation.method(

View File

@@ -6,6 +6,7 @@ import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:wger/exceptions/http_exception.dart';
import 'package:wger/exceptions/no_such_entry_exception.dart';
import 'package:wger/helpers/consts.dart';
import 'package:wger/models/measurements/measurement_category.dart';
import 'package:wger/models/measurements/measurement_entry.dart';
import 'package:wger/providers/base_provider.dart';
@@ -14,8 +15,6 @@ import 'package:wger/providers/measurement.dart';
import '../fixtures/fixture_reader.dart';
import 'measurement_provider_test.mocks.dart';
// class MockWgerBaseProvider extends Mock implements WgerBaseProvider {}
@GenerateMocks([WgerBaseProvider])
void main() {
late MeasurementProvider measurementProvider;
@@ -48,16 +47,17 @@ void main() {
measurementProvider = MeasurementProvider(mockWgerBaseProvider);
when(mockWgerBaseProvider.makeUrl(any)).thenReturn(tCategoryUri);
when(mockWgerBaseProvider.makeUrl(any, id: anyNamed('id'))).thenReturn(tCategoryUri);
when(mockWgerBaseProvider.fetch(any))
.thenAnswer((realInvocation) => Future.value(tMeasurementCategoriesMap));
when(mockWgerBaseProvider.makeUrl(any, id: anyNamed('id'), query: anyNamed('query')))
.thenReturn(tCategoryUri);
when(mockWgerBaseProvider.fetchPaginated(any))
.thenAnswer((realInvocation) => Future.value(tMeasurementCategoriesMap['results']));
when(mockWgerBaseProvider.makeUrl(entryUrl, query: anyNamed('query')))
.thenReturn(tCategoryEntriesUri);
when(mockWgerBaseProvider.makeUrl(entryUrl, id: anyNamed('id'), query: anyNamed('query')))
.thenReturn(tCategoryEntriesUri);
when(mockWgerBaseProvider.fetch(tCategoryEntriesUri))
.thenAnswer((realInvocation) => Future.value(tMeasurementCategoryMap));
when(mockWgerBaseProvider.fetchPaginated(tCategoryEntriesUri))
.thenAnswer((realInvocation) => Future.value(tMeasurementCategoryMap['results']));
});
group('clear()', () {
@@ -100,7 +100,7 @@ void main() {
await measurementProvider.fetchAndSetCategories();
// assert
verify(mockWgerBaseProvider.makeUrl(categoryUrl));
verify(mockWgerBaseProvider.makeUrl(categoryUrl, query: {'limit': API_MAX_PAGE_SIZE}));
});
test('should fetch data from api', () async {
@@ -108,7 +108,7 @@ void main() {
await measurementProvider.fetchAndSetCategories();
// assert
verify(mockWgerBaseProvider.fetch(tCategoryUri));
verify(mockWgerBaseProvider.fetchPaginated(tCategoryUri));
});
test('should set categories', () async {
@@ -130,7 +130,10 @@ void main() {
await measurementProvider.fetchAndSetCategoryEntries(tCategoryId);
// assert
verify(mockWgerBaseProvider.makeUrl(entryUrl, query: {'category': tCategoryId.toString()}));
verify(mockWgerBaseProvider.makeUrl(
entryUrl,
query: {'category': tCategoryId.toString(), 'limit': API_MAX_PAGE_SIZE},
));
});
test('should fetch categories entries for id', () async {
@@ -138,7 +141,7 @@ void main() {
await measurementProvider.fetchAndSetCategoryEntries(tCategoryId);
// assert
verify(mockWgerBaseProvider.fetch(tCategoryEntriesUri));
verify(mockWgerBaseProvider.fetchPaginated(tCategoryEntriesUri));
});
test('should add entries to category in list', () async {

View File

@@ -294,15 +294,6 @@ class MockRoutinesProvider extends _i1.Mock implements _i12.RoutinesProvider {
returnValue: 0,
) as int);
@override
void setActiveRoutine() => super.noSuchMethod(
Invocation.method(
#setActiveRoutine,
[],
),
returnValueForMissingStub: null,
);
@override
_i13.Future<void> fetchAndSetAllRoutinesFull() => (super.noSuchMethod(
Invocation.method(

View File

@@ -294,15 +294,6 @@ class MockRoutinesProvider extends _i1.Mock implements _i12.RoutinesProvider {
returnValue: 0,
) as int);
@override
void setActiveRoutine() => super.noSuchMethod(
Invocation.method(
#setActiveRoutine,
[],
),
returnValueForMissingStub: null,
);
@override
_i13.Future<void> fetchAndSetAllRoutinesFull() => (super.noSuchMethod(
Invocation.method(

View File

@@ -44,28 +44,31 @@ import '../../test_data/exercises.dart';
import '../../test_data/routines.dart';
import 'gym_mode_screen_test.mocks.dart';
@GenerateMocks([WgerBaseProvider, ExercisesProvider])
@GenerateMocks([WgerBaseProvider, ExercisesProvider, RoutinesProvider])
void main() {
final mockBaseProvider = MockWgerBaseProvider();
final key = GlobalKey<NavigatorState>();
final mockRoutinesProvider = MockRoutinesProvider();
final mockExerciseProvider = MockExercisesProvider();
final testRoutine = getTestRoutine();
final testExercises = getTestExercises();
setUp(() {
when(mockRoutinesProvider.findById(any)).thenReturn(testRoutine);
when(mockRoutinesProvider.items).thenReturn([testRoutine]);
when(mockRoutinesProvider.repetitionUnits).thenReturn(testRepetitionUnits);
when(mockRoutinesProvider.findRepetitionUnitById(1)).thenReturn(testRepetitionUnit1);
when(mockRoutinesProvider.weightUnits).thenReturn(testWeightUnits);
when(mockRoutinesProvider.findWeightUnitById(1)).thenReturn(testWeightUnit1);
when(mockRoutinesProvider.fetchAndSetRoutineFull(any))
.thenAnswer((_) => Future.value(testRoutine));
SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty();
});
Widget renderGymMode({locale = 'en'}) {
return ChangeNotifierProvider<RoutinesProvider>(
create: (context) => RoutinesProvider(
mockBaseProvider,
mockExerciseProvider,
[testRoutine],
repetitionUnits: testRepetitionUnits,
weightUnits: testWeightUnits,
),
create: (context) => mockRoutinesProvider,
child: ChangeNotifierProvider<ExercisesProvider>(
create: (context) => mockExerciseProvider,
child: riverpod.ProviderScope(

File diff suppressed because it is too large Load Diff

View File

@@ -294,15 +294,6 @@ class MockRoutinesProvider extends _i1.Mock implements _i12.RoutinesProvider {
returnValue: 0,
) as int);
@override
void setActiveRoutine() => super.noSuchMethod(
Invocation.method(
#setActiveRoutine,
[],
),
returnValueForMissingStub: null,
);
@override
_i13.Future<void> fetchAndSetAllRoutinesFull() => (super.noSuchMethod(
Invocation.method(

View File

@@ -294,15 +294,6 @@ class MockRoutinesProvider extends _i1.Mock implements _i12.RoutinesProvider {
returnValue: 0,
) as int);
@override
void setActiveRoutine() => super.noSuchMethod(
Invocation.method(
#setActiveRoutine,
[],
),
returnValueForMissingStub: null,
);
@override
_i13.Future<void> fetchAndSetAllRoutinesFull() => (super.noSuchMethod(
Invocation.method(

View File

@@ -294,15 +294,6 @@ class MockRoutinesProvider extends _i1.Mock implements _i12.RoutinesProvider {
returnValue: 0,
) as int);
@override
void setActiveRoutine() => super.noSuchMethod(
Invocation.method(
#setActiveRoutine,
[],
),
returnValueForMissingStub: null,
);
@override
_i13.Future<void> fetchAndSetAllRoutinesFull() => (super.noSuchMethod(
Invocation.method(

View File

@@ -294,15 +294,6 @@ class MockRoutinesProvider extends _i1.Mock implements _i12.RoutinesProvider {
returnValue: 0,
) as int);
@override
void setActiveRoutine() => super.noSuchMethod(
Invocation.method(
#setActiveRoutine,
[],
),
returnValueForMissingStub: null,
);
@override
_i13.Future<void> fetchAndSetAllRoutinesFull() => (super.noSuchMethod(
Invocation.method(

View File

@@ -294,15 +294,6 @@ class MockRoutinesProvider extends _i1.Mock implements _i12.RoutinesProvider {
returnValue: 0,
) as int);
@override
void setActiveRoutine() => super.noSuchMethod(
Invocation.method(
#setActiveRoutine,
[],
),
returnValueForMissingStub: null,
);
@override
_i13.Future<void> fetchAndSetAllRoutinesFull() => (super.noSuchMethod(
Invocation.method(

View File

@@ -294,15 +294,6 @@ class MockRoutinesProvider extends _i1.Mock implements _i12.RoutinesProvider {
returnValue: 0,
) as int);
@override
void setActiveRoutine() => super.noSuchMethod(
Invocation.method(
#setActiveRoutine,
[],
),
returnValueForMissingStub: null,
);
@override
_i13.Future<void> fetchAndSetAllRoutinesFull() => (super.noSuchMethod(
Invocation.method(

View File

@@ -54,11 +54,12 @@ void main() {
});
group('test the workout routine provider', () {
test('Test fetching and setting a plan', () async {
test('Test fetching and setting a routine', () async {
final exercisesProvider = ExercisesProvider(mockBaseProvider);
final uri = Uri.https('localhost', 'api/v2/routine/325397/');
when(mockBaseProvider.makeUrl('routine', id: 325397)).thenReturn(uri);
when(mockBaseProvider.makeUrl('routine', id: 325397, query: {'limit': API_MAX_PAGE_SIZE}))
.thenReturn(uri);
when(mockBaseProvider.fetch(uri)).thenAnswer(
(_) async => Future.value({
'id': 325397,

View File

@@ -580,6 +580,16 @@ class MockExercisesProvider extends _i1.Mock implements _i12.ExercisesProvider {
returnValueForMissingStub: _i11.Future<void>.value(),
) as _i11.Future<void>);
@override
_i11.Future<void> fetchAndSetAllExercises() => (super.noSuchMethod(
Invocation.method(
#fetchAndSetAllExercises,
[],
),
returnValue: _i11.Future<void>.value(),
returnValueForMissingStub: _i11.Future<void>.value(),
) as _i11.Future<void>);
@override
_i11.Future<_i6.Exercise?> fetchAndSetExercise(int? exerciseId) => (super.noSuchMethod(
Invocation.method(

View File

@@ -294,15 +294,6 @@ class MockRoutinesProvider extends _i1.Mock implements _i12.RoutinesProvider {
returnValue: 0,
) as int);
@override
void setActiveRoutine() => super.noSuchMethod(
Invocation.method(
#setActiveRoutine,
[],
),
returnValueForMissingStub: null,
);
@override
_i13.Future<void> fetchAndSetAllRoutinesFull() => (super.noSuchMethod(
Invocation.method(

View File

@@ -294,15 +294,6 @@ class MockRoutinesProvider extends _i1.Mock implements _i12.RoutinesProvider {
returnValue: 0,
) as int);
@override
void setActiveRoutine() => super.noSuchMethod(
Invocation.method(
#setActiveRoutine,
[],
),
returnValueForMissingStub: null,
);
@override
_i13.Future<void> fetchAndSetAllRoutinesFull() => (super.noSuchMethod(
Invocation.method(