mirror of
https://github.com/wger-project/flutter.git
synced 2026-02-18 23:42:00 +01:00
Merge branch 'refs/heads/master' into nutrition-plan-stats
This commit is contained in:
2
.github/actions/flutter-common/action.yml
vendored
2
.github/actions/flutter-common/action.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -21,6 +21,6 @@
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>12.0</string>
|
||||
<string>13.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>(
|
||||
|
||||
89
lib/widgets/core/settings/exercise_cache.dart
Normal file
89
lib/widgets/core/settings/exercise_cache.dart
Normal 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);
|
||||
}
|
||||
},
|
||||
)
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -53,8 +53,6 @@ class _RoutinesListState extends State<RoutinesList> {
|
||||
return Card(
|
||||
child: ListTile(
|
||||
onTap: () async {
|
||||
widget._routineProvider.activeRoutine = currentRoutine;
|
||||
|
||||
setState(() {
|
||||
_loadingRoutine = currentRoutine.id;
|
||||
});
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
72
pubspec.lock
72
pubspec.lock
@@ -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"
|
||||
|
||||
14
pubspec.yaml
14
pubspec.yaml
@@ -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
|
||||
|
||||
@@ -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')));
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user