mirror of
https://github.com/wger-project/flutter.git
synced 2026-02-18 00:17:48 +01:00
Merge branch 'master' into fork/dhituval/issue852/hide-diet-plan
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.38.3
|
||||
flutter-version: 3.38.6
|
||||
cache: true
|
||||
|
||||
- name: Install Flutter dependencies
|
||||
|
||||
4
.github/workflows/build-android.yml
vendored
4
.github/workflows/build-android.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
- name: Build APK
|
||||
run: flutter build apk --release
|
||||
|
||||
- uses: actions/upload-artifact@v5
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: builds-apk
|
||||
path: build/app/outputs/flutter-apk/app-release.apk
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
- name: Build AAB
|
||||
run: flutter build appbundle --release
|
||||
|
||||
- uses: actions/upload-artifact@v5
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: builds-aab
|
||||
path: build/app/outputs/bundle/release/app-release.aab
|
||||
6
.github/workflows/build-apple.yml
vendored
6
.github/workflows/build-apple.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
cd build/ios/iphoneos
|
||||
zip -r Runner.app.zip Runner.app
|
||||
|
||||
- uses: actions/upload-artifact@v5
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: builds-ios
|
||||
path: build/ios/iphoneos/Runner.app.zip
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
cd build/ios/archive
|
||||
zip -r Runner.xcarchive.zip Runner.xcarchive
|
||||
|
||||
- uses: actions/upload-artifact@v5
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: builds-ipa
|
||||
path: build/ios/archive/Runner.xcarchive.zip
|
||||
@@ -84,7 +84,7 @@ jobs:
|
||||
cd build/macos/Build/Products/Release
|
||||
zip -r wger.app.zip wger.app
|
||||
|
||||
- uses: actions/upload-artifact@v5
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: builds-macos
|
||||
path: build/macos/Build/Products/Release/wger.app.zip
|
||||
4
.github/workflows/build-linux.yml
vendored
4
.github/workflows/build-linux.yml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
sudo apt install -y pkg-config libgtk-3-dev liblzma-dev libstdc++-12-dev --no-install-recommends
|
||||
flutter build linux --release
|
||||
tar -zcvf linux-${{ matrix.platform }}.tar.gz build/linux/${{ matrix.platform }}/release/bundle
|
||||
- uses: actions/upload-artifact@v5
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: builds-linux
|
||||
path: |
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
|
||||
- name: Bump version and update manifest
|
||||
run: |
|
||||
git clone https://github.com/TheAppgineer/flatpak-flutter.git --branch 0.7.5 ../flatpak-flutter
|
||||
git clone https://github.com/TheAppgineer/flatpak-flutter.git --branch 0.11.0 ../flatpak-flutter
|
||||
pip install -r ../flatpak-flutter/requirements.txt
|
||||
python bump-wger-version.py ${{ inputs.ref }}
|
||||
../flatpak-flutter/flatpak-flutter.py --app-module wger flatpak-flutter.json
|
||||
|
||||
2
.github/workflows/build-windows.yml
vendored
2
.github/workflows/build-windows.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
- name: Build .exe
|
||||
run: flutter build windows --release
|
||||
|
||||
- uses: actions/upload-artifact@v5
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: builds-windows
|
||||
path: build\windows\x64\runner\Release\wger.exe
|
||||
6
.github/workflows/make-release.yml
vendored
6
.github/workflows/make-release.yml
vendored
@@ -67,7 +67,7 @@ jobs:
|
||||
ref: ${{ github.event.inputs.version }}
|
||||
|
||||
- name: Download builds
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: /tmp/
|
||||
|
||||
@@ -115,7 +115,7 @@ jobs:
|
||||
# uses: ./.github/actions/flutter-common
|
||||
#
|
||||
# - name: Download builds
|
||||
# uses: actions/download-artifact@v6
|
||||
# uses: actions/download-artifact@v7
|
||||
# with:
|
||||
# path: /tmp/
|
||||
#
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Download builds
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
|
||||
- name: Make Github release
|
||||
uses: softprops/action-gh-release@v2
|
||||
|
||||
6
.github/workflows/screenshots.yml
vendored
6
.github/workflows/screenshots.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
flutter drive --driver=test_driver/screenshot_driver.dart --target=integration_test/make_screenshots_test.dart --dart-define=DEVICE_TYPE=iOSPhoneBig -d "$SIMULATOR"
|
||||
|
||||
- name: Upload screenshots
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: screenshots-ios
|
||||
path: fastlane/metadata/ios/**/images/iPhone 6.9/*.png
|
||||
@@ -113,7 +113,7 @@ jobs:
|
||||
flutter drive --driver=test_driver/screenshot_driver.dart --target=integration_test/make_screenshots_test.dart --dart-define=DEVICE_TYPE=${{ matrix.device.device_type }}
|
||||
|
||||
- name: Upload screenshots
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: screenshots-android-${{ matrix.device.folder }}
|
||||
path: fastlane/metadata/android/**/images/${{ matrix.device.folder }}/*.png
|
||||
@@ -129,7 +129,7 @@ jobs:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Download all screenshot artifacts
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: screenshots
|
||||
|
||||
|
||||
27
Gemfile.lock
27
Gemfile.lock
@@ -8,8 +8,8 @@ GEM
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1190.0)
|
||||
aws-sdk-core (3.239.2)
|
||||
aws-partitions (1.1203.0)
|
||||
aws-sdk-core (3.241.3)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
@@ -17,18 +17,18 @@ GEM
|
||||
bigdecimal
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
logger
|
||||
aws-sdk-kms (1.118.0)
|
||||
aws-sdk-core (~> 3, >= 3.239.1)
|
||||
aws-sdk-kms (1.120.0)
|
||||
aws-sdk-core (~> 3, >= 3.241.3)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.206.0)
|
||||
aws-sdk-core (~> 3, >= 3.234.0)
|
||||
aws-sdk-s3 (1.211.0)
|
||||
aws-sdk-core (~> 3, >= 3.241.3)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.12.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
base64 (0.2.0)
|
||||
bigdecimal (3.3.1)
|
||||
bigdecimal (4.0.1)
|
||||
claide (1.1.0)
|
||||
colored (1.2)
|
||||
colored2 (3.1.2)
|
||||
@@ -61,7 +61,7 @@ GEM
|
||||
faraday-em_synchrony (1.0.1)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.1.1)
|
||||
faraday-multipart (1.2.0)
|
||||
multipart-post (~> 2.0)
|
||||
faraday-net_http (1.0.2)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
@@ -71,7 +71,7 @@ GEM
|
||||
faraday_middleware (1.2.1)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.4.0)
|
||||
fastlane (2.229.1)
|
||||
fastlane (2.230.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
abbrev (~> 0.1.2)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
@@ -100,6 +100,7 @@ GEM
|
||||
http-cookie (~> 1.0.5)
|
||||
json (< 3.0.0)
|
||||
jwt (>= 2.1.0, < 3)
|
||||
logger (>= 1.6, < 2.0)
|
||||
mini_magick (>= 4.9.4, < 5.0.0)
|
||||
multipart-post (>= 2.0.0, < 3.0.0)
|
||||
mutex_m (~> 0.3.0)
|
||||
@@ -164,22 +165,22 @@ GEM
|
||||
httpclient (2.9.0)
|
||||
mutex_m
|
||||
jmespath (1.6.2)
|
||||
json (2.17.1)
|
||||
json (2.18.0)
|
||||
jwt (2.10.2)
|
||||
base64
|
||||
logger (1.7.0)
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
multi_json (1.18.0)
|
||||
multi_json (1.19.1)
|
||||
multipart-post (2.4.1)
|
||||
mutex_m (0.3.0)
|
||||
nanaimo (0.4.0)
|
||||
naturally (2.3.0)
|
||||
nkf (0.2.0)
|
||||
optparse (0.8.0)
|
||||
optparse (0.8.1)
|
||||
os (1.1.4)
|
||||
plist (3.7.2)
|
||||
public_suffix (7.0.0)
|
||||
public_suffix (7.0.2)
|
||||
rake (13.3.1)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
|
||||
@@ -14,15 +14,16 @@ analyzer:
|
||||
# Allow self-reference to deprecated members (we do this because otherwise we have
|
||||
# to annotate every member in every test, assert, etc, when we deprecate something)
|
||||
deprecated_member_use_from_same_package: ignore
|
||||
# Ignore analyzer hints for updating pubspecs when using Future or
|
||||
# Stream and not importing dart:async
|
||||
# Please see https://github.com/flutter/flutter/pull/24528 for details.
|
||||
sdk_version_async_exported_from_core: ignore
|
||||
plugins:
|
||||
- riverpod_lint
|
||||
|
||||
formatter:
|
||||
page_width: 100
|
||||
trailing_commas: preserve
|
||||
|
||||
plugins:
|
||||
riverpod_lint: 3.1.0
|
||||
|
||||
linter:
|
||||
rules:
|
||||
# These rules are documented on and in the same order as
|
||||
|
||||
@@ -84,6 +84,24 @@
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<releases>
|
||||
<release version="1.9.5" date="2026-01-14">
|
||||
<description>
|
||||
<p>Bug fixes and improvements.</p>
|
||||
</description>
|
||||
<url>https://github.com/wger-project/flutter/releases/tag/1.9.5</url>
|
||||
</release>
|
||||
<release version="1.9.4" date="2025-12-23">
|
||||
<description>
|
||||
<p>Bug fixes and improvements.</p>
|
||||
</description>
|
||||
<url>https://github.com/wger-project/flutter/releases/tag/1.9.4</url>
|
||||
</release>
|
||||
<release version="1.9.3" date="2025-12-16">
|
||||
<description>
|
||||
<p>Bug fixes and improvements.</p>
|
||||
</description>
|
||||
<url>https://github.com/wger-project/flutter/releases/tag/1.9.3</url>
|
||||
</release>
|
||||
<release version="1.9.2" date="2025-12-04">
|
||||
<description>
|
||||
<p>Bug fixes and improvements.</p>
|
||||
|
||||
@@ -9,24 +9,28 @@ GEM
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.1018.0)
|
||||
aws-sdk-core (3.214.0)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1196.0)
|
||||
aws-sdk-core (3.240.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
base64
|
||||
bigdecimal
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.96.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
logger
|
||||
aws-sdk-kms (1.118.0)
|
||||
aws-sdk-core (~> 3, >= 3.239.1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.176.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sdk-s3 (1.208.0)
|
||||
aws-sdk-core (~> 3, >= 3.234.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.10.1)
|
||||
aws-sigv4 (1.12.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
base64 (0.2.0)
|
||||
bigdecimal (4.0.1)
|
||||
claide (1.1.0)
|
||||
colored (1.2)
|
||||
colored2 (3.1.2)
|
||||
@@ -157,6 +161,7 @@ GEM
|
||||
json (2.9.0)
|
||||
jwt (2.9.3)
|
||||
base64
|
||||
logger (1.7.0)
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
multi_json (1.15.0)
|
||||
|
||||
86
lib/core/exceptions/http_exception.dart
Normal file
86
lib/core/exceptions/http_exception.dart
Normal file
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (c) 2020 - 2026 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart';
|
||||
|
||||
enum ErrorType {
|
||||
json,
|
||||
html,
|
||||
text,
|
||||
}
|
||||
|
||||
const HTML_ERROR_KEY = 'html_error';
|
||||
|
||||
class WgerHttpException implements Exception {
|
||||
Map<String, dynamic> errors = {};
|
||||
|
||||
/// The exception type. While the majority will be json, it is possible that
|
||||
/// the server will return HTML, e.g. if there has been an internal server error
|
||||
/// or similar.
|
||||
late ErrorType type;
|
||||
|
||||
/// Custom http exception
|
||||
WgerHttpException(Response response) {
|
||||
type = ErrorType.json;
|
||||
final dynamic responseBody = response.body;
|
||||
|
||||
final contentType = response.headers[HttpHeaders.contentTypeHeader];
|
||||
if ((contentType != null && contentType.contains('text/html')) ||
|
||||
responseBody.toString().contains('<html')) {
|
||||
type = ErrorType.html;
|
||||
}
|
||||
|
||||
if (responseBody == null) {
|
||||
errors = {'unknown_error': 'An unknown error occurred, no further information available'};
|
||||
} else {
|
||||
try {
|
||||
if (type == ErrorType.json) {
|
||||
final response = json.decode(responseBody);
|
||||
errors = (response is Map ? response : {'unknown_error': response})
|
||||
.cast<String, dynamic>();
|
||||
} else if (type == ErrorType.html) {
|
||||
errors = {HTML_ERROR_KEY: responseBody.toString()};
|
||||
} else {
|
||||
errors = {'text_error': responseBody.toString()};
|
||||
}
|
||||
} catch (e) {
|
||||
errors = {'unknown_error': responseBody};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WgerHttpException.fromMap(Map<String, dynamic> map) : type = ErrorType.json {
|
||||
errors = map;
|
||||
}
|
||||
|
||||
String get htmlError {
|
||||
if (type != ErrorType.html) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return errors[HTML_ERROR_KEY] ?? '';
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'WgerHttpException ($type): $errors';
|
||||
}
|
||||
}
|
||||
@@ -429,7 +429,10 @@ typedef $$IngredientsTableProcessedTableManager =
|
||||
$$IngredientsTableAnnotationComposer,
|
||||
$$IngredientsTableCreateCompanionBuilder,
|
||||
$$IngredientsTableUpdateCompanionBuilder,
|
||||
(IngredientTable, BaseReferences<_$IngredientDatabase, $IngredientsTable, IngredientTable>),
|
||||
(
|
||||
IngredientTable,
|
||||
BaseReferences<_$IngredientDatabase, $IngredientsTable, IngredientTable>,
|
||||
),
|
||||
IngredientTable,
|
||||
PrefetchHooks Function()
|
||||
>;
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (C) 2020, 2021 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
class WgerHttpException implements Exception {
|
||||
Map<String, dynamic> errors = {};
|
||||
|
||||
/// Custom http exception.
|
||||
/// Expects the response body of the REST call and will try to parse it to
|
||||
/// JSON. Will use the response as-is if it fails.
|
||||
WgerHttpException(dynamic responseBody) {
|
||||
if (responseBody == null) {
|
||||
errors = {'unknown_error': 'An unknown error occurred, no further information available'};
|
||||
} else {
|
||||
try {
|
||||
final response = json.decode(responseBody);
|
||||
errors = (response is Map ? response : {'unknown_error': response}).cast<String, dynamic>();
|
||||
} catch (e) {
|
||||
errors = {'unknown_error': responseBody};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WgerHttpException.fromMap(Map<String, dynamic> map) {
|
||||
errors = map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return errors.values.toList().join(', ');
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (C) 2020, 2021 wger Team
|
||||
* Copyright (c) 2020 - 2026 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -22,11 +22,12 @@ import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_html/flutter_html.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:wger/exceptions/http_exception.dart';
|
||||
import 'package:wger/core/exceptions/http_exception.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/main.dart';
|
||||
import 'package:wger/models/workouts/log.dart';
|
||||
@@ -50,16 +51,21 @@ void showHttpExceptionErrorDialog(WgerHttpException exception, {BuildContext? co
|
||||
return;
|
||||
}
|
||||
|
||||
final errorList = formatApiErrors(extractErrors(exception.errors));
|
||||
|
||||
showDialog(
|
||||
context: dialogContext,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text(AppLocalizations.of(ctx).anErrorOccurred),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [...errorList],
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (exception.type == ErrorType.html)
|
||||
ServerHtmlError(data: exception.htmlError)
|
||||
else
|
||||
...formatApiErrors(extractErrors(exception.errors)),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
@@ -145,7 +151,10 @@ void showGeneralErrorDialog(dynamic error, StackTrace? stackTrace, {BuildContext
|
||||
tilePadding: EdgeInsets.zero,
|
||||
title: Text(i18n.errorViewDetails),
|
||||
children: [
|
||||
Text(issueErrorMessage, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
Text(
|
||||
issueErrorMessage,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Container(
|
||||
alignment: Alignment.topLeft,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
@@ -237,6 +246,31 @@ void showGeneralErrorDialog(dynamic error, StackTrace? stackTrace, {BuildContext
|
||||
);
|
||||
}
|
||||
|
||||
/// A widget to render HTML errors returned by the server
|
||||
///
|
||||
/// This is a simple wrapper around the `Html` Widget, with some light changes
|
||||
/// to the style.
|
||||
class ServerHtmlError extends StatelessWidget {
|
||||
final logger = Logger('ServerHtml');
|
||||
final String data;
|
||||
|
||||
ServerHtmlError({required this.data, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Html(
|
||||
data: data,
|
||||
style: {
|
||||
'h1': Style(fontSize: FontSize(theme.textTheme.bodyLarge?.fontSize ?? 15)),
|
||||
'h2': Style(fontSize: FontSize(theme.textTheme.bodyMedium?.fontSize ?? 15)),
|
||||
},
|
||||
doNotRenderTheseTags: const {'a'},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CopyToClipboardButton extends StatelessWidget {
|
||||
final logger = Logger('CopyToClipboardButton');
|
||||
final String text;
|
||||
@@ -423,38 +457,26 @@ class FormHttpErrorsWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Container(
|
||||
constraints: const BoxConstraints(maxHeight: 250),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: theme.colorScheme.error, width: 1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
padding: const EdgeInsets.all(10),
|
||||
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),
|
||||
Icon(Icons.error_outline, color: theme.colorScheme.error),
|
||||
if (exception.type == ErrorType.html)
|
||||
ServerHtmlError(data: exception.htmlError)
|
||||
else
|
||||
...formatApiErrors(
|
||||
extractErrors(exception.errors),
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -705,7 +705,7 @@
|
||||
"@whatVariationsExist": {},
|
||||
"previous": "Vorherige",
|
||||
"@previous": {},
|
||||
"next": "Nächste",
|
||||
"next": "Weiter",
|
||||
"@next": {},
|
||||
"swiss_ball": "Gymnastikball",
|
||||
"@swiss_ball": {},
|
||||
@@ -1127,5 +1127,65 @@
|
||||
"endWorkout": "Training beenden",
|
||||
"@endWorkout": {},
|
||||
"dayTypeCustom": "personalisierte",
|
||||
"@dayTypeCustom": {}
|
||||
"@dayTypeCustom": {},
|
||||
"dayTypeTabata": "Tabata",
|
||||
"@dayTypeTabata": {},
|
||||
"impressionGood": "Gut",
|
||||
"@impressionGood": {},
|
||||
"impressionNeutral": "Neutral",
|
||||
"@impressionNeutral": {},
|
||||
"impressionBad": "Schlecht",
|
||||
"@impressionBad": {},
|
||||
"gymModeShowExercises": "Übersichtsseiten der Übungen anzeigen",
|
||||
"@gymModeShowExercises": {},
|
||||
"gymModeShowTimer": "Timer zwischen Sätzen anzeigen",
|
||||
"@gymModeShowTimer": {},
|
||||
"gymModeTimerType": "Timer-Typ",
|
||||
"@gymModeTimerType": {},
|
||||
"gymModeTimerTypeHelText": "Wenn ein Satz eine Pausenzeit hat, wird immer ein Countdown genutzt.",
|
||||
"@gymModeTimerTypeHelText": {},
|
||||
"countdown": "Countdown",
|
||||
"@countdown": {},
|
||||
"stopwatch": "Stoppuhr",
|
||||
"@stopwatch": {},
|
||||
"gymModeDefaultCountdownTime": "Standard-Countdown-Zeit in Sekunden",
|
||||
"@gymModeDefaultCountdownTime": {},
|
||||
"gymModeNotifyOnCountdownFinish": "Benachrichtigung bei Ende des Countdowns",
|
||||
"@gymModeNotifyOnCountdownFinish": {},
|
||||
"duration": "Dauer",
|
||||
"@duration": {},
|
||||
"durationHoursMinutes": "{hours}h {minutes}m",
|
||||
"@durationHoursMinutes": {
|
||||
"description": "A duration, in hours and minutes",
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"type": "int"
|
||||
},
|
||||
"minutes": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"volume": "Volumen",
|
||||
"@volume": {
|
||||
"description": "The volume of a workout or set, i.e. weight x reps"
|
||||
},
|
||||
"workoutCompleted": "Training abgeschlossen",
|
||||
"@workoutCompleted": {},
|
||||
"formMinMaxValues": "Bitte geben Sie einen Wert zwischen {min} und {max}",
|
||||
"@formMinMaxValues": {
|
||||
"description": "Error message when the user needs to enter a value between min and max",
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"min": {
|
||||
"type": "int"
|
||||
},
|
||||
"max": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"superset": "Superset",
|
||||
"@superset": {}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"@weight": {
|
||||
"description": "The weight of a workout log or body weight entry"
|
||||
},
|
||||
"confirmDelete": "Êtes-vous sûre de vouloir supprimer « {toDelete} » ?",
|
||||
"confirmDelete": "Êtes-vous sûr de vouloir supprimer '{toDelete}' ?",
|
||||
"@confirmDelete": {
|
||||
"description": "Confirmation text before the user deletes an object",
|
||||
"type": "text",
|
||||
@@ -199,7 +199,7 @@
|
||||
"@reset": {
|
||||
"description": "Button text allowing the user to reset the entered values to the default"
|
||||
},
|
||||
"useCustomServer": "Utiliser le serveur personnalisé",
|
||||
"useCustomServer": "Utiliser un serveur personnalisé",
|
||||
"@useCustomServer": {
|
||||
"description": "Toggle button allowing users to switch between the default and a custom wger server"
|
||||
},
|
||||
@@ -364,7 +364,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"imageFormatNotSupportedDetail": "{imageFormat} non pris en charge",
|
||||
"imageFormatNotSupportedDetail": "{imageFormat} pas encore pris en charge.",
|
||||
"@imageFormatNotSupportedDetail": {
|
||||
"description": "Label shown on the image preview container when image format is not supported",
|
||||
"type": "text",
|
||||
@@ -1126,5 +1126,63 @@
|
||||
"enterTextInLanguage": "Veuillez saisir le texte dans la bonne langue !",
|
||||
"@enterTextInLanguage": {},
|
||||
"endWorkout": "Terminer l'entraînement",
|
||||
"@endWorkout": {}
|
||||
"@endWorkout": {},
|
||||
"impressionGood": "Bonne",
|
||||
"@impressionGood": {},
|
||||
"impressionNeutral": "Neutre",
|
||||
"@impressionNeutral": {},
|
||||
"impressionBad": "Mauvaise",
|
||||
"@impressionBad": {},
|
||||
"gymModeShowExercises": "Afficher les pages d'aperçu des exercices",
|
||||
"@gymModeShowExercises": {},
|
||||
"gymModeShowTimer": "Afficher le chronomètre entre les séries",
|
||||
"@gymModeShowTimer": {},
|
||||
"gymModeTimerType": "Type de chronomètre",
|
||||
"@gymModeTimerType": {},
|
||||
"gymModeTimerTypeHelText": "Si une série a un temps de pause, un compte à rebours est toujours utilisé.",
|
||||
"@gymModeTimerTypeHelText": {},
|
||||
"countdown": "Compte à rebours",
|
||||
"@countdown": {},
|
||||
"stopwatch": "Chronomètre",
|
||||
"@stopwatch": {},
|
||||
"gymModeDefaultCountdownTime": "Temps de compte à rebours par défaut, en secondes",
|
||||
"@gymModeDefaultCountdownTime": {},
|
||||
"gymModeNotifyOnCountdownFinish": "Notifier à la fin du compte à rebours",
|
||||
"@gymModeNotifyOnCountdownFinish": {},
|
||||
"duration": "Durée",
|
||||
"@duration": {},
|
||||
"durationHoursMinutes": "{hours}h {minutes}m",
|
||||
"@durationHoursMinutes": {
|
||||
"description": "A duration, in hours and minutes",
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"type": "int"
|
||||
},
|
||||
"minutes": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"volume": "Volume",
|
||||
"@volume": {
|
||||
"description": "The volume of a workout or set, i.e. weight x reps"
|
||||
},
|
||||
"workoutCompleted": "Entraînement terminé",
|
||||
"@workoutCompleted": {},
|
||||
"formMinMaxValues": "Veuillez entrer une valeur entre {min} et {max}",
|
||||
"@formMinMaxValues": {
|
||||
"description": "Error message when the user needs to enter a value between min and max",
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"min": {
|
||||
"type": "int"
|
||||
},
|
||||
"max": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"superset": "Superset",
|
||||
"@superset": {}
|
||||
}
|
||||
|
||||
1048
lib/l10n/app_nl.arb
1048
lib/l10n/app_nl.arb
File diff suppressed because it is too large
Load Diff
@@ -115,7 +115,7 @@
|
||||
"@close": {
|
||||
"description": "Translation for close"
|
||||
},
|
||||
"successfullyDeleted": "Excluído",
|
||||
"successfullyDeleted": "Removido com êxito",
|
||||
"@successfullyDeleted": {
|
||||
"description": "Message when an item was successfully deleted"
|
||||
},
|
||||
@@ -125,13 +125,13 @@
|
||||
"@goToToday": {
|
||||
"description": "Label on button to jump back to 'today' in the calendar widget"
|
||||
},
|
||||
"set": "Definir",
|
||||
"set": "Série",
|
||||
"@set": {
|
||||
"description": "A set in a workout plan"
|
||||
},
|
||||
"noMeasurementEntries": "Você não tem entradas de medição",
|
||||
"@noMeasurementEntries": {},
|
||||
"newSet": "Novo conjunto",
|
||||
"newSet": "Nova séries",
|
||||
"@newSet": {
|
||||
"description": "Header when adding a new set to a workout day"
|
||||
},
|
||||
@@ -189,7 +189,7 @@
|
||||
"@pause": {
|
||||
"description": "Noun, not an imperative! Label used for the pause when using the gym mode"
|
||||
},
|
||||
"success": "Sucesso",
|
||||
"success": "Bem-sucedido",
|
||||
"@success": {
|
||||
"description": "Message when an action completed successfully, usually used as a heading"
|
||||
},
|
||||
@@ -213,7 +213,7 @@
|
||||
"@newEntry": {
|
||||
"description": "Title when adding a new entry such as a weight or log entry"
|
||||
},
|
||||
"addSet": "Adicionar set",
|
||||
"addSet": "Adicionar séries",
|
||||
"@addSet": {
|
||||
"description": "Label for the button that adds a set (to a workout day)"
|
||||
},
|
||||
@@ -271,7 +271,7 @@
|
||||
"@loadingText": {
|
||||
"description": "Text to show when entries are being loaded in the background: Loading..."
|
||||
},
|
||||
"selectExercises": "Se quiser fazer um superset você pode procurar vários exercícios, eles estarão agrupados",
|
||||
"selectExercises": "Se quiser fazer um superséries você pode procurar vários exercícios, eles estarão agrupados",
|
||||
"@selectExercises": {},
|
||||
"nutritionalDiary": "Diário nutricional",
|
||||
"@nutritionalDiary": {},
|
||||
@@ -953,7 +953,7 @@
|
||||
"@noRoutines": {},
|
||||
"restTime": "Tempo de descanso",
|
||||
"@restTime": {},
|
||||
"sets": "Conjuntos",
|
||||
"sets": "Séries",
|
||||
"@sets": {
|
||||
"description": "The number of sets to be done for one exercise"
|
||||
},
|
||||
@@ -967,7 +967,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"supersetNr": "Superset {nr}",
|
||||
"supersetNr": "Supersérie {nr}",
|
||||
"@supersetNr": {
|
||||
"description": "Header in form indicating the number of the current exercise. Can also be translated as something like 'Superset Nr. xy'.",
|
||||
"type": "text",
|
||||
@@ -981,7 +981,7 @@
|
||||
"@restDay": {},
|
||||
"isRestDay": "É dia de descanso",
|
||||
"@isRestDay": {},
|
||||
"isRestDayHelp": "Por favor, note que todos os conjuntos e exercícios serão removidos quando marcar um dia como um dia de descanso.",
|
||||
"isRestDayHelp": "Por favor, note que todos as séries e exercícios serão removidos quando marcar um dia como um dia de descanso.",
|
||||
"@isRestDayHelp": {},
|
||||
"needsLogsToAdvance": "Precisa de logs para avançar",
|
||||
"@needsLogsToAdvance": {},
|
||||
@@ -1005,7 +1005,7 @@
|
||||
"@toAddMealsToThePlanGoToNutritionalPlanDetails": {
|
||||
"description": "Message shown to guide users to the nutritional plan details page to add meals"
|
||||
},
|
||||
"errorInfoDescription": "Algo de errado aconteceu. Você pode nos ajudar a concertar esse problema reportando o problema no Github",
|
||||
"errorInfoDescription": "Algo de errado aconteceu. Você pode nos ajudar a concertar esse problema reportando o problema no Github.",
|
||||
"@errorInfoDescription": {},
|
||||
"errorInfoDescription2": "Você pode continuar usando o applicativo, mas algumas funcionalidades não estarão disponíveis.",
|
||||
"@errorInfoDescription2": {},
|
||||
@@ -1021,7 +1021,7 @@
|
||||
"@min": {},
|
||||
"max": "Max",
|
||||
"@max": {},
|
||||
"aboutWhySupportTitle": "Open Source & free to use ❤️",
|
||||
"aboutWhySupportTitle": "Código aberto e de uso gratuito ❤️",
|
||||
"@aboutWhySupportTitle": {},
|
||||
"aboutContributeTitle": "Contribua",
|
||||
"@aboutContributeTitle": {},
|
||||
@@ -1043,13 +1043,13 @@
|
||||
"@fitInWeek": {},
|
||||
"fitInWeekHelp": "Se ligado, os dias vão se repetir semanalmente, caso contrário os dias seguirão sequencialmente se considerar o começo de uma nova semana.",
|
||||
"@fitInWeekHelp": {},
|
||||
"addSuperset": "Adicionar superset",
|
||||
"addSuperset": "Adicionar superséries",
|
||||
"@addSuperset": {},
|
||||
"setHasProgression": "Treino tem prograssão",
|
||||
"@setHasProgression": {},
|
||||
"setHasProgressionWarning": "Observe que, no momento, não é possível editar todas as configurações de um conjunto no aplicativo móvel nem configurar a progressão automática. Por enquanto, use o aplicativo web.",
|
||||
"setHasProgressionWarning": "Observe que, no momento, não é possível editar todas as configurações de um séries no aplicativo móvel nem configurar a progressão automática. Por enquanto, use o aplicativo web.",
|
||||
"@setHasProgressionWarning": {},
|
||||
"setHasNoExercises": "Este treino ainda não tem exercícios!",
|
||||
"setHasNoExercises": "Este séries ainda não tem exercícios!",
|
||||
"@setHasNoExercises": {},
|
||||
"simpleMode": "Modo simples",
|
||||
"@simpleMode": {},
|
||||
@@ -1057,7 +1057,7 @@
|
||||
"@simpleModeHelp": {},
|
||||
"progressionRules": "Este exercício tem regras de progressão e não pode ser editado no aplicativo móvel. Use o aplicativo web para editá-lo.",
|
||||
"@progressionRules": {},
|
||||
"resistance_band": "Resistance band",
|
||||
"resistance_band": "Banda de resistência",
|
||||
"@resistance_band": {
|
||||
"description": "Generated entry for translation for server strings"
|
||||
},
|
||||
@@ -1077,8 +1077,176 @@
|
||||
"@startDate": {},
|
||||
"dayTypeCustom": "Personalizado",
|
||||
"@dayTypeCustom": {},
|
||||
"dayTypeHiit": "Treino de alta intensidade",
|
||||
"dayTypeHiit": "Treinamento intervalado de alta intensidade",
|
||||
"@dayTypeHiit": {},
|
||||
"dayTypeTabata": "Tabata",
|
||||
"@dayTypeTabata": {}
|
||||
"dayTypeTabata": "Método Tabata",
|
||||
"@dayTypeTabata": {},
|
||||
"impressionGood": "Boa",
|
||||
"@impressionGood": {},
|
||||
"impressionNeutral": "Neutra",
|
||||
"@impressionNeutral": {},
|
||||
"impressionBad": "Ruim",
|
||||
"@impressionBad": {},
|
||||
"gymModeShowExercises": "Mostrar páginas de visão geral dos exercícios",
|
||||
"@gymModeShowExercises": {},
|
||||
"gymModeShowTimer": "Mostrar cronômetro entre séries",
|
||||
"@gymModeShowTimer": {},
|
||||
"gymModeTimerType": "Tipo de temporizador",
|
||||
"@gymModeTimerType": {},
|
||||
"gymModeTimerTypeHelText": "Se uma série tiver tempo de pausa, sempre será usada uma contagem regressiva.",
|
||||
"@gymModeTimerTypeHelText": {},
|
||||
"countdown": "Contagem regressiva",
|
||||
"@countdown": {},
|
||||
"stopwatch": "cronômetro",
|
||||
"@stopwatch": {},
|
||||
"gymModeDefaultCountdownTime": "Tempo de contagem regressiva padrão, em segundos",
|
||||
"@gymModeDefaultCountdownTime": {},
|
||||
"gymModeNotifyOnCountdownFinish": "Notificar no final da contagem regressiva",
|
||||
"@gymModeNotifyOnCountdownFinish": {},
|
||||
"duration": "Duração",
|
||||
"@duration": {},
|
||||
"durationHoursMinutes": "{hours}h {minutes}m",
|
||||
"@durationHoursMinutes": {
|
||||
"description": "A duration, in hours and minutes",
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"type": "int"
|
||||
},
|
||||
"minutes": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"volume": "Volume",
|
||||
"@volume": {
|
||||
"description": "The volume of a workout or set, i.e. weight x reps"
|
||||
},
|
||||
"workoutCompleted": "Treino concluído",
|
||||
"@workoutCompleted": {},
|
||||
"dayTypeEnom": "Cada minuto a minuto",
|
||||
"@dayTypeEnom": {},
|
||||
"dayTypeAmrap": "Tantas rodadas quanto possível",
|
||||
"@dayTypeAmrap": {},
|
||||
"dayTypeEdt": "Treinamento de densidade crescente",
|
||||
"@dayTypeEdt": {},
|
||||
"dayTypeRft": "Rodadas para ganhar tempo",
|
||||
"@dayTypeRft": {},
|
||||
"dayTypeAfap": "O mais rápido possível",
|
||||
"@dayTypeAfap": {},
|
||||
"slotEntryTypeNormal": "Normal",
|
||||
"@slotEntryTypeNormal": {},
|
||||
"slotEntryTypePartial": "Parcial",
|
||||
"@slotEntryTypePartial": {},
|
||||
"slotEntryTypeForced": "Forçado",
|
||||
"@slotEntryTypeForced": {},
|
||||
"slotEntryTypeTut": "Tempo Sob Tensão",
|
||||
"@slotEntryTypeTut": {},
|
||||
"slotEntryTypeIso": "Fixação isométrica",
|
||||
"@slotEntryTypeIso": {},
|
||||
"slotEntryTypeJump": "Pular",
|
||||
"@slotEntryTypeJump": {},
|
||||
"applicationLogs": "Registros de aplicativos",
|
||||
"@applicationLogs": {},
|
||||
"openEnded": "Aberto",
|
||||
"@openEnded": {
|
||||
"description": "When a nutrition plan has no pre-defined end date"
|
||||
},
|
||||
"overview": "visão global",
|
||||
"@overview": {},
|
||||
"formMinMaxValues": "Insira um valor entre {min} e {max}",
|
||||
"@formMinMaxValues": {
|
||||
"description": "Error message when the user needs to enter a value between min and max",
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"min": {
|
||||
"type": "int"
|
||||
},
|
||||
"max": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"identicalExercisePleaseDiscard": "Se você notar um exercício idêntico ao que você está adicionando, descarte o rascunho e edite o exercício.",
|
||||
"@identicalExercisePleaseDiscard": {},
|
||||
"checkInformationBeforeSubmitting": "Verifique se as informações inseridas estão corretas antes de enviar o exercício",
|
||||
"@checkInformationBeforeSubmitting": {},
|
||||
"imageDetailsTitle": "Detalhes da imagem",
|
||||
"@imageDetailsTitle": {
|
||||
"description": "Title for image details form"
|
||||
},
|
||||
"imageDetailsLicenseTitle": "Titulo",
|
||||
"@imageDetailsLicenseTitle": {
|
||||
"description": "Label for image title field"
|
||||
},
|
||||
"imageDetailsLicenseTitleHint": "Insira o título da imagem",
|
||||
"@imageDetailsLicenseTitleHint": {
|
||||
"description": "Hint text for image title field"
|
||||
},
|
||||
"imageDetailsSourceLink": "Link para o site de origem",
|
||||
"@imageDetailsSourceLink": {
|
||||
"description": "Label for source link field"
|
||||
},
|
||||
"author": "Autor(s)",
|
||||
"@author": {},
|
||||
"authorHint": "Digite o nome do autor",
|
||||
"@authorHint": {
|
||||
"description": "Hint text for author field"
|
||||
},
|
||||
"imageDetailsAuthorLink": "Link para o site ou perfil do autor",
|
||||
"@imageDetailsAuthorLink": {
|
||||
"description": "Label for author link field"
|
||||
},
|
||||
"imageDetailsDerivativeSource": "Link para a fonte original, se este for um trabalho derivado",
|
||||
"@imageDetailsDerivativeSource": {
|
||||
"description": "Label for derivative source field"
|
||||
},
|
||||
"imageDetailsDerivativeHelp": "Um trabalho derivado é baseado em um trabalho anterior, mas contém conteúdo novo e criativo suficiente para ter direito aos seus próprios direitos autorais.",
|
||||
"@imageDetailsDerivativeHelp": {
|
||||
"description": "Helper text explaining derivative works"
|
||||
},
|
||||
"imageDetailsImageType": "Tipo de imagem",
|
||||
"@imageDetailsImageType": {
|
||||
"description": "Label for image type selector"
|
||||
},
|
||||
"imageDetailsLicenseNotice": "Ao enviar esta imagem, você concorda em liberá-la sob CC-BY-SA-4. A imagem deve ser de sua autoria ou o autor deve tê-la divulgado sob uma licença compatível com ela.",
|
||||
"@imageDetailsLicenseNotice": {},
|
||||
"imageDetailsLicenseNoticeLinkToLicense": "Consulte o texto da licença.",
|
||||
"@imageDetailsLicenseNoticeLinkToLicense": {},
|
||||
"imageFormatNotSupported": "{imageFormat} não compatível",
|
||||
"@imageFormatNotSupported": {
|
||||
"description": "Label shown on the error container when image format is not supported",
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"imageFormat": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"imageFormatNotSupportedDetail": "Imagens {imageFormat} ainda não são suportadas.",
|
||||
"@imageFormatNotSupportedDetail": {
|
||||
"description": "Label shown on the image preview container when image format is not supported",
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"imageFormat": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"add": "adicionar",
|
||||
"@add": {
|
||||
"description": "Add button text"
|
||||
},
|
||||
"superset": "Supersérie",
|
||||
"@superset": {},
|
||||
"enterTextInLanguage": "Por favor, insira o texto no idioma correto!",
|
||||
"@enterTextInLanguage": {},
|
||||
"endWorkout": "Terminar treino",
|
||||
"@endWorkout": {
|
||||
"description": "Use the imperative, label on button to finish the current workout in gym mode"
|
||||
},
|
||||
"slotEntryTypeMyo": "Myo",
|
||||
"@slotEntryTypeMyo": {},
|
||||
"slotEntryTypeDropset": "Drop set",
|
||||
"@slotEntryTypeDropset": {}
|
||||
}
|
||||
|
||||
@@ -1162,5 +1162,63 @@
|
||||
"slotEntryTypeJump": "Стрибок",
|
||||
"@slotEntryTypeJump": {},
|
||||
"endWorkout": "Закінчити тренування",
|
||||
"@endWorkout": {}
|
||||
"@endWorkout": {},
|
||||
"impressionGood": "Добре",
|
||||
"@impressionGood": {},
|
||||
"impressionNeutral": "Нейтральний",
|
||||
"@impressionNeutral": {},
|
||||
"impressionBad": "Погано",
|
||||
"@impressionBad": {},
|
||||
"gymModeShowExercises": "Показати сторінки огляду вправ",
|
||||
"@gymModeShowExercises": {},
|
||||
"gymModeShowTimer": "Показувати таймер між сетами",
|
||||
"@gymModeShowTimer": {},
|
||||
"gymModeTimerType": "Тип таймера",
|
||||
"@gymModeTimerType": {},
|
||||
"gymModeTimerTypeHelText": "Якщо сет має час паузи, завжди використовується зворотний відлік.",
|
||||
"@gymModeTimerTypeHelText": {},
|
||||
"countdown": "Зворотний відлік",
|
||||
"@countdown": {},
|
||||
"stopwatch": "Секундомір",
|
||||
"@stopwatch": {},
|
||||
"gymModeDefaultCountdownTime": "Час зворотного відліку за замовчуванням, у секундах",
|
||||
"@gymModeDefaultCountdownTime": {},
|
||||
"gymModeNotifyOnCountdownFinish": "Повідомити про закінчення зворотного відліку",
|
||||
"@gymModeNotifyOnCountdownFinish": {},
|
||||
"duration": "Тривалість",
|
||||
"@duration": {},
|
||||
"durationHoursMinutes": "{hours}г {minutes}хв",
|
||||
"@durationHoursMinutes": {
|
||||
"description": "A duration, in hours and minutes",
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"type": "int"
|
||||
},
|
||||
"minutes": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"volume": "Обсяг",
|
||||
"@volume": {
|
||||
"description": "The volume of a workout or set, i.e. weight x reps"
|
||||
},
|
||||
"workoutCompleted": "Тренування завершено",
|
||||
"@workoutCompleted": {},
|
||||
"formMinMaxValues": "Будь ласка, введіть значення від {min} до {max}",
|
||||
"@formMinMaxValues": {
|
||||
"description": "Error message when the user needs to enter a value between min and max",
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"min": {
|
||||
"type": "int"
|
||||
},
|
||||
"max": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"superset": "Суперсет",
|
||||
"@superset": {}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart' as riverpod;
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/core/exceptions/http_exception.dart';
|
||||
import 'package:wger/core/locator.dart';
|
||||
import 'package:wger/exceptions/http_exception.dart';
|
||||
import 'package:wger/helpers/errors.dart';
|
||||
import 'package:wger/helpers/shared_preferences.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:wger/exceptions/no_such_entry_exception.dart';
|
||||
import 'package:wger/core/exceptions/no_such_entry_exception.dart';
|
||||
import 'package:wger/models/measurements/measurement_entry.dart';
|
||||
|
||||
part 'measurement_category.g.dart';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (C) 2020, 2021 wger Team
|
||||
* Copyright (c) 2020 - 2026 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -51,7 +51,7 @@ class Log {
|
||||
late int routineId;
|
||||
|
||||
@JsonKey(required: true, name: 'session')
|
||||
late int? sessionId;
|
||||
int? sessionId;
|
||||
|
||||
@JsonKey(required: true)
|
||||
int? iteration;
|
||||
@@ -72,22 +72,22 @@ class Log {
|
||||
num? repetitionsTarget;
|
||||
|
||||
@JsonKey(required: true, name: 'repetitions_unit')
|
||||
late int? repetitionsUnitId;
|
||||
int? repetitionsUnitId;
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
late RepetitionUnit? repetitionsUnitObj;
|
||||
RepetitionUnit? repetitionsUnitObj;
|
||||
|
||||
@JsonKey(required: true, fromJson: stringToNumNull, toJson: numToString)
|
||||
late num? weight;
|
||||
num? weight;
|
||||
|
||||
@JsonKey(required: true, fromJson: stringToNumNull, toJson: numToString, name: 'weight_target')
|
||||
num? weightTarget;
|
||||
|
||||
@JsonKey(required: true, name: 'weight_unit')
|
||||
late int? weightUnitId;
|
||||
int? weightUnitId;
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
late WeightUnit? weightUnitObj;
|
||||
WeightUnit? weightUnitObj;
|
||||
|
||||
@JsonKey(required: true, fromJson: utcIso8601ToLocalDate, toJson: dateToUtcIso8601)
|
||||
late DateTime date;
|
||||
@@ -101,41 +101,78 @@ class Log {
|
||||
this.repetitions,
|
||||
this.repetitionsTarget,
|
||||
this.repetitionsUnitId = REP_UNIT_REPETITIONS_ID,
|
||||
required this.rir,
|
||||
this.rir,
|
||||
this.rirTarget,
|
||||
this.weight,
|
||||
this.weightTarget,
|
||||
this.weightUnitId = WEIGHT_UNIT_KG,
|
||||
required this.date,
|
||||
});
|
||||
DateTime? date,
|
||||
}) : date = date ?? DateTime.now();
|
||||
|
||||
Log.empty();
|
||||
|
||||
Log.fromSetConfigData(SetConfigData data) {
|
||||
Log.fromSetConfigData(SetConfigData setConfig) {
|
||||
date = DateTime.now();
|
||||
sessionId = null;
|
||||
|
||||
slotEntryId = data.slotEntryId;
|
||||
exerciseBase = data.exercise;
|
||||
slotEntryId = setConfig.slotEntryId;
|
||||
exerciseBase = setConfig.exercise;
|
||||
|
||||
if (data.weight != null) {
|
||||
weight = data.weight;
|
||||
weightTarget = data.weight;
|
||||
}
|
||||
if (data.weightUnit != null) {
|
||||
weightUnit = data.weightUnit;
|
||||
weight = setConfig.weight;
|
||||
weightTarget = setConfig.weight;
|
||||
weightUnit = setConfig.weightUnit;
|
||||
|
||||
repetitions = setConfig.repetitions;
|
||||
repetitionsTarget = setConfig.repetitions;
|
||||
repetitionUnit = setConfig.repetitionsUnit;
|
||||
|
||||
rir = setConfig.rir;
|
||||
rirTarget = setConfig.rir;
|
||||
}
|
||||
|
||||
Log copyWith({
|
||||
int? id,
|
||||
int? exerciseId,
|
||||
int? routineId,
|
||||
int? sessionId,
|
||||
int? iteration,
|
||||
int? slotEntryId,
|
||||
num? rir,
|
||||
num? rirTarget,
|
||||
num? repetitions,
|
||||
num? repetitionsTarget,
|
||||
int? repetitionsUnitId,
|
||||
num? weight,
|
||||
num? weightTarget,
|
||||
int? weightUnitId,
|
||||
DateTime? date,
|
||||
}) {
|
||||
final out = Log(
|
||||
id: id ?? this.id,
|
||||
exerciseId: exerciseId ?? this.exerciseId,
|
||||
iteration: iteration ?? this.iteration,
|
||||
slotEntryId: slotEntryId ?? this.slotEntryId,
|
||||
routineId: routineId ?? this.routineId,
|
||||
repetitions: repetitions ?? this.repetitions,
|
||||
repetitionsTarget: repetitionsTarget ?? this.repetitionsTarget,
|
||||
repetitionsUnitId: repetitionsUnitId ?? this.repetitionsUnitId,
|
||||
rir: rir ?? this.rir,
|
||||
rirTarget: rirTarget ?? this.rirTarget,
|
||||
weight: weight ?? this.weight,
|
||||
weightTarget: weightTarget ?? this.weightTarget,
|
||||
weightUnitId: weightUnitId ?? this.weightUnitId,
|
||||
date: date ?? this.date,
|
||||
);
|
||||
|
||||
if (sessionId != null) {
|
||||
out.sessionId = sessionId;
|
||||
}
|
||||
|
||||
if (data.repetitions != null) {
|
||||
repetitions = data.repetitions;
|
||||
repetitionsTarget = data.repetitions;
|
||||
}
|
||||
if (data.repetitionsUnit != null) {
|
||||
repetitionUnit = data.repetitionsUnit;
|
||||
}
|
||||
out.exerciseBase = exercise;
|
||||
out.repetitionUnit = repetitionsUnitObj;
|
||||
out.weightUnitObj = weightUnitObj;
|
||||
|
||||
rir = data.rir;
|
||||
rirTarget = data.rir;
|
||||
return out;
|
||||
}
|
||||
|
||||
// Boilerplate
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (C) 2020, 2021 wger Team
|
||||
* Copyright (c) 2025 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -16,9 +16,11 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:wger/helpers/consts.dart';
|
||||
import 'package:wger/helpers/json.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/models/workouts/log.dart';
|
||||
@@ -62,14 +64,14 @@ class WorkoutSession {
|
||||
this.id,
|
||||
this.dayId,
|
||||
required this.routineId,
|
||||
this.impression = 2,
|
||||
this.impression = DEFAULT_IMPRESSION,
|
||||
this.notes = '',
|
||||
this.timeStart,
|
||||
this.timeEnd,
|
||||
this.logs = const <Log>[],
|
||||
DateTime? date,
|
||||
}) {
|
||||
this.date = date ?? DateTime.now();
|
||||
this.date = date ?? clock.now();
|
||||
}
|
||||
|
||||
Duration? get duration {
|
||||
|
||||
@@ -23,7 +23,9 @@ WorkoutSession _$WorkoutSessionFromJson(Map<String, dynamic> json) {
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
dayId: (json['day'] as num?)?.toInt(),
|
||||
routineId: (json['routine'] as num?)?.toInt(),
|
||||
impression: json['impression'] == null ? 2 : int.parse(json['impression'] as String),
|
||||
impression: json['impression'] == null
|
||||
? DEFAULT_IMPRESSION
|
||||
: int.parse(json['impression'] as String),
|
||||
notes: json['notes'] as String? ?? '',
|
||||
timeStart: stringToTimeNull(json['time_start'] as String?),
|
||||
timeEnd: stringToTimeNull(json['time_end'] as String?),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (C) 2020, 2021 wger Team
|
||||
* Copyright (c) 2025 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -36,8 +36,8 @@ class SlotData {
|
||||
late List<SetConfigData> setConfigs;
|
||||
|
||||
SlotData({
|
||||
required this.comment,
|
||||
required this.isSuperset,
|
||||
this.comment = '',
|
||||
this.isSuperset = false,
|
||||
this.exerciseIds = const [],
|
||||
this.setConfigs = const [],
|
||||
});
|
||||
|
||||
@@ -12,8 +12,8 @@ SlotData _$SlotDataFromJson(Map<String, dynamic> json) {
|
||||
requiredKeys: const ['comment', 'is_superset', 'exercises', 'sets'],
|
||||
);
|
||||
return SlotData(
|
||||
comment: json['comment'] as String,
|
||||
isSuperset: json['is_superset'] as bool,
|
||||
comment: json['comment'] as String? ?? '',
|
||||
isSuperset: json['is_superset'] as bool? ?? false,
|
||||
exerciseIds:
|
||||
(json['exercises'] as List<dynamic>?)?.map((e) => (e as num).toInt()).toList() ?? const [],
|
||||
setConfigs:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (C) 2020, 2021 wger Team
|
||||
* Copyright (c) 2020 - 2026 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -27,7 +27,7 @@ import 'package:http/http.dart' as http;
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:version/version.dart';
|
||||
import 'package:wger/exceptions/http_exception.dart';
|
||||
import 'package:wger/core/exceptions/http_exception.dart';
|
||||
import 'package:wger/helpers/consts.dart';
|
||||
import 'package:wger/helpers/shared_preferences.dart';
|
||||
|
||||
@@ -58,7 +58,7 @@ class AuthProvider with ChangeNotifier {
|
||||
static const SERVER_VERSION_URL = 'version';
|
||||
static const REGISTRATION_URL = 'register';
|
||||
static const LOGIN_URL = 'login';
|
||||
static const TEST_URL = 'userprofile';
|
||||
static const USERPROFILE_URL = 'userprofile';
|
||||
|
||||
late http.Client client;
|
||||
|
||||
@@ -132,7 +132,7 @@ class AuthProvider with ChangeNotifier {
|
||||
);
|
||||
|
||||
if (response.statusCode >= 400) {
|
||||
throw WgerHttpException(response.body);
|
||||
throw WgerHttpException(response);
|
||||
}
|
||||
|
||||
return login(username, password, serverUrl, null);
|
||||
@@ -150,7 +150,7 @@ class AuthProvider with ChangeNotifier {
|
||||
// Login using the API token
|
||||
if (apiToken != null && apiToken.isNotEmpty) {
|
||||
final response = await client.get(
|
||||
makeUri(serverUrl, TEST_URL),
|
||||
makeUri(serverUrl, USERPROFILE_URL),
|
||||
headers: {
|
||||
HttpHeaders.contentTypeHeader: 'application/json; charset=UTF-8',
|
||||
HttpHeaders.userAgentHeader: getAppNameHeader(),
|
||||
@@ -158,8 +158,8 @@ class AuthProvider with ChangeNotifier {
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw WgerHttpException(response.body);
|
||||
if (response.statusCode >= 400) {
|
||||
throw WgerHttpException(response);
|
||||
}
|
||||
|
||||
token = apiToken;
|
||||
@@ -169,17 +169,18 @@ class AuthProvider with ChangeNotifier {
|
||||
final response = await client.post(
|
||||
makeUri(serverUrl, LOGIN_URL),
|
||||
headers: {
|
||||
HttpHeaders.contentTypeHeader: 'application/json; charset=UTF-8',
|
||||
HttpHeaders.contentTypeHeader: 'application/json; charset=utf-8',
|
||||
HttpHeaders.userAgentHeader: getAppNameHeader(),
|
||||
},
|
||||
body: json.encode({'username': username, 'password': password}),
|
||||
);
|
||||
final responseData = json.decode(response.body);
|
||||
|
||||
if (response.statusCode >= 400) {
|
||||
throw WgerHttpException(response.body);
|
||||
throw WgerHttpException(response);
|
||||
}
|
||||
|
||||
final responseData = json.decode(response.body);
|
||||
|
||||
token = responseData['token'];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (C) 2020, 2021 wger Team
|
||||
* Copyright (c) 2026 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -16,19 +16,27 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/http.dart';
|
||||
import 'package:wger/exceptions/http_exception.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:wger/core/exceptions/http_exception.dart';
|
||||
import 'package:wger/providers/auth.dart';
|
||||
import 'package:wger/providers/helpers.dart';
|
||||
|
||||
/// initial delay for fetch retries, in milliseconds
|
||||
const FETCH_INITIAL_DELAY = 250;
|
||||
|
||||
/// Base provider class.
|
||||
///
|
||||
/// Provides a couple of comfort functions so we avoid a bit of boilerplate.
|
||||
class WgerBaseProvider {
|
||||
final _logger = Logger('WgerBaseProvider');
|
||||
|
||||
AuthProvider auth;
|
||||
late http.Client client;
|
||||
|
||||
@@ -56,21 +64,53 @@ class WgerBaseProvider {
|
||||
}
|
||||
|
||||
/// Fetch and retrieve the overview list of objects, returns the JSON parsed response
|
||||
Future<dynamic> fetch(Uri uri) async {
|
||||
// Future<Map<String, dynamic> | List<dynamic>> fetch(Uri uri) async {
|
||||
// Send the request
|
||||
final response = await client.get(
|
||||
uri,
|
||||
headers: getDefaultHeaders(includeAuth: true),
|
||||
);
|
||||
/// with a simple retry mechanism for transient errors.
|
||||
Future<dynamic> fetch(
|
||||
Uri uri, {
|
||||
int maxRetries = 3,
|
||||
Duration initialDelay = const Duration(milliseconds: 250),
|
||||
}) async {
|
||||
int attempt = 0;
|
||||
final random = math.Random();
|
||||
|
||||
// Something wrong with our request
|
||||
if (response.statusCode >= 400) {
|
||||
throw WgerHttpException(response.body);
|
||||
Future<void> wait(String reason) async {
|
||||
final backoff = (initialDelay.inMilliseconds * math.pow(2, attempt - 1)).toInt();
|
||||
final jitter = random.nextInt((backoff * 0.25).toInt() + 1); // up to 25% jitter
|
||||
final delay = backoff + jitter;
|
||||
_logger.info('Retrying fetch for $uri, attempt $attempt (${delay}ms), reason: $reason');
|
||||
|
||||
await Future.delayed(Duration(milliseconds: delay));
|
||||
}
|
||||
|
||||
// Process the response
|
||||
return json.decode(utf8.decode(response.bodyBytes)) as dynamic;
|
||||
while (true) {
|
||||
try {
|
||||
final response = await client
|
||||
.get(uri, headers: getDefaultHeaders(includeAuth: true))
|
||||
.timeout(const Duration(seconds: 5));
|
||||
|
||||
if (response.statusCode >= 400) {
|
||||
// Retry on server errors (5xx); e.g. 502 might be transient
|
||||
if (response.statusCode >= 500 && attempt < maxRetries) {
|
||||
attempt++;
|
||||
await wait('status code ${response.statusCode}');
|
||||
continue;
|
||||
}
|
||||
throw WgerHttpException(response);
|
||||
}
|
||||
|
||||
return json.decode(utf8.decode(response.bodyBytes)) as dynamic;
|
||||
} catch (e) {
|
||||
final isRetryable =
|
||||
e is SocketException || e is http.ClientException || e is TimeoutException;
|
||||
if (isRetryable && attempt < maxRetries) {
|
||||
attempt++;
|
||||
await wait(e.toString());
|
||||
continue;
|
||||
}
|
||||
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch and retrieve the overview list of objects, returns the JSON parsed response
|
||||
@@ -104,7 +144,7 @@ class WgerBaseProvider {
|
||||
|
||||
// Something wrong with our request
|
||||
if (response.statusCode >= 400) {
|
||||
throw WgerHttpException(response.body);
|
||||
throw WgerHttpException(response);
|
||||
}
|
||||
|
||||
return json.decode(response.body);
|
||||
@@ -120,7 +160,7 @@ class WgerBaseProvider {
|
||||
|
||||
// Something wrong with our request
|
||||
if (response.statusCode >= 400) {
|
||||
throw WgerHttpException(response.body);
|
||||
throw WgerHttpException(response);
|
||||
}
|
||||
|
||||
return json.decode(response.body);
|
||||
@@ -137,7 +177,7 @@ class WgerBaseProvider {
|
||||
|
||||
// Something wrong with our request
|
||||
if (response.statusCode >= 400) {
|
||||
throw WgerHttpException(response.body);
|
||||
throw WgerHttpException(response);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:wger/exceptions/http_exception.dart';
|
||||
import 'package:wger/core/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';
|
||||
@@ -115,7 +115,7 @@ class BodyWeightProvider with ChangeNotifier {
|
||||
if (response.statusCode >= 400) {
|
||||
_entries.insert(existingEntryIndex, existingWeightEntry);
|
||||
notifyListeners();
|
||||
throw WgerHttpException(response.body);
|
||||
throw WgerHttpException(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (C) 2020, 2021 wger Team
|
||||
* Copyright (c) 2020 - 2026 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -22,9 +22,9 @@ import 'dart:convert';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:wger/core/exceptions/no_such_entry_exception.dart';
|
||||
import 'package:wger/core/locator.dart';
|
||||
import 'package:wger/database/exercises/exercise_database.dart';
|
||||
import 'package:wger/exceptions/no_such_entry_exception.dart';
|
||||
import 'package:wger/helpers/consts.dart';
|
||||
import 'package:wger/helpers/shared_preferences.dart';
|
||||
import 'package:wger/models/exercises/category.dart';
|
||||
@@ -234,7 +234,12 @@ class ExercisesProvider with ChangeNotifier {
|
||||
|
||||
Future<void> fetchAndSetCategoriesFromApi() async {
|
||||
_logger.info('Loading exercise categories from API');
|
||||
final categories = await baseProvider.fetchPaginated(baseProvider.makeUrl(categoriesUrlPath));
|
||||
final categories = await baseProvider.fetchPaginated(
|
||||
baseProvider.makeUrl(
|
||||
categoriesUrlPath,
|
||||
query: {'limit': API_MAX_PAGE_SIZE},
|
||||
),
|
||||
);
|
||||
for (final category in categories) {
|
||||
_categories.add(ExerciseCategory.fromJson(category));
|
||||
}
|
||||
@@ -242,7 +247,12 @@ class ExercisesProvider with ChangeNotifier {
|
||||
|
||||
Future<void> fetchAndSetMusclesFromApi() async {
|
||||
_logger.info('Loading muscles from API');
|
||||
final muscles = await baseProvider.fetchPaginated(baseProvider.makeUrl(musclesUrlPath));
|
||||
final muscles = await baseProvider.fetchPaginated(
|
||||
baseProvider.makeUrl(
|
||||
musclesUrlPath,
|
||||
query: {'limit': API_MAX_PAGE_SIZE},
|
||||
),
|
||||
);
|
||||
|
||||
for (final muscle in muscles) {
|
||||
_muscles.add(Muscle.fromJson(muscle));
|
||||
@@ -251,7 +261,12 @@ class ExercisesProvider with ChangeNotifier {
|
||||
|
||||
Future<void> fetchAndSetEquipmentsFromApi() async {
|
||||
_logger.info('Loading equipment from API');
|
||||
final equipments = await baseProvider.fetchPaginated(baseProvider.makeUrl(equipmentUrlPath));
|
||||
final equipments = await baseProvider.fetchPaginated(
|
||||
baseProvider.makeUrl(
|
||||
equipmentUrlPath,
|
||||
query: {'limit': API_MAX_PAGE_SIZE},
|
||||
),
|
||||
);
|
||||
|
||||
for (final equipment in equipments) {
|
||||
_equipment.add(Equipment.fromJson(equipment));
|
||||
@@ -261,7 +276,12 @@ class ExercisesProvider with ChangeNotifier {
|
||||
Future<void> fetchAndSetLanguagesFromApi() async {
|
||||
_logger.info('Loading languages from API');
|
||||
|
||||
final languageData = await baseProvider.fetchPaginated(baseProvider.makeUrl(languageUrlPath));
|
||||
final languageData = await baseProvider.fetchPaginated(
|
||||
baseProvider.makeUrl(
|
||||
languageUrlPath,
|
||||
query: {'limit': API_MAX_PAGE_SIZE},
|
||||
),
|
||||
);
|
||||
|
||||
for (final language in languageData) {
|
||||
_languages.add(Language.fromJson(language));
|
||||
|
||||
46
lib/providers/gym_log_state.dart
Normal file
46
lib/providers/gym_log_state.dart
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (c) 2026 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:wger/models/workouts/log.dart';
|
||||
|
||||
part 'gym_log_state.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class GymLogNotifier extends _$GymLogNotifier {
|
||||
final _logger = Logger('GymLogNotifier');
|
||||
|
||||
@override
|
||||
Log? build() {
|
||||
_logger.finer('Initializing GymLogNotifier');
|
||||
return null;
|
||||
}
|
||||
|
||||
void setLog(Log newLog) {
|
||||
state = newLog;
|
||||
}
|
||||
|
||||
void setWeight(num weight) {
|
||||
state = state?.copyWith(weight: weight);
|
||||
}
|
||||
|
||||
void setRepetitions(num repetitions) {
|
||||
state = state?.copyWith(repetitions: repetitions);
|
||||
}
|
||||
}
|
||||
55
lib/providers/gym_log_state.g.dart
Normal file
55
lib/providers/gym_log_state.g.dart
Normal file
@@ -0,0 +1,55 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'gym_log_state.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(GymLogNotifier)
|
||||
final gymLogProvider = GymLogNotifierProvider._();
|
||||
|
||||
final class GymLogNotifierProvider extends $NotifierProvider<GymLogNotifier, Log?> {
|
||||
GymLogNotifierProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'gymLogProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$gymLogNotifierHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
GymLogNotifier create() => GymLogNotifier();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(Log? value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<Log?>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$gymLogNotifierHash() => r'4523975eeeaacceca4e86fb2e4ddd9a42c263d8e';
|
||||
|
||||
abstract class _$GymLogNotifier extends $Notifier<Log?> {
|
||||
Log? build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref = this.ref as $Ref<Log?, Log?>;
|
||||
final element =
|
||||
ref.element as $ClassProviderElement<AnyNotifier<Log?, Log?>, Log?, Object?, Object?>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (c) 2026 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -7,8 +25,10 @@ import 'package:wger/helpers/shared_preferences.dart';
|
||||
import 'package:wger/helpers/uuid.dart';
|
||||
import 'package:wger/models/exercises/exercise.dart';
|
||||
import 'package:wger/models/workouts/day_data.dart';
|
||||
import 'package:wger/models/workouts/log.dart';
|
||||
import 'package:wger/models/workouts/routine.dart';
|
||||
import 'package:wger/models/workouts/set_config_data.dart';
|
||||
import 'package:wger/providers/gym_log_state.dart';
|
||||
|
||||
part 'gym_state.g.dart';
|
||||
|
||||
@@ -113,7 +133,11 @@ class SlotPageEntry {
|
||||
this.setConfigData,
|
||||
this.logDone = false,
|
||||
String? uuid,
|
||||
}) : uuid = uuid ?? uuidV4();
|
||||
}) : assert(
|
||||
type != SlotPageType.log || setConfigData != null,
|
||||
'You need to set setConfigData for SlotPageType.log',
|
||||
),
|
||||
uuid = uuid ?? uuidV4();
|
||||
|
||||
SlotPageEntry copyWith({
|
||||
String? uuid,
|
||||
@@ -463,7 +487,7 @@ class GymStateNotifier extends _$GymStateNotifier {
|
||||
pages.add(PageEntry(type: PageType.workoutSummary, pageIndex: pageIndex + 1));
|
||||
|
||||
state = state.copyWith(pages: pages);
|
||||
// _logger.finer(readPageStructure());
|
||||
// print(readPageStructure());
|
||||
_logger.finer('Initialized ${state.pages.length} pages');
|
||||
}
|
||||
|
||||
@@ -555,6 +579,17 @@ class GymStateNotifier extends _$GymStateNotifier {
|
||||
|
||||
void setCurrentPage(int page) {
|
||||
state = state.copyWith(currentPage: page);
|
||||
|
||||
// Ensure that there is a log entry for the current slot entry
|
||||
final slotEntryPage = state.getSlotEntryPageByIndex();
|
||||
if (slotEntryPage == null || slotEntryPage.setConfigData == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final log = Log.fromSetConfigData(slotEntryPage.setConfigData!);
|
||||
log.routineId = state.routine.id!;
|
||||
log.iteration = state.iteration;
|
||||
ref.read(gymLogProvider.notifier).setLog(log);
|
||||
}
|
||||
|
||||
void setShowExercisePages(bool value) {
|
||||
|
||||
@@ -10,10 +10,10 @@ part of 'gym_state.dart';
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(GymStateNotifier)
|
||||
const gymStateProvider = GymStateNotifierProvider._();
|
||||
final gymStateProvider = GymStateNotifierProvider._();
|
||||
|
||||
final class GymStateNotifierProvider extends $NotifierProvider<GymStateNotifier, GymModeState> {
|
||||
const GymStateNotifierProvider._()
|
||||
GymStateNotifierProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
@@ -40,14 +40,13 @@ final class GymStateNotifierProvider extends $NotifierProvider<GymStateNotifier,
|
||||
}
|
||||
}
|
||||
|
||||
String _$gymStateNotifierHash() => r'449bd80d3b534f68af4f0dbb8556c7f093f3b918';
|
||||
String _$gymStateNotifierHash() => r'3a0bb78e9f7e682ba93a40a73b170126b5eb5ca9';
|
||||
|
||||
abstract class _$GymStateNotifier extends $Notifier<GymModeState> {
|
||||
GymModeState build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<GymModeState, GymModeState>;
|
||||
final element =
|
||||
ref.element
|
||||
@@ -57,6 +56,6 @@ abstract class _$GymStateNotifier extends $Notifier<GymModeState> {
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
|
||||
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/core/exceptions/http_exception.dart';
|
||||
import 'package:wger/core/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';
|
||||
|
||||
@@ -21,10 +21,10 @@ import 'dart:convert';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:wger/core/exceptions/http_exception.dart';
|
||||
import 'package:wger/core/exceptions/no_such_entry_exception.dart';
|
||||
import 'package:wger/core/locator.dart';
|
||||
import 'package:wger/database/ingredients/ingredients_database.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/nutrition/ingredient.dart';
|
||||
import 'package:wger/models/nutrition/ingredient_image.dart';
|
||||
@@ -220,7 +220,7 @@ class NutritionPlansProvider with ChangeNotifier {
|
||||
if (response.statusCode >= 400) {
|
||||
_plans.insert(existingPlanIndex, existingPlan);
|
||||
notifyListeners();
|
||||
throw WgerHttpException(response.body);
|
||||
throw WgerHttpException(response);
|
||||
}
|
||||
//existingPlan = null;
|
||||
}
|
||||
@@ -263,7 +263,7 @@ class NutritionPlansProvider with ChangeNotifier {
|
||||
if (response.statusCode >= 400) {
|
||||
plan.meals.insert(mealIndex, existingMeal);
|
||||
notifyListeners();
|
||||
throw WgerHttpException(response.body);
|
||||
throw WgerHttpException(response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,7 +293,7 @@ class NutritionPlansProvider with ChangeNotifier {
|
||||
if (response.statusCode >= 400) {
|
||||
meal.mealItems.insert(mealItemIndex, existingMealItem);
|
||||
notifyListeners();
|
||||
throw WgerHttpException(response.body);
|
||||
throw WgerHttpException(response);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (C) 2020, 2021 wger Team
|
||||
* Copyright (c) 2026 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -20,7 +20,7 @@ import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:wger/exceptions/http_exception.dart';
|
||||
import 'package:wger/core/exceptions/http_exception.dart';
|
||||
import 'package:wger/helpers/consts.dart';
|
||||
import 'package:wger/helpers/shared_preferences.dart';
|
||||
import 'package:wger/models/exercises/exercise.dart';
|
||||
@@ -374,7 +374,7 @@ class RoutinesProvider with ChangeNotifier {
|
||||
if (response.statusCode >= 400) {
|
||||
_routines.insert(routineIndex, routine);
|
||||
notifyListeners();
|
||||
throw WgerHttpException(response.body);
|
||||
throw WgerHttpException(response);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/core/exceptions/http_exception.dart';
|
||||
import 'package:wger/core/wide_screen_wrapper.dart';
|
||||
import 'package:wger/exceptions/http_exception.dart';
|
||||
import 'package:wger/helpers/consts.dart';
|
||||
import 'package:wger/helpers/errors.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/exceptions/http_exception.dart';
|
||||
import 'package:wger/core/exceptions/http_exception.dart';
|
||||
import 'package:wger/helpers/consts.dart';
|
||||
import 'package:wger/helpers/errors.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (C) 2020, 2021 wger Team
|
||||
* Copyright (c) 2025 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* wger Workout Manager is distributed in the hope that it will be useful,
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
@@ -18,8 +18,10 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/core/exceptions/no_such_entry_exception.dart';
|
||||
import 'package:wger/core/wide_screen_wrapper.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/models/measurements/measurement_category.dart';
|
||||
import 'package:wger/providers/measurement.dart';
|
||||
import 'package:wger/screens/form_screen.dart';
|
||||
import 'package:wger/widgets/measurements/entries.dart';
|
||||
@@ -38,7 +40,19 @@ class MeasurementEntriesScreen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final categoryId = ModalRoute.of(context)!.settings.arguments as int;
|
||||
final category = Provider.of<MeasurementProvider>(context).findCategoryById(categoryId);
|
||||
final provider = Provider.of<MeasurementProvider>(context);
|
||||
MeasurementCategory? category;
|
||||
|
||||
try {
|
||||
category = provider.findCategoryById(categoryId);
|
||||
} on NoSuchEntryException {
|
||||
Future.microtask(() {
|
||||
if (context.mounted && Navigator.of(context).canPop()) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
});
|
||||
return const SizedBox(); // Return empty widget until pop happens
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
@@ -65,7 +79,7 @@ class MeasurementEntriesScreen extends StatelessWidget {
|
||||
builder: (BuildContext contextDialog) {
|
||||
return AlertDialog(
|
||||
content: Text(
|
||||
AppLocalizations.of(context).confirmDelete(category.name),
|
||||
AppLocalizations.of(context).confirmDelete(category!.name),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
@@ -84,11 +98,12 @@ class MeasurementEntriesScreen extends StatelessWidget {
|
||||
Provider.of<MeasurementProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
).deleteCategory(category.id!);
|
||||
|
||||
).deleteCategory(category!.id!);
|
||||
// Close the popup
|
||||
Navigator.of(contextDialog).pop();
|
||||
|
||||
Navigator.of(context).pop(); // Exit detail screen
|
||||
|
||||
// and inform the user
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
@@ -138,7 +153,7 @@ class MeasurementEntriesScreen extends StatelessWidget {
|
||||
body: WidescreenWrapper(
|
||||
child: SingleChildScrollView(
|
||||
child: Consumer<MeasurementProvider>(
|
||||
builder: (context, provider, child) => EntriesList(category),
|
||||
builder: (context, provider, child) => EntriesList(category!),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (C) 2020, 2021 wger Team
|
||||
* Copyright (c) 2026 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* wger Workout Manager is distributed in the hope that it will be useful,
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
@@ -134,33 +134,29 @@ class _DashboardCalendarWidgetState extends State<DashboardCalendarWidget>
|
||||
|
||||
// Process workout sessions
|
||||
final routinesProvider = context.read<RoutinesProvider>();
|
||||
await routinesProvider.fetchSessionData().then((sessions) {
|
||||
for (final session in sessions) {
|
||||
final date = DateFormatLists.format(session.date);
|
||||
if (!_events.containsKey(date)) {
|
||||
_events[date] = [];
|
||||
}
|
||||
var time = '';
|
||||
time = '(${timeToString(session.timeStart)} - ${timeToString(session.timeEnd)})';
|
||||
|
||||
// Add events to lists
|
||||
_events[date]?.add(
|
||||
Event(
|
||||
EventType.session,
|
||||
'${i18n.impression}: ${session.impressionAsString(context)} $time',
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
final sessions = await routinesProvider.fetchSessionData();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (final session in sessions) {
|
||||
final date = DateFormatLists.format(session.date);
|
||||
_events.putIfAbsent(date, () => []);
|
||||
|
||||
final time = (session.timeStart != null && session.timeEnd != null)
|
||||
? '(${timeToString(session.timeStart)} - ${timeToString(session.timeEnd)})'
|
||||
: '';
|
||||
|
||||
_events[date]?.add(
|
||||
Event(
|
||||
EventType.session,
|
||||
'${i18n.impression}: ${session.impressionAsString(context)}${time.isNotEmpty ? ' $time' : ''}',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Process nutritional plans
|
||||
final NutritionPlansProvider nutritionProvider = Provider.of<NutritionPlansProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
final nutritionProvider = context.read<NutritionPlansProvider>();
|
||||
for (final plan in nutritionProvider.items) {
|
||||
for (final entry in plan.logEntriesValues.entries) {
|
||||
final date = DateFormatLists.format(entry.key);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (C) 2020, 2021 wger Team
|
||||
* Copyright (c) 2020 - 2025 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* wger Workout Manager is distributed in the hope that it will be useful,
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
@@ -20,6 +20,7 @@ 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/core/exceptions/http_exception.dart';
|
||||
import 'package:wger/helpers/errors.dart';
|
||||
import 'package:wger/models/exercises/video.dart';
|
||||
|
||||
@@ -33,9 +34,10 @@ class ExerciseVideoWidget extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ExerciseVideoWidgetState extends State<ExerciseVideoWidget> {
|
||||
final logger = Logger('ExerciseVideoWidgetState');
|
||||
|
||||
late VideoPlayerController _controller;
|
||||
bool hasError = false;
|
||||
final logger = Logger('ExerciseVideoWidgetState');
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -66,10 +68,14 @@ class _ExerciseVideoWidgetState extends State<ExerciseVideoWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return hasError
|
||||
? const GeneralErrorsWidget(
|
||||
[
|
||||
'An error happened while loading the video. If you can, please check the application logs.',
|
||||
],
|
||||
? FormHttpErrorsWidget(
|
||||
WgerHttpException.fromMap(
|
||||
const {
|
||||
'error':
|
||||
'An error happened while loading the video. If you can, '
|
||||
'please check the application logs.',
|
||||
},
|
||||
),
|
||||
)
|
||||
: _controller.value.isInitialized
|
||||
? AspectRatio(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/exceptions/http_exception.dart';
|
||||
import 'package:wger/core/exceptions/http_exception.dart';
|
||||
import 'package:wger/helpers/consts.dart';
|
||||
import 'package:wger/helpers/errors.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (C) 2020, 2021 wger Team
|
||||
* Copyright (c) 2020 - 2025 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* wger Workout Manager is distributed in the hope that it will be useful,
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
@@ -29,7 +29,7 @@ class RepetitionUnitInputWidget extends StatefulWidget {
|
||||
late int? selectedRepetitionUnit;
|
||||
final ValueChanged<int?> onChanged;
|
||||
|
||||
RepetitionUnitInputWidget(initialValue, {required this.onChanged}) {
|
||||
RepetitionUnitInputWidget(int? initialValue, {super.key, required this.onChanged}) {
|
||||
selectedRepetitionUnit = initialValue;
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ class _RepetitionUnitInputWidgetState extends State<RepetitionUnitInputWidget> {
|
||||
: null;
|
||||
|
||||
return DropdownButtonFormField(
|
||||
value: selectedWeightUnit,
|
||||
initialValue: selectedWeightUnit,
|
||||
decoration: InputDecoration(
|
||||
labelText: AppLocalizations.of(context).repetitionUnit,
|
||||
),
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (C) 2020, 2021 wger Team
|
||||
* Copyright (c) 2020 - 2025 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* wger Workout Manager is distributed in the hope that it will be useful,
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
@@ -30,7 +30,7 @@ class RiRInputWidget extends StatefulWidget {
|
||||
|
||||
static const SLIDER_START = -0.5;
|
||||
|
||||
RiRInputWidget(this._initialValue, {required this.onChanged}) {
|
||||
RiRInputWidget(this._initialValue, {super.key, required this.onChanged}) {
|
||||
_logger.finer('Initializing with initial value: $_initialValue');
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/exceptions/http_exception.dart';
|
||||
import 'package:wger/core/exceptions/http_exception.dart';
|
||||
import 'package:wger/helpers/consts.dart';
|
||||
import 'package:wger/helpers/errors.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (C) 2020, 2021 wger Team
|
||||
* Copyright (c) 2020 - 2025 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* wger Workout Manager is distributed in the hope that it will be useful,
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
@@ -20,7 +20,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/core/exceptions/http_exception.dart';
|
||||
import 'package:wger/helpers/consts.dart';
|
||||
import 'package:wger/helpers/errors.dart';
|
||||
import 'package:wger/helpers/json.dart';
|
||||
@@ -100,6 +100,7 @@ class _SessionFormState extends State<SessionForm> {
|
||||
children: [
|
||||
errorMessage,
|
||||
ToggleButtons(
|
||||
key: const ValueKey('impression-toggle-buttons'),
|
||||
renderBorder: false,
|
||||
onPressed: (int index) {
|
||||
setState(() {
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/exceptions/http_exception.dart';
|
||||
import 'package:wger/core/exceptions/http_exception.dart';
|
||||
import 'package:wger/helpers/errors.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/models/workouts/day.dart';
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/exceptions/http_exception.dart';
|
||||
import 'package:wger/core/exceptions/http_exception.dart';
|
||||
import 'package:wger/helpers/consts.dart';
|
||||
import 'package:wger/helpers/errors.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (C) 2020, 2021 wger Team
|
||||
* Copyright (c) 2020 - 2025 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* wger Workout Manager is distributed in the hope that it will be useful,
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
@@ -29,7 +29,7 @@ class WeightUnitInputWidget extends StatefulWidget {
|
||||
late int? selectedWeightUnit;
|
||||
final ValueChanged<int?> onChanged;
|
||||
|
||||
WeightUnitInputWidget(int? initialValue, {required this.onChanged}) {
|
||||
WeightUnitInputWidget(int? initialValue, {super.key, required this.onChanged}) {
|
||||
selectedWeightUnit = initialValue;
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ class _WeightUnitInputWidgetState extends State<WeightUnitInputWidget> {
|
||||
: null;
|
||||
|
||||
return DropdownButtonFormField(
|
||||
value: selectedWeightUnit,
|
||||
initialValue: selectedWeightUnit,
|
||||
decoration: InputDecoration(labelText: AppLocalizations.of(context).weightUnit),
|
||||
onChanged: (WeightUnit? newValue) {
|
||||
setState(() {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (C) 2020, 2021 wger Team
|
||||
* Copyright (c) 2020 - 2026 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* wger Workout Manager is distributed in the hope that it will be useful,
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
@@ -15,17 +15,19 @@
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:provider/provider.dart' as provider;
|
||||
import 'package:wger/exceptions/http_exception.dart';
|
||||
import 'package:wger/core/exceptions/http_exception.dart';
|
||||
import 'package:wger/helpers/consts.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/models/workouts/log.dart';
|
||||
import 'package:wger/models/workouts/set_config_data.dart';
|
||||
import 'package:wger/models/workouts/slot_entry.dart';
|
||||
import 'package:wger/providers/gym_log_state.dart';
|
||||
import 'package:wger/providers/gym_state.dart';
|
||||
import 'package:wger/providers/plate_weights.dart';
|
||||
import 'package:wger/providers/routines.dart';
|
||||
@@ -38,60 +40,37 @@ import 'package:wger/widgets/routines/forms/weight_unit.dart';
|
||||
import 'package:wger/widgets/routines/gym_mode/navigation.dart';
|
||||
import 'package:wger/widgets/routines/plate_calculator.dart';
|
||||
|
||||
class LogPage extends ConsumerStatefulWidget {
|
||||
class LogPage extends ConsumerWidget {
|
||||
final _logger = Logger('LogPage');
|
||||
|
||||
final PageController _controller;
|
||||
|
||||
LogPage(this._controller);
|
||||
|
||||
@override
|
||||
_LogPageState createState() => _LogPageState();
|
||||
}
|
||||
|
||||
class _LogPageState extends ConsumerState<LogPage> {
|
||||
final GlobalKey<_LogFormWidgetState> _logFormKey = GlobalKey<_LogFormWidgetState>();
|
||||
|
||||
late FocusNode focusNode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
focusNode = FocusNode();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
final state = ref.watch(gymStateProvider);
|
||||
final gymState = ref.watch(gymStateProvider);
|
||||
final languageCode = Localizations.localeOf(context).languageCode;
|
||||
|
||||
final page = state.getPageByIndex();
|
||||
final page = gymState.getPageByIndex();
|
||||
if (page == null) {
|
||||
widget._logger.info(
|
||||
'getPageByIndex for ${state.currentPage} returned null, showing empty container.',
|
||||
_logger.info(
|
||||
'getPageByIndex for ${gymState.currentPage} returned null, showing empty container.',
|
||||
);
|
||||
return Container();
|
||||
}
|
||||
|
||||
final slotEntryPage = state.getSlotEntryPageByIndex();
|
||||
final slotEntryPage = gymState.getSlotEntryPageByIndex();
|
||||
if (slotEntryPage == null) {
|
||||
widget._logger.info(
|
||||
'getSlotPageByIndex for ${state.currentPage} returned null, showing empty container',
|
||||
_logger.info(
|
||||
'getSlotPageByIndex for ${gymState.currentPage} returned null, showing empty container',
|
||||
);
|
||||
return Container();
|
||||
}
|
||||
|
||||
final setConfigData = slotEntryPage.setConfigData!;
|
||||
|
||||
final log = Log.fromSetConfigData(setConfigData)
|
||||
..routineId = state.routine.id!
|
||||
..iteration = state.iteration;
|
||||
final log = ref.watch(gymLogProvider);
|
||||
|
||||
// Mark done sets
|
||||
final decorationStyle = slotEntryPage.logDone
|
||||
@@ -101,8 +80,9 @@ class _LogPageState extends ConsumerState<LogPage> {
|
||||
return Column(
|
||||
children: [
|
||||
NavigationHeader(
|
||||
log.exercise.getTranslation(Localizations.localeOf(context).languageCode).name,
|
||||
widget._controller,
|
||||
log!.exercise.getTranslation(languageCode).name,
|
||||
_controller,
|
||||
key: const ValueKey('log-page-navigation-header'),
|
||||
),
|
||||
|
||||
Container(
|
||||
@@ -148,16 +128,9 @@ class _LogPageState extends ConsumerState<LogPage> {
|
||||
Text(slotEntryPage.setConfigData!.comment, textAlign: TextAlign.center),
|
||||
const SizedBox(height: 10),
|
||||
Expanded(
|
||||
child: (state.routine.filterLogsByExercise(log.exercise.id!).isNotEmpty)
|
||||
child: (gymState.routine.filterLogsByExercise(log.exerciseId).isNotEmpty)
|
||||
? LogsPastLogsWidget(
|
||||
log: log,
|
||||
pastLogs: state.routine.filterLogsByExercise(log.exercise.id!),
|
||||
onCopy: (pastLog) {
|
||||
_logFormKey.currentState?.copyFromPastLog(pastLog);
|
||||
},
|
||||
setStateCallback: (fn) {
|
||||
setState(fn);
|
||||
},
|
||||
pastLogs: gymState.routine.filterLogsByExercise(log.exerciseId),
|
||||
)
|
||||
: Container(),
|
||||
),
|
||||
@@ -170,16 +143,15 @@ class _LogPageState extends ConsumerState<LogPage> {
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 5),
|
||||
child: LogFormWidget(
|
||||
key: _logFormKey,
|
||||
controller: widget._controller,
|
||||
controller: _controller,
|
||||
configData: setConfigData,
|
||||
log: log,
|
||||
focusNode: focusNode,
|
||||
// log: log!,
|
||||
key: _logFormKey,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
NavigationFooter(widget._controller),
|
||||
NavigationFooter(_controller),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -239,68 +211,62 @@ class LogsPlatesWidget extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class LogsRepsWidget extends StatelessWidget {
|
||||
final TextEditingController controller;
|
||||
final SetConfigData configData;
|
||||
final FocusNode focusNode;
|
||||
final Log log;
|
||||
final void Function(VoidCallback fn) setStateCallback;
|
||||
|
||||
class LogsRepsWidget extends ConsumerWidget {
|
||||
final _logger = Logger('LogsRepsWidget');
|
||||
|
||||
final num valueChange;
|
||||
|
||||
LogsRepsWidget({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.configData,
|
||||
required this.focusNode,
|
||||
required this.log,
|
||||
required this.setStateCallback,
|
||||
});
|
||||
num? valueChange,
|
||||
}) : valueChange = valueChange ?? 1;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final repsValueChange = configData.repetitionsRounding ?? 1;
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final numberFormat = NumberFormat.decimalPattern(Localizations.localeOf(context).toString());
|
||||
|
||||
final i18n = AppLocalizations.of(context);
|
||||
|
||||
final logNotifier = ref.read(gymLogProvider.notifier);
|
||||
final log = ref.watch(gymLogProvider);
|
||||
|
||||
final currentReps = log?.repetitions;
|
||||
final repText = currentReps != null ? numberFormat.format(currentReps) : '';
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
// "Quick-remove" button
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove, color: Colors.black),
|
||||
onPressed: () {
|
||||
final currentValue = numberFormat.tryParse(controller.text) ?? 0;
|
||||
final newValue = currentValue - repsValueChange;
|
||||
if (newValue >= 0) {
|
||||
setStateCallback(() {
|
||||
log.repetitions = newValue;
|
||||
controller.text = numberFormat.format(newValue);
|
||||
});
|
||||
final base = currentReps ?? 0;
|
||||
final newValue = base - valueChange;
|
||||
if (newValue >= 0 && log != null) {
|
||||
logNotifier.setRepetitions(newValue);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
// Text field
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
decoration: InputDecoration(labelText: i18n.repetitions),
|
||||
enabled: true,
|
||||
controller: controller,
|
||||
key: ValueKey('reps-field-$repText'),
|
||||
initialValue: repText,
|
||||
keyboardType: textInputTypeDecimal,
|
||||
focusNode: focusNode,
|
||||
onChanged: (value) {
|
||||
try {
|
||||
final newValue = numberFormat.parse(value);
|
||||
setStateCallback(() {
|
||||
log.repetitions = newValue;
|
||||
});
|
||||
logNotifier.setRepetitions(newValue);
|
||||
} on FormatException catch (error) {
|
||||
_logger.fine('Error parsing repetitions: $error');
|
||||
_logger.finer('Error parsing repetitions: $error');
|
||||
}
|
||||
},
|
||||
onSaved: (newValue) {
|
||||
_logger.info('Saving new reps value: $newValue');
|
||||
setStateCallback(() {
|
||||
log.repetitions = numberFormat.parse(newValue!);
|
||||
});
|
||||
if (newValue == null || log == null) {
|
||||
return;
|
||||
}
|
||||
logNotifier.setRepetitions(numberFormat.parse(newValue));
|
||||
},
|
||||
validator: (value) {
|
||||
if (numberFormat.tryParse(value ?? '') == null) {
|
||||
@@ -310,19 +276,15 @@ class LogsRepsWidget extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// "Quick-add" button
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add, color: Colors.black),
|
||||
onPressed: () {
|
||||
final value = controller.text.isNotEmpty ? controller.text : '0';
|
||||
|
||||
try {
|
||||
final newValue = numberFormat.parse(value) + repsValueChange;
|
||||
setStateCallback(() {
|
||||
log.repetitions = newValue;
|
||||
controller.text = numberFormat.format(newValue);
|
||||
});
|
||||
} on FormatException catch (error) {
|
||||
_logger.fine('Error parsing reps during quick-add: $error');
|
||||
final base = currentReps ?? 0;
|
||||
final newValue = base + valueChange;
|
||||
if (newValue >= 0 && log != null) {
|
||||
logNotifier.setRepetitions(newValue);
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -332,76 +294,62 @@ class LogsRepsWidget extends StatelessWidget {
|
||||
}
|
||||
|
||||
class LogsWeightWidget extends ConsumerWidget {
|
||||
final TextEditingController controller;
|
||||
final SetConfigData configData;
|
||||
final FocusNode focusNode;
|
||||
final Log log;
|
||||
final void Function(VoidCallback fn) setStateCallback;
|
||||
|
||||
final _logger = Logger('LogsWeightWidget');
|
||||
|
||||
final num valueChange;
|
||||
|
||||
LogsWeightWidget({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.configData,
|
||||
required this.focusNode,
|
||||
required this.log,
|
||||
required this.setStateCallback,
|
||||
});
|
||||
num? valueChange,
|
||||
}) : valueChange = valueChange ?? 1.25;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final weightValueChange = configData.weightRounding ?? 1.25;
|
||||
final numberFormat = NumberFormat.decimalPattern(Localizations.localeOf(context).toString());
|
||||
final i18n = AppLocalizations.of(context);
|
||||
|
||||
final plateProvider = ref.read(plateCalculatorProvider.notifier);
|
||||
final logProvider = ref.read(gymLogProvider.notifier);
|
||||
final log = ref.watch(gymLogProvider);
|
||||
|
||||
final currentWeight = log?.weight;
|
||||
final weightText = currentWeight != null ? numberFormat.format(currentWeight) : '';
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
IconButton(
|
||||
// "Quick-remove" button
|
||||
icon: const Icon(Icons.remove, color: Colors.black),
|
||||
onPressed: () {
|
||||
try {
|
||||
final newValue = numberFormat.parse(controller.text) - weightValueChange;
|
||||
if (newValue > 0) {
|
||||
setStateCallback(() {
|
||||
log.weight = newValue;
|
||||
controller.text = numberFormat.format(newValue);
|
||||
ref
|
||||
.read(plateCalculatorProvider.notifier)
|
||||
.setWeight(
|
||||
controller.text == '' ? 0 : newValue,
|
||||
);
|
||||
});
|
||||
}
|
||||
} on FormatException catch (error) {
|
||||
_logger.fine('Error parsing weight during quick-remove: $error');
|
||||
final base = currentWeight ?? 0;
|
||||
final newValue = base - valueChange;
|
||||
if (newValue >= 0 && log != null) {
|
||||
logProvider.setWeight(newValue);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
// Text field
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
key: ValueKey('weight-field-$weightText'),
|
||||
decoration: InputDecoration(labelText: i18n.weight),
|
||||
controller: controller,
|
||||
initialValue: weightText,
|
||||
keyboardType: textInputTypeDecimal,
|
||||
onChanged: (value) {
|
||||
try {
|
||||
final newValue = numberFormat.parse(value);
|
||||
setStateCallback(() {
|
||||
log.weight = newValue;
|
||||
ref
|
||||
.read(plateCalculatorProvider.notifier)
|
||||
.setWeight(
|
||||
controller.text == '' ? 0 : numberFormat.parse(controller.text),
|
||||
);
|
||||
});
|
||||
plateProvider.setWeight(newValue);
|
||||
logProvider.setWeight(newValue);
|
||||
} on FormatException catch (error) {
|
||||
_logger.fine('Error parsing weight: $error');
|
||||
_logger.finer('Error parsing weight: $error');
|
||||
}
|
||||
},
|
||||
onSaved: (newValue) {
|
||||
setStateCallback(() {
|
||||
log.weight = numberFormat.parse(newValue!);
|
||||
});
|
||||
if (newValue == null || log == null) {
|
||||
return;
|
||||
}
|
||||
logProvider.setWeight(numberFormat.parse(newValue));
|
||||
},
|
||||
validator: (value) {
|
||||
if (numberFormat.tryParse(value ?? '') == null) {
|
||||
@@ -411,24 +359,15 @@ class LogsWeightWidget extends ConsumerWidget {
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// "Quick-add" button
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add, color: Colors.black),
|
||||
onPressed: () {
|
||||
final value = controller.text.isNotEmpty ? controller.text : '0';
|
||||
|
||||
try {
|
||||
final newValue = numberFormat.parse(value) + weightValueChange;
|
||||
setStateCallback(() {
|
||||
log.weight = newValue;
|
||||
controller.text = numberFormat.format(newValue);
|
||||
ref
|
||||
.read(plateCalculatorProvider.notifier)
|
||||
.setWeight(
|
||||
controller.text == '' ? 0 : newValue,
|
||||
);
|
||||
});
|
||||
} on FormatException catch (error) {
|
||||
_logger.fine('Error parsing weight during quick-add: $error');
|
||||
final base = currentWeight ?? 0;
|
||||
final newValue = base + valueChange;
|
||||
if (log != null) {
|
||||
logProvider.setWeight(newValue);
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -437,22 +376,19 @@ class LogsWeightWidget extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class LogsPastLogsWidget extends StatelessWidget {
|
||||
final Log log;
|
||||
class LogsPastLogsWidget extends ConsumerWidget {
|
||||
final List<Log> pastLogs;
|
||||
final void Function(Log pastLog) onCopy;
|
||||
final void Function(VoidCallback fn) setStateCallback;
|
||||
|
||||
const LogsPastLogsWidget({
|
||||
super.key,
|
||||
required this.log,
|
||||
required this.pastLogs,
|
||||
required this.onCopy,
|
||||
required this.setStateCallback,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final logProvider = ref.read(gymLogProvider.notifier);
|
||||
final dateFormat = DateFormat.yMd(Localizations.localeOf(context).languageCode);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: ListView(
|
||||
@@ -466,25 +402,16 @@ class LogsPastLogsWidget extends StatelessWidget {
|
||||
return ListTile(
|
||||
key: ValueKey('past-log-${pastLog.id}'),
|
||||
title: Text(pastLog.repTextNoNl(context)),
|
||||
subtitle: Text(
|
||||
DateFormat.yMd(Localizations.localeOf(context).languageCode).format(pastLog.date),
|
||||
),
|
||||
subtitle: Text(dateFormat.format(pastLog.date)),
|
||||
trailing: const Icon(Icons.copy),
|
||||
onTap: () {
|
||||
setStateCallback(() {
|
||||
log.rir = pastLog.rir;
|
||||
log.repetitionUnit = pastLog.repetitionsUnitObj;
|
||||
log.weightUnit = pastLog.weightUnitObj;
|
||||
|
||||
onCopy(pastLog);
|
||||
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context).dataCopied),
|
||||
),
|
||||
);
|
||||
});
|
||||
logProvider.setLog(pastLog);
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context).dataCopied),
|
||||
),
|
||||
);
|
||||
},
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 40),
|
||||
);
|
||||
@@ -500,15 +427,11 @@ class LogFormWidget extends ConsumerStatefulWidget {
|
||||
|
||||
final PageController controller;
|
||||
final SetConfigData configData;
|
||||
final Log log;
|
||||
final FocusNode focusNode;
|
||||
|
||||
LogFormWidget({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.configData,
|
||||
required this.log,
|
||||
required this.focusNode,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -519,61 +442,11 @@ class _LogFormWidgetState extends ConsumerState<LogFormWidget> {
|
||||
final _form = GlobalKey<FormState>();
|
||||
var _detailed = false;
|
||||
bool _isSaving = false;
|
||||
late Log _log;
|
||||
|
||||
late final TextEditingController _repetitionsController;
|
||||
late final TextEditingController _weightController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_log = widget.log;
|
||||
_repetitionsController = TextEditingController();
|
||||
_weightController = TextEditingController();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final locale = Localizations.localeOf(context).toString();
|
||||
final numberFormat = NumberFormat.decimalPattern(locale);
|
||||
|
||||
if (widget.configData.repetitions != null) {
|
||||
_repetitionsController.text = numberFormat.format(widget.configData.repetitions);
|
||||
}
|
||||
|
||||
if (widget.configData.weight != null) {
|
||||
_weightController.text = numberFormat.format(widget.configData.weight);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_repetitionsController.dispose();
|
||||
_weightController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void copyFromPastLog(Log pastLog) {
|
||||
final locale = Localizations.localeOf(context).toString();
|
||||
final numberFormat = NumberFormat.decimalPattern(locale);
|
||||
|
||||
setState(() {
|
||||
_repetitionsController.text = pastLog.repetitions != null
|
||||
? numberFormat.format(pastLog.repetitions)
|
||||
: '';
|
||||
widget._logger.finer('Setting log repetitions to ${_repetitionsController.text}');
|
||||
|
||||
_weightController.text = pastLog.weight != null ? numberFormat.format(pastLog.weight) : '';
|
||||
widget._logger.finer('Setting log weight to ${_weightController.text}');
|
||||
|
||||
_log.rir = pastLog.rir;
|
||||
widget._logger.finer('Setting log rir to ${_log.rir}');
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final i18n = AppLocalizations.of(context);
|
||||
final log = ref.watch(gymLogProvider);
|
||||
|
||||
return Form(
|
||||
key: _form,
|
||||
@@ -590,25 +463,15 @@ class _LogFormWidgetState extends ConsumerState<LogFormWidget> {
|
||||
children: [
|
||||
Flexible(
|
||||
child: LogsRepsWidget(
|
||||
controller: _repetitionsController,
|
||||
configData: widget.configData,
|
||||
focusNode: widget.focusNode,
|
||||
log: _log,
|
||||
setStateCallback: (fn) {
|
||||
setState(fn);
|
||||
},
|
||||
key: const ValueKey('logs-reps-widget'),
|
||||
valueChange: widget.configData.repetitionsRounding,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: LogsWeightWidget(
|
||||
controller: _weightController,
|
||||
configData: widget.configData,
|
||||
focusNode: widget.focusNode,
|
||||
log: _log,
|
||||
setStateCallback: (fn) {
|
||||
setState(fn);
|
||||
},
|
||||
key: const ValueKey('logs-weight-widget'),
|
||||
valueChange: widget.configData.weightRounding,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -619,19 +482,15 @@ class _LogFormWidgetState extends ConsumerState<LogFormWidget> {
|
||||
children: [
|
||||
Flexible(
|
||||
child: LogsRepsWidget(
|
||||
controller: _repetitionsController,
|
||||
configData: widget.configData,
|
||||
focusNode: widget.focusNode,
|
||||
log: _log,
|
||||
setStateCallback: (fn) {
|
||||
setState(fn);
|
||||
},
|
||||
key: const ValueKey('logs-reps-widget'),
|
||||
valueChange: widget.configData.repetitionsRounding,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: RepetitionUnitInputWidget(
|
||||
_log.repetitionsUnitId,
|
||||
key: const ValueKey('repetition-unit-input-widget'),
|
||||
log!.repetitionsUnitId,
|
||||
onChanged: (v) => {},
|
||||
),
|
||||
),
|
||||
@@ -644,34 +503,31 @@ class _LogFormWidgetState extends ConsumerState<LogFormWidget> {
|
||||
children: [
|
||||
Flexible(
|
||||
child: LogsWeightWidget(
|
||||
controller: _weightController,
|
||||
configData: widget.configData,
|
||||
focusNode: widget.focusNode,
|
||||
log: _log,
|
||||
setStateCallback: (fn) {
|
||||
setState(fn);
|
||||
},
|
||||
key: const ValueKey('logs-weight-widget'),
|
||||
valueChange: widget.configData.weightRounding,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: WeightUnitInputWidget(_log.weightUnitId, onChanged: (v) => {}),
|
||||
child: WeightUnitInputWidget(
|
||||
log!.weightUnitId,
|
||||
onChanged: (v) => {},
|
||||
key: const ValueKey('weight-unit-input-widget'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
if (_detailed)
|
||||
RiRInputWidget(
|
||||
_log.rir,
|
||||
key: const ValueKey('rir-input-widget'),
|
||||
log!.rir,
|
||||
onChanged: (value) {
|
||||
if (value == '') {
|
||||
_log.rir = null;
|
||||
} else {
|
||||
_log.rir = num.parse(value);
|
||||
}
|
||||
log.rir = value == '' ? null : num.parse(value);
|
||||
},
|
||||
),
|
||||
SwitchListTile(
|
||||
key: const ValueKey('units-switch'),
|
||||
dense: true,
|
||||
title: Text(i18n.setUnitsAndRir),
|
||||
value: _detailed,
|
||||
@@ -682,6 +538,7 @@ class _LogFormWidgetState extends ConsumerState<LogFormWidget> {
|
||||
},
|
||||
),
|
||||
FilledButton(
|
||||
key: const ValueKey('save-log-button'),
|
||||
onPressed: _isSaving
|
||||
? null
|
||||
: () async {
|
||||
@@ -699,7 +556,7 @@ class _LogFormWidgetState extends ConsumerState<LogFormWidget> {
|
||||
await provider.Provider.of<RoutinesProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
).addLog(_log);
|
||||
).addLog(log!);
|
||||
final page = gymState.getSlotEntryPageByIndex()!;
|
||||
gymProvider.markSlotPageAsDone(page.uuid, isDone: true);
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (C) 2020, 2021 wger Team
|
||||
* Copyright (c) 2026 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* wger Workout Manager is distributed in the hope that it will be useful,
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
@@ -28,7 +28,12 @@ class NavigationHeader extends StatelessWidget {
|
||||
final String _title;
|
||||
final bool showEndWorkoutButton;
|
||||
|
||||
const NavigationHeader(this._title, this._controller, {this.showEndWorkoutButton = true});
|
||||
const NavigationHeader(
|
||||
this._title,
|
||||
this._controller, {
|
||||
this.showEndWorkoutButton = true,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (C) 2020, 2025 wger Team
|
||||
* Copyright (c) 2020 - 2025 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* wger Workout Manager is distributed in the hope that it will be useful,
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
@@ -15,59 +15,96 @@
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/helpers/consts.dart';
|
||||
import 'package:wger/helpers/date.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/models/workouts/routine.dart';
|
||||
import 'package:wger/models/workouts/session.dart';
|
||||
import 'package:wger/providers/gym_state.dart';
|
||||
import 'package:wger/providers/routines.dart';
|
||||
import 'package:wger/widgets/routines/forms/session.dart';
|
||||
import 'package:wger/widgets/routines/gym_mode/navigation.dart';
|
||||
|
||||
class SessionPage extends ConsumerWidget {
|
||||
class SessionPage extends ConsumerStatefulWidget {
|
||||
final PageController _controller;
|
||||
|
||||
const SessionPage(this._controller);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final state = ref.watch(gymStateProvider);
|
||||
ConsumerState<SessionPage> createState() => _SessionPageState();
|
||||
}
|
||||
|
||||
final session = state.routine.sessions
|
||||
.map((sessionApi) => sessionApi.session)
|
||||
.firstWhere(
|
||||
(session) => session.date.isSameDayAs(clock.now()),
|
||||
orElse: () => WorkoutSession(
|
||||
dayId: state.dayId,
|
||||
routineId: state.routine.id,
|
||||
impression: DEFAULT_IMPRESSION,
|
||||
date: clock.now(),
|
||||
timeStart: state.startTime,
|
||||
timeEnd: TimeOfDay.fromDateTime(clock.now()),
|
||||
),
|
||||
);
|
||||
class _SessionPageState extends ConsumerState<SessionPage> {
|
||||
late Future<void> _initData;
|
||||
late Routine _routine;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initData = _reloadRoutineData();
|
||||
}
|
||||
|
||||
Future<void> _reloadRoutineData() async {
|
||||
final gymState = ref.read(gymStateProvider);
|
||||
_routine = await context.read<RoutinesProvider>().fetchAndSetRoutineFull(gymState.routine.id!);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final i18n = AppLocalizations.of(context);
|
||||
final gymState = ref.read(gymStateProvider);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
NavigationHeader(
|
||||
AppLocalizations.of(context).workoutSession,
|
||||
_controller,
|
||||
),
|
||||
Expanded(child: Container()),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
child: SessionForm(
|
||||
state.routine.id,
|
||||
onSaved: () => _controller.nextPage(
|
||||
duration: DEFAULT_ANIMATION_DURATION,
|
||||
curve: DEFAULT_ANIMATION_CURVE,
|
||||
),
|
||||
session: session,
|
||||
NavigationHeader(i18n.workoutSession, widget._controller),
|
||||
Expanded(
|
||||
child: FutureBuilder<void>(
|
||||
future: _initData,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (snapshot.hasError) {
|
||||
return Center(child: Text('Error: ${snapshot.error}'));
|
||||
} else {
|
||||
final session = _routine.sessions
|
||||
.map((sessionApi) => sessionApi.session)
|
||||
.firstWhere(
|
||||
(s) => s.date.isSameDayAs(clock.now()),
|
||||
orElse: () => WorkoutSession(
|
||||
dayId: gymState.dayId,
|
||||
date: clock.now(),
|
||||
routineId: gymState.routine.id,
|
||||
timeStart: gymState.startTime,
|
||||
timeEnd: TimeOfDay.fromDateTime(clock.now()),
|
||||
),
|
||||
);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(child: Container()),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
child: SessionForm(
|
||||
gymState.routine.id,
|
||||
onSaved: () => widget._controller.nextPage(
|
||||
duration: DEFAULT_ANIMATION_DURATION,
|
||||
curve: DEFAULT_ANIMATION_CURVE,
|
||||
),
|
||||
session: session,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
NavigationFooter(_controller),
|
||||
NavigationFooter(widget._controller),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
144
pubspec.lock
144
pubspec.lock
@@ -13,10 +13,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: a40a0cee526a7e1f387c6847bd8a5ccbf510a75952ef8a28338e989558072cb0
|
||||
sha256: f51c8499b35f9b26820cfe914828a6a98a94efd5cc78b37bb7d03debae3a1d08
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.4.0"
|
||||
version: "8.4.1"
|
||||
analyzer_buffer:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -25,14 +25,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.11"
|
||||
analyzer_plugin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer_plugin
|
||||
sha256: "08cfefa90b4f4dd3b447bda831cecf644029f9f8e22820f6ee310213ebe2dd53"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.13.10"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -69,10 +61,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build
|
||||
sha256: c1668065e9ba04752570ad7e038288559d1e2ca5c6d0131c0f5f55e39e777413
|
||||
sha256: "275bf6bb2a00a9852c28d4e0b410da1d833a734d57d39d44f94bfc895a484ec3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.3"
|
||||
version: "4.0.4"
|
||||
build_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -93,10 +85,10 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
sha256: "110c56ef29b5eb367b4d17fc79375fa8c18a6cd7acd92c05bb3986c17a079057"
|
||||
sha256: b4d854962a32fd9f8efc0b76f98214790b833af8b2e9b2df6bfc927c0415a072
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.10.4"
|
||||
version: "2.10.5"
|
||||
built_collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -109,10 +101,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_value
|
||||
sha256: "426cf75afdb23aa74bd4e471704de3f9393f3c7b04c1e2d9c6f1073ae0b8b139"
|
||||
sha256: "120df83d4a4ce6bed06ad653c0a3e85737e0f66664f31e17a55136ff5a738cde"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.12.1"
|
||||
version: "8.12.2"
|
||||
camera:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -125,18 +117,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: camera_android_camerax
|
||||
sha256: "1f1d1ff65223c59018d58bdac5211417c2af60bcb469c9d26f928dd412eb91cf"
|
||||
sha256: bc7a96998258adddd0b653dd693b0874537707d58b0489708f2a646e4f124246
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.24+3"
|
||||
version: "0.6.27"
|
||||
camera_avfoundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: camera_avfoundation
|
||||
sha256: "035b90c1e33c2efad7548f402572078f6e514d4f82be0a315cd6c6af7e855aa8"
|
||||
sha256: "087a9fadef20325cb246b4c13344a3ce8e408acfc3e0c665ebff0ec3144d7163"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.22+6"
|
||||
version: "0.9.22+8"
|
||||
camera_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -149,10 +141,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: camera_web
|
||||
sha256: "77e53acb64d9de8917424eeb32b5c7c73572d1e00954bbf54a1e609d79a751a2"
|
||||
sha256: "57f49a635c8bf249d07fb95eb693d7e4dda6796dedb3777f9127fb54847beba7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.5+1"
|
||||
version: "0.3.5+3"
|
||||
carousel_slider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -197,10 +189,10 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: cider
|
||||
sha256: dfff70e9324f99e315857c596c31f54cb7380cfa20dfdfdca11a3631e05b7d3e
|
||||
sha256: "455e3549bd1d21708326985702703345245acd3d7a2ac485de4183affb414a2c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.8"
|
||||
version: "0.2.9"
|
||||
cli_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -229,10 +221,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_builder
|
||||
sha256: "11654819532ba94c34de52ff5feb52bd81cba1de00ef2ed622fd50295f9d4243"
|
||||
sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.11.0"
|
||||
version: "4.11.1"
|
||||
collection:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -289,22 +281,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.8"
|
||||
custom_lint_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: custom_lint_core
|
||||
sha256: "85b339346154d5646952d44d682965dfe9e12cae5febd706f0db3aa5010d6423"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.1"
|
||||
custom_lint_visitor:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: custom_lint_visitor
|
||||
sha256: "91f2a81e9f0abb4b9f3bb529f78b6227ce6050300d1ae5b1e2c69c66c7a566d8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0+8.4.0"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -317,26 +293,26 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: drift
|
||||
sha256: "83290a32ae006a7535c5ecf300722cb77177250d9df4ee2becc5fa8a36095114"
|
||||
sha256: "5ea2f718558c0b31d4b8c36a3d8e5b7016f1265f46ceb5a5920e16117f0c0d6a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.29.0"
|
||||
version: "2.30.1"
|
||||
drift_dev:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: drift_dev
|
||||
sha256: "6019f827544e77524ffd5134ae0cb75dfd92ef5ef3e269872af92840c929cd43"
|
||||
sha256: "892dfb5d69d9e604bdcd102a9376de8b41768cf7be93fd26b63cfc4d8f91ad5f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.29.0"
|
||||
version: "2.30.1"
|
||||
equatable:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: equatable
|
||||
sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7"
|
||||
sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.7"
|
||||
version: "2.0.8"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -349,10 +325,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
|
||||
sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
version: "2.1.5"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -516,10 +492,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_riverpod
|
||||
sha256: "9e2d6907f12cc7d23a846847615941bddee8709bf2bfd274acdf5e80bcf22fde"
|
||||
sha256: "38ec6c303e2c83ee84512f5fc2a82ae311531021938e63d7137eccc107bf3c02"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
version: "3.1.0"
|
||||
flutter_staggered_grid_view:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -667,10 +643,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image
|
||||
sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
|
||||
sha256: "492bd52f6c4fbb6ee41f781ff27765ce5f627910e1e0cbecfa3d9add5562604c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.4"
|
||||
version: "4.7.2"
|
||||
image_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -699,10 +675,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_ios
|
||||
sha256: "997d100ce1dda5b1ba4085194c5e36c9f8a1fb7987f6a36ab677a344cd2dc986"
|
||||
sha256: "956c16a42c0c708f914021666ffcd8265dde36e673c9fa68c81f7d085d9774ad"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.13+2"
|
||||
version: "0.8.13+3"
|
||||
image_picker_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -880,10 +856,10 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: mockito
|
||||
sha256: dac24d461418d363778d53198d9ac0510b9d073869f078450f195766ec48d05e
|
||||
sha256: a45d1aa065b796922db7b9e7e7e45f921aed17adf3a8318a1f47097e7e695566
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.6.1"
|
||||
version: "5.6.3"
|
||||
multi_select_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -976,10 +952,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "97390a0719146c7c3e71b6866c34f1cde92685933165c1c671984390d2aca776"
|
||||
sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.4"
|
||||
version: "2.5.1"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1144,50 +1120,50 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: riverpod
|
||||
sha256: c406de02bff19d920b832bddfb8283548bfa05ce41c59afba57ce643e116aa59
|
||||
sha256: "16ff608d21e8ea64364f2b7c049c94a02ab81668f78845862b6e88b71dd4935a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
version: "3.1.0"
|
||||
riverpod_analyzer_utils:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: riverpod_analyzer_utils
|
||||
sha256: a0f68adb078b790faa3c655110a017f9a7b7b079a57bbd40f540e80dce5fcd29
|
||||
sha256: "947b05d04c52a546a2ac6b19ef2a54b08520ff6bdf9f23d67957a4c8df1c3bc0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0-dev.7"
|
||||
version: "1.0.0-dev.8"
|
||||
riverpod_annotation:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: riverpod_annotation
|
||||
sha256: "7230014155777fc31ba3351bc2cb5a3b5717b11bfafe52b1553cb47d385f8897"
|
||||
sha256: cc1474bc2df55ec3c1da1989d139dcef22cd5e2bd78da382e867a69a8eca2e46
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
version: "4.0.0"
|
||||
riverpod_generator:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: riverpod_generator
|
||||
sha256: "49894543a42cf7a9954fc4e7366b6d3cb2e6ec0fa07775f660afcdd92d097702"
|
||||
sha256: e43b1537229cc8f487f09b0c20d15dba840acbadcf5fc6dad7ad5e8ab75950dc
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
version: "4.0.0+1"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
|
||||
sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.3"
|
||||
version: "2.5.4"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: "46a46fd64659eff15f4638bbe19de43f9483f0e0bf024a9fb6b3582064bacc7b"
|
||||
sha256: "83af5c682796c0f7719c2bbf74792d113e40ae97981b8f266fa84574573556bc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.17"
|
||||
version: "2.4.18"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1333,10 +1309,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlparser
|
||||
sha256: "54eea43e36dd3769274c3108625f9ea1a382f8d2ac8b16f3e4589d9bd9b0e16c"
|
||||
sha256: f52f5d5649dcc13ed198c4176ddef74bf6851c30f4f31603f1b37788695b93e2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.42.0"
|
||||
version: "0.43.0"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1497,14 +1473,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.5"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: uuid
|
||||
sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.2"
|
||||
vector_graphics:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1565,18 +1533,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_android
|
||||
sha256: "3f7ef3fb7b29f510e58f4d56b6ffbc3463b1071f2cf56e10f8d25f5b991ed85b"
|
||||
sha256: ee4fd520b0cafa02e4a867a0f882092e727cdaa1a2d24762171e787f8a502b0a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.8.21"
|
||||
version: "2.9.1"
|
||||
video_player_avfoundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_avfoundation
|
||||
sha256: "6bced1739cf1f96f03058118adb8ac0dd6f96aa1a1a6e526424ab92fd2a6a77d"
|
||||
sha256: d1eb970495a76abb35e5fa93ee3c58bd76fb6839e2ddf2fbb636674f2b971dd4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.8.7"
|
||||
version: "2.8.9"
|
||||
video_player_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1605,10 +1573,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: watcher
|
||||
sha256: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a"
|
||||
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.4"
|
||||
version: "1.2.1"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
14
pubspec.yaml
14
pubspec.yaml
@@ -21,7 +21,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# - the version number is taken from the git tag vX.Y.Z
|
||||
# - the build number is computed by reading the last one from the play store
|
||||
# and increasing by one
|
||||
version: 1.9.2+120
|
||||
version: 1.9.5+150
|
||||
|
||||
environment:
|
||||
sdk: '>=3.8.0 <4.0.0'
|
||||
@@ -36,8 +36,8 @@ dependencies:
|
||||
clock: ^1.1.2
|
||||
collection: ^1.18.0
|
||||
cupertino_icons: ^1.0.8
|
||||
drift: ^2.29.0
|
||||
equatable: ^2.0.7
|
||||
drift: ^2.30.0
|
||||
equatable: ^2.0.8
|
||||
fl_chart: ^1.1.1
|
||||
flex_color_scheme: ^8.3.1
|
||||
flutter_html: ^3.0.0
|
||||
@@ -66,8 +66,8 @@ dependencies:
|
||||
version: ^3.0.2
|
||||
video_player: ^2.10.1
|
||||
logging: ^1.3.0
|
||||
flutter_riverpod: ^3.0.3
|
||||
riverpod_annotation: ^3.0.3
|
||||
flutter_riverpod: ^3.1.0
|
||||
riverpod_annotation: ^4.0.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
@@ -75,7 +75,7 @@ dev_dependencies:
|
||||
integration_test:
|
||||
sdk: flutter
|
||||
build_runner: ^2.10.4
|
||||
cider: ^0.2.7
|
||||
cider: ^0.2.9
|
||||
drift_dev: ^2.29.0
|
||||
flutter_lints: ^6.0.0
|
||||
freezed: ^3.2.0
|
||||
@@ -83,7 +83,7 @@ dev_dependencies:
|
||||
mockito: ^5.4.4
|
||||
network_image_mock: ^2.1.1
|
||||
shared_preferences_platform_interface: ^2.0.0
|
||||
riverpod_generator: ^3.0.3
|
||||
riverpod_generator: ^4.0.0+1
|
||||
|
||||
# Script to read out unused translations
|
||||
#translations_cleaner: ^0.0.5
|
||||
|
||||
107
test/core/http_exception_test.dart
Normal file
107
test/core/http_exception_test.dart
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (c) 2020 - 2026 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:wger/core/exceptions/http_exception.dart';
|
||||
|
||||
void main() {
|
||||
group('WgerHttpException', () {
|
||||
test('parses valid JSON response', () {
|
||||
// Arrange
|
||||
final resp = http.Response(
|
||||
'{"foo":"bar"}',
|
||||
400,
|
||||
headers: {HttpHeaders.contentTypeHeader: 'application/json'},
|
||||
);
|
||||
|
||||
// Act
|
||||
final ex = WgerHttpException(resp);
|
||||
|
||||
// Assert
|
||||
expect(ex.type, ErrorType.json);
|
||||
expect(ex.errors['foo'], 'bar');
|
||||
expect(ex.toString(), contains('WgerHttpException'));
|
||||
});
|
||||
|
||||
test('falls back on malformed JSON', () {
|
||||
// Arrange
|
||||
const body = '{"foo":';
|
||||
final resp = http.Response(
|
||||
body,
|
||||
500,
|
||||
headers: {HttpHeaders.contentTypeHeader: 'application/json'},
|
||||
);
|
||||
|
||||
// Act
|
||||
final ex = WgerHttpException(resp);
|
||||
|
||||
// Assert
|
||||
expect(ex.type, ErrorType.json);
|
||||
expect(ex.errors['unknown_error'], body);
|
||||
});
|
||||
|
||||
test('detects HTML response from headers', () {
|
||||
// Arrange
|
||||
const body = '<html lang="en"><body>Error</body></html>';
|
||||
final resp = http.Response(
|
||||
body,
|
||||
500,
|
||||
headers: {HttpHeaders.contentTypeHeader: 'text/html; charset=utf-8'},
|
||||
);
|
||||
|
||||
// Act
|
||||
final ex = WgerHttpException(resp);
|
||||
|
||||
// Assert
|
||||
expect(ex.type, ErrorType.html);
|
||||
expect(ex.htmlError, body);
|
||||
});
|
||||
|
||||
test('detects HTML response from content', () {
|
||||
// Arrange
|
||||
const body = '<html lang="en"><body>Error</body></html>';
|
||||
final resp = http.Response(
|
||||
body,
|
||||
500,
|
||||
headers: {HttpHeaders.contentTypeHeader: 'text/foo; charset=utf-8'},
|
||||
);
|
||||
|
||||
// Act
|
||||
final ex = WgerHttpException(resp);
|
||||
|
||||
// Assert
|
||||
expect(ex.type, ErrorType.html);
|
||||
expect(ex.htmlError, body);
|
||||
});
|
||||
|
||||
test('fromMap sets errors and type', () {
|
||||
// Arrange
|
||||
final map = <String, dynamic>{'field': 'value'};
|
||||
|
||||
// Act
|
||||
final ex = WgerHttpException.fromMap(map);
|
||||
|
||||
// Assert
|
||||
expect(ex.type, ErrorType.json);
|
||||
expect(ex.errors, map);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1100,9 +1100,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i2.WgerBaseProvider {
|
||||
as Uri);
|
||||
|
||||
@override
|
||||
_i18.Future<dynamic> fetch(Uri? uri) =>
|
||||
_i18.Future<dynamic> fetch(
|
||||
Uri? uri, {
|
||||
int? maxRetries = 3,
|
||||
Duration? initialDelay = const Duration(milliseconds: 250),
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#fetch, [uri]),
|
||||
Invocation.method(
|
||||
#fetch,
|
||||
[uri],
|
||||
{#maxRetries: maxRetries, #initialDelay: initialDelay},
|
||||
),
|
||||
returnValue: _i18.Future<dynamic>.value(),
|
||||
)
|
||||
as _i18.Future<dynamic>);
|
||||
|
||||
@@ -20,6 +20,7 @@ import 'package:wger/l10n/generated/app_localizations.dart' as _i2;
|
||||
// ignore_for_file: unnecessary_parenthesis
|
||||
// ignore_for_file: camel_case_types
|
||||
// ignore_for_file: subtype_of_sealed_class
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
/// A class which mocks [AppLocalizations].
|
||||
///
|
||||
@@ -919,6 +920,39 @@ class MockAppLocalizations extends _i1.Mock implements _i2.AppLocalizations {
|
||||
)
|
||||
as String);
|
||||
|
||||
@override
|
||||
String get impressionGood =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#impressionGood),
|
||||
returnValue: _i3.dummyValue<String>(
|
||||
this,
|
||||
Invocation.getter(#impressionGood),
|
||||
),
|
||||
)
|
||||
as String);
|
||||
|
||||
@override
|
||||
String get impressionNeutral =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#impressionNeutral),
|
||||
returnValue: _i3.dummyValue<String>(
|
||||
this,
|
||||
Invocation.getter(#impressionNeutral),
|
||||
),
|
||||
)
|
||||
as String);
|
||||
|
||||
@override
|
||||
String get impressionBad =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#impressionBad),
|
||||
returnValue: _i3.dummyValue<String>(
|
||||
this,
|
||||
Invocation.getter(#impressionBad),
|
||||
),
|
||||
)
|
||||
as String);
|
||||
|
||||
@override
|
||||
String get impression =>
|
||||
(super.noSuchMethod(
|
||||
@@ -1095,6 +1129,105 @@ class MockAppLocalizations extends _i1.Mock implements _i2.AppLocalizations {
|
||||
)
|
||||
as String);
|
||||
|
||||
@override
|
||||
String get gymModeTimerType =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#gymModeTimerType),
|
||||
returnValue: _i3.dummyValue<String>(
|
||||
this,
|
||||
Invocation.getter(#gymModeTimerType),
|
||||
),
|
||||
)
|
||||
as String);
|
||||
|
||||
@override
|
||||
String get gymModeTimerTypeHelText =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#gymModeTimerTypeHelText),
|
||||
returnValue: _i3.dummyValue<String>(
|
||||
this,
|
||||
Invocation.getter(#gymModeTimerTypeHelText),
|
||||
),
|
||||
)
|
||||
as String);
|
||||
|
||||
@override
|
||||
String get countdown =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#countdown),
|
||||
returnValue: _i3.dummyValue<String>(
|
||||
this,
|
||||
Invocation.getter(#countdown),
|
||||
),
|
||||
)
|
||||
as String);
|
||||
|
||||
@override
|
||||
String get stopwatch =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#stopwatch),
|
||||
returnValue: _i3.dummyValue<String>(
|
||||
this,
|
||||
Invocation.getter(#stopwatch),
|
||||
),
|
||||
)
|
||||
as String);
|
||||
|
||||
@override
|
||||
String get gymModeDefaultCountdownTime =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#gymModeDefaultCountdownTime),
|
||||
returnValue: _i3.dummyValue<String>(
|
||||
this,
|
||||
Invocation.getter(#gymModeDefaultCountdownTime),
|
||||
),
|
||||
)
|
||||
as String);
|
||||
|
||||
@override
|
||||
String get gymModeNotifyOnCountdownFinish =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#gymModeNotifyOnCountdownFinish),
|
||||
returnValue: _i3.dummyValue<String>(
|
||||
this,
|
||||
Invocation.getter(#gymModeNotifyOnCountdownFinish),
|
||||
),
|
||||
)
|
||||
as String);
|
||||
|
||||
@override
|
||||
String get duration =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#duration),
|
||||
returnValue: _i3.dummyValue<String>(
|
||||
this,
|
||||
Invocation.getter(#duration),
|
||||
),
|
||||
)
|
||||
as String);
|
||||
|
||||
@override
|
||||
String get volume =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#volume),
|
||||
returnValue: _i3.dummyValue<String>(
|
||||
this,
|
||||
Invocation.getter(#volume),
|
||||
),
|
||||
)
|
||||
as String);
|
||||
|
||||
@override
|
||||
String get workoutCompleted =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#workoutCompleted),
|
||||
returnValue: _i3.dummyValue<String>(
|
||||
this,
|
||||
Invocation.getter(#workoutCompleted),
|
||||
),
|
||||
)
|
||||
as String);
|
||||
|
||||
@override
|
||||
String get plateCalculator =>
|
||||
(super.noSuchMethod(
|
||||
@@ -3677,6 +3810,17 @@ class MockAppLocalizations extends _i1.Mock implements _i2.AppLocalizations {
|
||||
)
|
||||
as String);
|
||||
|
||||
@override
|
||||
String durationHoursMinutes(int? hours, int? minutes) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#durationHoursMinutes, [hours, minutes]),
|
||||
returnValue: _i3.dummyValue<String>(
|
||||
this,
|
||||
Invocation.method(#durationHoursMinutes, [hours, minutes]),
|
||||
),
|
||||
)
|
||||
as String);
|
||||
|
||||
@override
|
||||
String chartAllTimeTitle(String? name) =>
|
||||
(super.noSuchMethod(
|
||||
@@ -3765,6 +3909,17 @@ class MockAppLocalizations extends _i1.Mock implements _i2.AppLocalizations {
|
||||
)
|
||||
as String);
|
||||
|
||||
@override
|
||||
String formMinMaxValues(int? min, int? max) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#formMinMaxValues, [min, max]),
|
||||
returnValue: _i3.dummyValue<String>(
|
||||
this,
|
||||
Invocation.method(#formMinMaxValues, [min, max]),
|
||||
),
|
||||
)
|
||||
as String);
|
||||
|
||||
@override
|
||||
String enterMinCharacters(String? min) =>
|
||||
(super.noSuchMethod(
|
||||
|
||||
@@ -402,9 +402,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i2.WgerBaseProvider {
|
||||
as Uri);
|
||||
|
||||
@override
|
||||
_i14.Future<dynamic> fetch(Uri? uri) =>
|
||||
_i14.Future<dynamic> fetch(
|
||||
Uri? uri, {
|
||||
int? maxRetries = 3,
|
||||
Duration? initialDelay = const Duration(milliseconds: 250),
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#fetch, [uri]),
|
||||
Invocation.method(
|
||||
#fetch,
|
||||
[uri],
|
||||
{#maxRetries: maxRetries, #initialDelay: initialDelay},
|
||||
),
|
||||
returnValue: _i14.Future<dynamic>.value(),
|
||||
)
|
||||
as _i14.Future<dynamic>);
|
||||
|
||||
@@ -21,7 +21,7 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/exceptions/http_exception.dart';
|
||||
import 'package:wger/core/exceptions/http_exception.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/providers/add_exercise.dart';
|
||||
import 'package:wger/providers/exercises.dart';
|
||||
@@ -422,7 +422,7 @@ void main() {
|
||||
testWidgets('Failed submission displays error message', (WidgetTester tester) async {
|
||||
// Setup: Create verified user and mock failed submission
|
||||
setupFullVerifiedUserContext();
|
||||
final httpException = WgerHttpException({
|
||||
final httpException = WgerHttpException.fromMap({
|
||||
'name': ['This field is required'],
|
||||
});
|
||||
when(mockAddExerciseProvider.postExerciseToServer()).thenThrow(httpException);
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (c) 2026 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:drift/native.dart';
|
||||
@@ -100,19 +118,25 @@ void main() {
|
||||
SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty();
|
||||
|
||||
// Mock categories
|
||||
when(mockBaseProvider.makeUrl(categoryUrl)).thenReturn(tCategoryEntriesUri);
|
||||
when(
|
||||
mockBaseProvider.makeUrl(categoryUrl, query: anyNamed('query')),
|
||||
).thenReturn(tCategoryEntriesUri);
|
||||
when(
|
||||
mockBaseProvider.fetchPaginated(tCategoryEntriesUri),
|
||||
).thenAnswer((_) => Future.value(tCategoryMap['results']));
|
||||
|
||||
// Mock muscles
|
||||
when(mockBaseProvider.makeUrl(muscleUrl)).thenReturn(tMuscleEntriesUri);
|
||||
when(
|
||||
mockBaseProvider.makeUrl(muscleUrl, query: anyNamed('query')),
|
||||
).thenReturn(tMuscleEntriesUri);
|
||||
when(
|
||||
mockBaseProvider.fetchPaginated(tMuscleEntriesUri),
|
||||
).thenAnswer((_) => Future.value(tMuscleMap['results']));
|
||||
|
||||
// Mock equipment
|
||||
when(mockBaseProvider.makeUrl(equipmentUrl)).thenReturn(tEquipmentEntriesUri);
|
||||
when(
|
||||
mockBaseProvider.makeUrl(equipmentUrl, query: anyNamed('query')),
|
||||
).thenReturn(tEquipmentEntriesUri);
|
||||
when(
|
||||
mockBaseProvider.fetchPaginated(tEquipmentEntriesUri),
|
||||
).thenAnswer((_) => Future.value(tEquipmentMap['results']));
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (c) 2026 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
@@ -7,8 +25,8 @@ import 'package:mockito/mockito.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart';
|
||||
import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart';
|
||||
import 'package:wger/core/exceptions/no_such_entry_exception.dart';
|
||||
import 'package:wger/database/exercises/exercise_database.dart';
|
||||
import 'package:wger/exceptions/no_such_entry_exception.dart';
|
||||
import 'package:wger/helpers/consts.dart';
|
||||
import 'package:wger/helpers/shared_preferences.dart';
|
||||
import 'package:wger/models/exercises/category.dart';
|
||||
@@ -105,19 +123,25 @@ void main() {
|
||||
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
|
||||
|
||||
// Mock categories
|
||||
when(mockBaseProvider.makeUrl(categoryUrl)).thenReturn(tCategoryEntriesUri);
|
||||
when(
|
||||
mockBaseProvider.makeUrl(categoryUrl, query: anyNamed('query')),
|
||||
).thenReturn(tCategoryEntriesUri);
|
||||
when(
|
||||
mockBaseProvider.fetchPaginated(tCategoryEntriesUri),
|
||||
).thenAnswer((_) => Future.value(tCategoryMap['results']));
|
||||
|
||||
// Mock muscles
|
||||
when(mockBaseProvider.makeUrl(muscleUrl)).thenReturn(tMuscleEntriesUri);
|
||||
when(
|
||||
mockBaseProvider.makeUrl(muscleUrl, query: anyNamed('query')),
|
||||
).thenReturn(tMuscleEntriesUri);
|
||||
when(
|
||||
mockBaseProvider.fetchPaginated(tMuscleEntriesUri),
|
||||
).thenAnswer((_) => Future.value(tMuscleMap['results']));
|
||||
|
||||
// Mock equipment
|
||||
when(mockBaseProvider.makeUrl(equipmentUrl)).thenReturn(tEquipmentEntriesUri);
|
||||
when(
|
||||
mockBaseProvider.makeUrl(equipmentUrl, query: anyNamed('query')),
|
||||
).thenReturn(tEquipmentEntriesUri);
|
||||
when(
|
||||
mockBaseProvider.fetchPaginated(tEquipmentEntriesUri),
|
||||
).thenAnswer((_) => Future.value(tEquipmentMap['results']));
|
||||
|
||||
@@ -175,9 +175,17 @@ class MockGalleryProvider extends _i1.Mock implements _i4.GalleryProvider {
|
||||
as Uri);
|
||||
|
||||
@override
|
||||
_i6.Future<dynamic> fetch(Uri? uri) =>
|
||||
_i6.Future<dynamic> fetch(
|
||||
Uri? uri, {
|
||||
int? maxRetries = 3,
|
||||
Duration? initialDelay = const Duration(milliseconds: 250),
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#fetch, [uri]),
|
||||
Invocation.method(
|
||||
#fetch,
|
||||
[uri],
|
||||
{#maxRetries: maxRetries, #initialDelay: initialDelay},
|
||||
),
|
||||
returnValue: _i6.Future<dynamic>.value(),
|
||||
)
|
||||
as _i6.Future<dynamic>);
|
||||
|
||||
@@ -175,9 +175,17 @@ class MockGalleryProvider extends _i1.Mock implements _i4.GalleryProvider {
|
||||
as Uri);
|
||||
|
||||
@override
|
||||
_i6.Future<dynamic> fetch(Uri? uri) =>
|
||||
_i6.Future<dynamic> fetch(
|
||||
Uri? uri, {
|
||||
int? maxRetries = 3,
|
||||
Duration? initialDelay = const Duration(milliseconds: 250),
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#fetch, [uri]),
|
||||
Invocation.method(
|
||||
#fetch,
|
||||
[uri],
|
||||
{#maxRetries: maxRetries, #initialDelay: initialDelay},
|
||||
),
|
||||
returnValue: _i6.Future<dynamic>.value(),
|
||||
)
|
||||
as _i6.Future<dynamic>);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:wger/exceptions/no_such_entry_exception.dart';
|
||||
import 'package:wger/core/exceptions/no_such_entry_exception.dart';
|
||||
import 'package:wger/models/measurements/measurement_category.dart';
|
||||
import 'package:wger/models/measurements/measurement_entry.dart';
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart';
|
||||
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/core/exceptions/http_exception.dart';
|
||||
import 'package:wger/core/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';
|
||||
@@ -271,7 +271,7 @@ void main() {
|
||||
'should re-add the "removed" MeasurementCategory and relay the exception on WgerHttpException',
|
||||
() {
|
||||
// arrange
|
||||
when(mockWgerBaseProvider.deleteRequest(any, any)).thenThrow(WgerHttpException('{}'));
|
||||
when(mockWgerBaseProvider.deleteRequest(any, any)).thenThrow(WgerHttpException.fromMap({}));
|
||||
|
||||
// act & assert
|
||||
expect(
|
||||
@@ -330,7 +330,7 @@ void main() {
|
||||
|
||||
test('should keep categories list as is on WgerHttpException', () {
|
||||
// arrange
|
||||
when(mockWgerBaseProvider.patch(any, any)).thenThrow(WgerHttpException('{}'));
|
||||
when(mockWgerBaseProvider.patch(any, any)).thenThrow(WgerHttpException.fromMap({}));
|
||||
|
||||
// act & assert
|
||||
expect(
|
||||
@@ -550,7 +550,7 @@ void main() {
|
||||
),
|
||||
const MeasurementCategory(id: 2, name: 'Biceps', unit: 'cm'),
|
||||
];
|
||||
when(mockWgerBaseProvider.deleteRequest(any, any)).thenThrow(WgerHttpException('{}'));
|
||||
when(mockWgerBaseProvider.deleteRequest(any, any)).thenThrow(WgerHttpException.fromMap({}));
|
||||
|
||||
// act & assert
|
||||
expect(
|
||||
|
||||
@@ -112,9 +112,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider {
|
||||
as Uri);
|
||||
|
||||
@override
|
||||
_i5.Future<dynamic> fetch(Uri? uri) =>
|
||||
_i5.Future<dynamic> fetch(
|
||||
Uri? uri, {
|
||||
int? maxRetries = 3,
|
||||
Duration? initialDelay = const Duration(milliseconds: 250),
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#fetch, [uri]),
|
||||
Invocation.method(
|
||||
#fetch,
|
||||
[uri],
|
||||
{#maxRetries: maxRetries, #initialDelay: initialDelay},
|
||||
),
|
||||
returnValue: _i5.Future<dynamic>.value(),
|
||||
)
|
||||
as _i5.Future<dynamic>);
|
||||
|
||||
@@ -122,9 +122,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider {
|
||||
as Uri);
|
||||
|
||||
@override
|
||||
_i5.Future<dynamic> fetch(Uri? uri) =>
|
||||
_i5.Future<dynamic> fetch(
|
||||
Uri? uri, {
|
||||
int? maxRetries = 3,
|
||||
Duration? initialDelay = const Duration(milliseconds: 250),
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#fetch, [uri]),
|
||||
Invocation.method(
|
||||
#fetch,
|
||||
[uri],
|
||||
{#maxRetries: maxRetries, #initialDelay: initialDelay},
|
||||
),
|
||||
returnValue: _i5.Future<dynamic>.value(),
|
||||
)
|
||||
as _i5.Future<dynamic>);
|
||||
|
||||
@@ -357,9 +357,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i8.WgerBaseProvider {
|
||||
as Uri);
|
||||
|
||||
@override
|
||||
_i5.Future<dynamic> fetch(Uri? uri) =>
|
||||
_i5.Future<dynamic> fetch(
|
||||
Uri? uri, {
|
||||
int? maxRetries = 3,
|
||||
Duration? initialDelay = const Duration(milliseconds: 250),
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#fetch, [uri]),
|
||||
Invocation.method(
|
||||
#fetch,
|
||||
[uri],
|
||||
{#maxRetries: maxRetries, #initialDelay: initialDelay},
|
||||
),
|
||||
returnValue: _i5.Future<dynamic>.value(),
|
||||
)
|
||||
as _i5.Future<dynamic>);
|
||||
|
||||
135
test/providers/base_provider.dart
Normal file
135
test/providers/base_provider.dart
Normal file
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (c) 2026 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:wger/core/exceptions/http_exception.dart';
|
||||
import 'package:wger/providers/base_provider.dart';
|
||||
|
||||
import '../utils.dart';
|
||||
import 'base_provider.mocks.dart';
|
||||
|
||||
@GenerateMocks([Client])
|
||||
void main() {
|
||||
final Uri testUri = Uri(scheme: 'https', host: 'localhost', path: 'api/v2/test/');
|
||||
|
||||
test('Retry on SocketException then succeeds', () async {
|
||||
// Arrange
|
||||
final mockClient = MockClient();
|
||||
var callCount = 0;
|
||||
when(mockClient.get(testUri, headers: anyNamed('headers'))).thenAnswer((_) {
|
||||
if (callCount == 0) {
|
||||
callCount++;
|
||||
return Future.error(const SocketException('conn fail'));
|
||||
}
|
||||
return Future.value(Response('{"ok": true}', 200));
|
||||
});
|
||||
|
||||
// Act
|
||||
final provider = WgerBaseProvider(testAuthProvider, mockClient);
|
||||
final result = await provider.fetch(testUri, initialDelay: const Duration(milliseconds: 1));
|
||||
|
||||
// Assert
|
||||
expect(result, isA<Map>());
|
||||
expect(result['ok'], isTrue);
|
||||
verify(mockClient.get(testUri, headers: anyNamed('headers'))).called(2);
|
||||
});
|
||||
|
||||
test('Retry on 5xx then succeeds', () async {
|
||||
// Arrange
|
||||
final mockClient = MockClient();
|
||||
var callCount = 0;
|
||||
when(mockClient.get(testUri, headers: anyNamed('headers'))).thenAnswer((_) {
|
||||
if (callCount == 0) {
|
||||
callCount++;
|
||||
return Future.value(Response('{"msg":"error"}', 502));
|
||||
}
|
||||
return Future.value(Response('{"ok": true}', 200));
|
||||
});
|
||||
|
||||
// Act
|
||||
final provider = WgerBaseProvider(testAuthProvider, mockClient);
|
||||
final result = await provider.fetch(testUri, initialDelay: const Duration(milliseconds: 1));
|
||||
|
||||
// Assert
|
||||
expect(result, isA<Map>());
|
||||
expect(result['ok'], isTrue);
|
||||
verify(mockClient.get(testUri, headers: anyNamed('headers'))).called(2);
|
||||
});
|
||||
|
||||
test('Do not retry on 4xx client error', () async {
|
||||
// Arrange
|
||||
final mockClient = MockClient();
|
||||
when(
|
||||
mockClient.get(testUri, headers: anyNamed('headers')),
|
||||
).thenAnswer((_) => Future.value(Response('{"error":"bad"}', 400)));
|
||||
|
||||
// Act
|
||||
final provider = WgerBaseProvider(testAuthProvider, mockClient);
|
||||
|
||||
// Assert
|
||||
await expectLater(
|
||||
provider.fetch(testUri, initialDelay: const Duration(milliseconds: 1)),
|
||||
throwsA(isA<WgerHttpException>()),
|
||||
);
|
||||
verify(mockClient.get(testUri, headers: anyNamed('headers'))).called(1);
|
||||
});
|
||||
|
||||
test('Exceed max retries and rethrow after retries', () async {
|
||||
// Arrange
|
||||
final mockClient = MockClient();
|
||||
when(
|
||||
mockClient.get(testUri, headers: anyNamed('headers')),
|
||||
).thenAnswer((_) => Future.error(ClientException('conn fail')));
|
||||
|
||||
// Act
|
||||
final provider = WgerBaseProvider(testAuthProvider, mockClient);
|
||||
dynamic caught;
|
||||
try {
|
||||
await provider.fetch(testUri, initialDelay: const Duration(milliseconds: 1));
|
||||
} catch (e) {
|
||||
caught = e;
|
||||
}
|
||||
|
||||
// Assert
|
||||
expect(caught, isA<ClientException>());
|
||||
// initial try + 3 retries = 4 calls
|
||||
verify(mockClient.get(testUri, headers: anyNamed('headers'))).called(4);
|
||||
});
|
||||
|
||||
test('Request succeeds without retries', () async {
|
||||
// Arrange
|
||||
final mockClient = MockClient();
|
||||
when(
|
||||
mockClient.get(testUri, headers: anyNamed('headers')),
|
||||
).thenAnswer((_) => Future.value(Response('{"ok": true}', 200)));
|
||||
|
||||
// Act
|
||||
final provider = WgerBaseProvider(testAuthProvider, mockClient);
|
||||
final result = await provider.fetch(testUri);
|
||||
|
||||
// Assert
|
||||
expect(result, isA<Map>());
|
||||
expect(result['ok'], isTrue);
|
||||
verify(mockClient.get(testUri, headers: anyNamed('headers'))).called(1);
|
||||
});
|
||||
}
|
||||
218
test/providers/base_provider.mocks.dart
Normal file
218
test/providers/base_provider.mocks.dart
Normal file
@@ -0,0 +1,218 @@
|
||||
// Mocks generated by Mockito 5.4.6 from annotations
|
||||
// in wger/test/providers/base_provider.dart.
|
||||
// Do not manually edit this file.
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'dart:async' as _i3;
|
||||
import 'dart:convert' as _i4;
|
||||
import 'dart:typed_data' as _i6;
|
||||
|
||||
import 'package:http/http.dart' as _i2;
|
||||
import 'package:mockito/mockito.dart' as _i1;
|
||||
import 'package:mockito/src/dummies.dart' as _i5;
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: avoid_redundant_argument_values
|
||||
// ignore_for_file: avoid_setters_without_getters
|
||||
// ignore_for_file: comment_references
|
||||
// ignore_for_file: deprecated_member_use
|
||||
// ignore_for_file: deprecated_member_use_from_same_package
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: invalid_use_of_visible_for_testing_member
|
||||
// ignore_for_file: must_be_immutable
|
||||
// ignore_for_file: prefer_const_constructors
|
||||
// ignore_for_file: unnecessary_parenthesis
|
||||
// ignore_for_file: camel_case_types
|
||||
// ignore_for_file: subtype_of_sealed_class
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
class _FakeResponse_0 extends _i1.SmartFake implements _i2.Response {
|
||||
_FakeResponse_0(Object parent, Invocation parentInvocation) : super(parent, parentInvocation);
|
||||
}
|
||||
|
||||
class _FakeStreamedResponse_1 extends _i1.SmartFake implements _i2.StreamedResponse {
|
||||
_FakeStreamedResponse_1(Object parent, Invocation parentInvocation)
|
||||
: super(parent, parentInvocation);
|
||||
}
|
||||
|
||||
/// A class which mocks [Client].
|
||||
///
|
||||
/// See the documentation for Mockito's code generation for more information.
|
||||
class MockClient extends _i1.Mock implements _i2.Client {
|
||||
MockClient() {
|
||||
_i1.throwOnMissingStub(this);
|
||||
}
|
||||
|
||||
@override
|
||||
_i3.Future<_i2.Response> head(Uri? url, {Map<String, String>? headers}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#head, [url], {#headers: headers}),
|
||||
returnValue: _i3.Future<_i2.Response>.value(
|
||||
_FakeResponse_0(
|
||||
this,
|
||||
Invocation.method(#head, [url], {#headers: headers}),
|
||||
),
|
||||
),
|
||||
)
|
||||
as _i3.Future<_i2.Response>);
|
||||
|
||||
@override
|
||||
_i3.Future<_i2.Response> get(Uri? url, {Map<String, String>? headers}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#get, [url], {#headers: headers}),
|
||||
returnValue: _i3.Future<_i2.Response>.value(
|
||||
_FakeResponse_0(
|
||||
this,
|
||||
Invocation.method(#get, [url], {#headers: headers}),
|
||||
),
|
||||
),
|
||||
)
|
||||
as _i3.Future<_i2.Response>);
|
||||
|
||||
@override
|
||||
_i3.Future<_i2.Response> post(
|
||||
Uri? url, {
|
||||
Map<String, String>? headers,
|
||||
Object? body,
|
||||
_i4.Encoding? encoding,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#post,
|
||||
[url],
|
||||
{#headers: headers, #body: body, #encoding: encoding},
|
||||
),
|
||||
returnValue: _i3.Future<_i2.Response>.value(
|
||||
_FakeResponse_0(
|
||||
this,
|
||||
Invocation.method(
|
||||
#post,
|
||||
[url],
|
||||
{#headers: headers, #body: body, #encoding: encoding},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
as _i3.Future<_i2.Response>);
|
||||
|
||||
@override
|
||||
_i3.Future<_i2.Response> put(
|
||||
Uri? url, {
|
||||
Map<String, String>? headers,
|
||||
Object? body,
|
||||
_i4.Encoding? encoding,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#put,
|
||||
[url],
|
||||
{#headers: headers, #body: body, #encoding: encoding},
|
||||
),
|
||||
returnValue: _i3.Future<_i2.Response>.value(
|
||||
_FakeResponse_0(
|
||||
this,
|
||||
Invocation.method(
|
||||
#put,
|
||||
[url],
|
||||
{#headers: headers, #body: body, #encoding: encoding},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
as _i3.Future<_i2.Response>);
|
||||
|
||||
@override
|
||||
_i3.Future<_i2.Response> patch(
|
||||
Uri? url, {
|
||||
Map<String, String>? headers,
|
||||
Object? body,
|
||||
_i4.Encoding? encoding,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#patch,
|
||||
[url],
|
||||
{#headers: headers, #body: body, #encoding: encoding},
|
||||
),
|
||||
returnValue: _i3.Future<_i2.Response>.value(
|
||||
_FakeResponse_0(
|
||||
this,
|
||||
Invocation.method(
|
||||
#patch,
|
||||
[url],
|
||||
{#headers: headers, #body: body, #encoding: encoding},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
as _i3.Future<_i2.Response>);
|
||||
|
||||
@override
|
||||
_i3.Future<_i2.Response> delete(
|
||||
Uri? url, {
|
||||
Map<String, String>? headers,
|
||||
Object? body,
|
||||
_i4.Encoding? encoding,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#delete,
|
||||
[url],
|
||||
{#headers: headers, #body: body, #encoding: encoding},
|
||||
),
|
||||
returnValue: _i3.Future<_i2.Response>.value(
|
||||
_FakeResponse_0(
|
||||
this,
|
||||
Invocation.method(
|
||||
#delete,
|
||||
[url],
|
||||
{#headers: headers, #body: body, #encoding: encoding},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
as _i3.Future<_i2.Response>);
|
||||
|
||||
@override
|
||||
_i3.Future<String> read(Uri? url, {Map<String, String>? headers}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#read, [url], {#headers: headers}),
|
||||
returnValue: _i3.Future<String>.value(
|
||||
_i5.dummyValue<String>(
|
||||
this,
|
||||
Invocation.method(#read, [url], {#headers: headers}),
|
||||
),
|
||||
),
|
||||
)
|
||||
as _i3.Future<String>);
|
||||
|
||||
@override
|
||||
_i3.Future<_i6.Uint8List> readBytes(
|
||||
Uri? url, {
|
||||
Map<String, String>? headers,
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#readBytes, [url], {#headers: headers}),
|
||||
returnValue: _i3.Future<_i6.Uint8List>.value(_i6.Uint8List(0)),
|
||||
)
|
||||
as _i3.Future<_i6.Uint8List>);
|
||||
|
||||
@override
|
||||
_i3.Future<_i2.StreamedResponse> send(_i2.BaseRequest? request) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#send, [request]),
|
||||
returnValue: _i3.Future<_i2.StreamedResponse>.value(
|
||||
_FakeStreamedResponse_1(
|
||||
this,
|
||||
Invocation.method(#send, [request]),
|
||||
),
|
||||
),
|
||||
)
|
||||
as _i3.Future<_i2.StreamedResponse>);
|
||||
|
||||
@override
|
||||
void close() => super.noSuchMethod(
|
||||
Invocation.method(#close, []),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import 'package:shared_preferences/src/shared_preferences_async.dart' as _i2;
|
||||
// ignore_for_file: unnecessary_parenthesis
|
||||
// ignore_for_file: camel_case_types
|
||||
// ignore_for_file: subtype_of_sealed_class
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
/// A class which mocks [SharedPreferencesAsync].
|
||||
///
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/exceptions/http_exception.dart';
|
||||
import 'package:wger/core/exceptions/http_exception.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/models/workouts/session.dart';
|
||||
import 'package:wger/providers/routines.dart';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (c) 2020, 2025 wger Team
|
||||
* Copyright (c) 2020 - 2026 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -109,12 +109,10 @@ void main() {
|
||||
|
||||
await withClock(Clock.fixed(DateTime(2025, 3, 29, 14, 33)), () async {
|
||||
await tester.pumpWidget(renderGymMode());
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byType(TextButton));
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
//await tester.ensureVisible(find.byKey(Key(key as String)));
|
||||
|
||||
//
|
||||
// Start page
|
||||
//
|
||||
@@ -306,6 +304,7 @@ void main() {
|
||||
expect(find.byIcon(Icons.chevron_right), findsNothing);
|
||||
});
|
||||
},
|
||||
tags: ['golden'],
|
||||
semanticsEnabled: false,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -201,9 +201,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider {
|
||||
as Uri);
|
||||
|
||||
@override
|
||||
_i20.Future<dynamic> fetch(Uri? uri) =>
|
||||
_i20.Future<dynamic> fetch(
|
||||
Uri? uri, {
|
||||
int? maxRetries = 3,
|
||||
Duration? initialDelay = const Duration(milliseconds: 250),
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#fetch, [uri]),
|
||||
Invocation.method(
|
||||
#fetch,
|
||||
[uri],
|
||||
{#maxRetries: maxRetries, #initialDelay: initialDelay},
|
||||
),
|
||||
returnValue: _i20.Future<dynamic>.value(),
|
||||
)
|
||||
as _i20.Future<dynamic>);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (c) 2020, wger Team
|
||||
* Copyright (c) 2020 - 2025 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -56,6 +56,9 @@ void main() {
|
||||
when(mockRoutinesProvider.editSession(any)).thenAnswer(
|
||||
(_) => Future.value(testRoutine.sessions[0].session),
|
||||
);
|
||||
when(mockRoutinesProvider.fetchAndSetRoutineFull(any)).thenAnswer(
|
||||
(_) => Future.value(testRoutine),
|
||||
);
|
||||
});
|
||||
|
||||
Widget renderSessionPage({locale = 'en'}) {
|
||||
@@ -85,6 +88,9 @@ void main() {
|
||||
testWidgets('Test that data from session is loaded', (WidgetTester tester) async {
|
||||
withClock(Clock.fixed(DateTime(2021, 5, 1)), () async {
|
||||
await tester.pumpWidget(renderSessionPage());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
debugDumpApp();
|
||||
expect(find.text('10:00'), findsOneWidget);
|
||||
expect(find.text('12:34'), findsOneWidget);
|
||||
expect(find.text('This is a note'), findsOneWidget);
|
||||
@@ -102,6 +108,7 @@ void main() {
|
||||
|
||||
withClock(Clock.fixed(DateTime(2021, 5, 1)), () async {
|
||||
await tester.pumpWidget(renderSessionPage());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final startTimeField = find.byKey(const ValueKey('time-start'));
|
||||
expect(startTimeField, findsOneWidget);
|
||||
@@ -123,6 +130,7 @@ void main() {
|
||||
|
||||
// Act
|
||||
await tester.pumpWidget(renderSessionPage());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Assert
|
||||
expect(find.text('13:35'), findsOneWidget);
|
||||
@@ -134,11 +142,11 @@ void main() {
|
||||
testWidgets('Test that correct data is send to server', (WidgetTester tester) async {
|
||||
withClock(Clock.fixed(DateTime(2021, 5, 1)), () async {
|
||||
await tester.pumpWidget(renderSessionPage());
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byKey(const ValueKey('save-button')));
|
||||
final captured =
|
||||
verify(mockRoutinesProvider.editSession(captureAny)).captured.single as WorkoutSession;
|
||||
|
||||
print(captured);
|
||||
expect(captured.id, 1);
|
||||
expect(captured.impression, 3);
|
||||
expect(captured.notes, equals('This is a note'));
|
||||
|
||||
@@ -21,7 +21,7 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/exceptions/http_exception.dart';
|
||||
import 'package:wger/core/exceptions/http_exception.dart';
|
||||
import 'package:wger/helpers/consts.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/models/workouts/routine.dart';
|
||||
|
||||
@@ -112,9 +112,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider {
|
||||
as Uri);
|
||||
|
||||
@override
|
||||
_i5.Future<dynamic> fetch(Uri? uri) =>
|
||||
_i5.Future<dynamic> fetch(
|
||||
Uri? uri, {
|
||||
int? maxRetries = 3,
|
||||
Duration? initialDelay = const Duration(milliseconds: 250),
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#fetch, [uri]),
|
||||
Invocation.method(
|
||||
#fetch,
|
||||
[uri],
|
||||
{#maxRetries: maxRetries, #initialDelay: initialDelay},
|
||||
),
|
||||
returnValue: _i5.Future<dynamic>.value(),
|
||||
)
|
||||
as _i5.Future<dynamic>);
|
||||
|
||||
@@ -151,9 +151,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider {
|
||||
as Uri);
|
||||
|
||||
@override
|
||||
_i11.Future<dynamic> fetch(Uri? uri) =>
|
||||
_i11.Future<dynamic> fetch(
|
||||
Uri? uri, {
|
||||
int? maxRetries = 3,
|
||||
Duration? initialDelay = const Duration(milliseconds: 250),
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#fetch, [uri]),
|
||||
Invocation.method(
|
||||
#fetch,
|
||||
[uri],
|
||||
{#maxRetries: maxRetries, #initialDelay: initialDelay},
|
||||
),
|
||||
returnValue: _i11.Future<dynamic>.value(),
|
||||
)
|
||||
as _i11.Future<dynamic>);
|
||||
|
||||
@@ -112,9 +112,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider {
|
||||
as Uri);
|
||||
|
||||
@override
|
||||
_i5.Future<dynamic> fetch(Uri? uri) =>
|
||||
_i5.Future<dynamic> fetch(
|
||||
Uri? uri, {
|
||||
int? maxRetries = 3,
|
||||
Duration? initialDelay = const Duration(milliseconds: 250),
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#fetch, [uri]),
|
||||
Invocation.method(
|
||||
#fetch,
|
||||
[uri],
|
||||
{#maxRetries: maxRetries, #initialDelay: initialDelay},
|
||||
),
|
||||
returnValue: _i5.Future<dynamic>.value(),
|
||||
)
|
||||
as _i5.Future<dynamic>);
|
||||
|
||||
@@ -23,6 +23,7 @@ import 'package:wger/providers/base_provider.dart' as _i4;
|
||||
// ignore_for_file: unnecessary_parenthesis
|
||||
// ignore_for_file: camel_case_types
|
||||
// ignore_for_file: subtype_of_sealed_class
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
class _FakeAuthProvider_0 extends _i1.SmartFake implements _i2.AuthProvider {
|
||||
_FakeAuthProvider_0(Object parent, Invocation parentInvocation) : super(parent, parentInvocation);
|
||||
@@ -65,14 +66,14 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider {
|
||||
as _i3.Client);
|
||||
|
||||
@override
|
||||
set auth(_i2.AuthProvider? _auth) => super.noSuchMethod(
|
||||
Invocation.setter(#auth, _auth),
|
||||
set auth(_i2.AuthProvider? value) => super.noSuchMethod(
|
||||
Invocation.setter(#auth, value),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
set client(_i3.Client? _client) => super.noSuchMethod(
|
||||
Invocation.setter(#client, _client),
|
||||
set client(_i3.Client? value) => super.noSuchMethod(
|
||||
Invocation.setter(#client, value),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@@ -111,9 +112,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider {
|
||||
as Uri);
|
||||
|
||||
@override
|
||||
_i5.Future<dynamic> fetch(Uri? uri) =>
|
||||
_i5.Future<dynamic> fetch(
|
||||
Uri? uri, {
|
||||
int? maxRetries = 3,
|
||||
Duration? initialDelay = const Duration(milliseconds: 250),
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#fetch, [uri]),
|
||||
Invocation.method(
|
||||
#fetch,
|
||||
[uri],
|
||||
{#maxRetries: maxRetries, #initialDelay: initialDelay},
|
||||
),
|
||||
returnValue: _i5.Future<dynamic>.value(),
|
||||
)
|
||||
as _i5.Future<dynamic>);
|
||||
|
||||
@@ -112,9 +112,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider {
|
||||
as Uri);
|
||||
|
||||
@override
|
||||
_i5.Future<dynamic> fetch(Uri? uri) =>
|
||||
_i5.Future<dynamic> fetch(
|
||||
Uri? uri, {
|
||||
int? maxRetries = 3,
|
||||
Duration? initialDelay = const Duration(milliseconds: 250),
|
||||
}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#fetch, [uri]),
|
||||
Invocation.method(
|
||||
#fetch,
|
||||
[uri],
|
||||
{#maxRetries: maxRetries, #initialDelay: initialDelay},
|
||||
),
|
||||
returnValue: _i5.Future<dynamic>.value(),
|
||||
)
|
||||
as _i5.Future<dynamic>);
|
||||
|
||||
241
test/widgets/routines/gym_mode/log_page_test.dart
Normal file
241
test/widgets/routines/gym_mode/log_page_test.dart
Normal file
@@ -0,0 +1,241 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (c) 2025 - 2026 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:provider/provider.dart' as provider;
|
||||
import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart';
|
||||
import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/models/exercises/exercise.dart';
|
||||
import 'package:wger/models/workouts/day_data.dart';
|
||||
import 'package:wger/models/workouts/log.dart';
|
||||
import 'package:wger/models/workouts/set_config_data.dart';
|
||||
import 'package:wger/models/workouts/slot_data.dart';
|
||||
import 'package:wger/providers/exercises.dart';
|
||||
import 'package:wger/providers/gym_state.dart';
|
||||
import 'package:wger/providers/routines.dart';
|
||||
import 'package:wger/widgets/routines/gym_mode/log_page.dart';
|
||||
|
||||
import '../../../../test_data/exercises.dart';
|
||||
import '../../../../test_data/routines.dart' as testdata;
|
||||
import 'log_page_test.mocks.dart';
|
||||
|
||||
@GenerateMocks([ExercisesProvider, RoutinesProvider])
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('LogPage tests', () {
|
||||
late List<Exercise> testExercises;
|
||||
late ProviderContainer container;
|
||||
|
||||
setUp(() {
|
||||
SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty();
|
||||
testExercises = getTestExercises();
|
||||
container = ProviderContainer.test();
|
||||
});
|
||||
|
||||
Future<void> pumpLogPage(WidgetTester tester, {RoutinesProvider? routinesProvider}) async {
|
||||
final providerValue = routinesProvider ?? MockRoutinesProvider();
|
||||
|
||||
await tester.pumpWidget(
|
||||
UncontrolledProviderScope(
|
||||
container: container,
|
||||
child: provider.ChangeNotifierProvider<RoutinesProvider>.value(
|
||||
value: providerValue,
|
||||
child: MaterialApp(
|
||||
locale: const Locale('en'),
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: Scaffold(
|
||||
// Provide a PageView so the PageController used by LogPage is attached
|
||||
body: Builder(
|
||||
builder: (context) {
|
||||
final controller = PageController();
|
||||
return PageView(
|
||||
controller: controller,
|
||||
children: [LogPage(controller)],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
testWidgets('handles null values', (tester) async {
|
||||
// Arrange
|
||||
final notifier = container.read(gymStateProvider.notifier);
|
||||
final routine = testdata.getTestRoutine();
|
||||
routine.dayDataGym = [
|
||||
DayData(
|
||||
iteration: 1,
|
||||
date: DateTime(2024, 11, 01),
|
||||
label: '',
|
||||
day: routine.dayDataGym.first.day,
|
||||
slots: [
|
||||
SlotData(
|
||||
isSuperset: false,
|
||||
exerciseIds: [testExercises[0].id!],
|
||||
setConfigs: [
|
||||
SetConfigData(
|
||||
exerciseId: testExercises[0].id!,
|
||||
exercise: testExercises[0],
|
||||
slotEntryId: 1,
|
||||
nrOfSets: 1,
|
||||
repetitions: null,
|
||||
repetitionsUnit: null,
|
||||
weight: null,
|
||||
weightUnit: null,
|
||||
restTime: 120,
|
||||
rir: 1.5,
|
||||
rpe: 8,
|
||||
textRepr: '3x100kg',
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
notifier.state = notifier.state.copyWith(
|
||||
dayId: routine.days.first.id,
|
||||
routine: routine,
|
||||
iteration: 1,
|
||||
currentPage: 2,
|
||||
);
|
||||
|
||||
// Act
|
||||
notifier.calculatePages();
|
||||
notifier.setCurrentPage(2);
|
||||
|
||||
// Assert
|
||||
expect(notifier.state.getSlotEntryPageByIndex()!.type, SlotPageType.log);
|
||||
await pumpLogPage(tester);
|
||||
expect(find.byType(LogPage), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('renders without crashing for default slotEntryPage', (tester) async {
|
||||
final notifier = container.read(gymStateProvider.notifier);
|
||||
final routine = testdata.getTestRoutine();
|
||||
notifier.state = notifier.state.copyWith(
|
||||
dayId: routine.days.first.id,
|
||||
routine: routine,
|
||||
iteration: 1,
|
||||
);
|
||||
notifier.calculatePages();
|
||||
await pumpLogPage(tester);
|
||||
|
||||
expect(find.byType(LogPage), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('copy from past log updates form fields and shows SnackBar', (tester) async {
|
||||
// Arrange
|
||||
final notifier = container.read(gymStateProvider.notifier);
|
||||
final routine = testdata.getTestRoutine();
|
||||
notifier.state = notifier.state.copyWith(
|
||||
dayId: routine.days.first.id,
|
||||
routine: routine,
|
||||
iteration: 1,
|
||||
);
|
||||
notifier.calculatePages();
|
||||
notifier.setCurrentPage(2);
|
||||
|
||||
// Act
|
||||
// Log page is at index 2
|
||||
notifier.state = notifier.state.copyWith(currentPage: 2);
|
||||
expect(notifier.state.getSlotEntryPageByIndex()!.type, SlotPageType.log);
|
||||
await pumpLogPage(tester);
|
||||
|
||||
// Assert
|
||||
final pastLogTile = find.byKey(const ValueKey('past-log-1'));
|
||||
expect(pastLogTile, findsOneWidget);
|
||||
await tester.tap(pastLogTile);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final editableFields = find.byType(EditableText);
|
||||
expect(editableFields, findsWidgets);
|
||||
|
||||
// Get controller texts
|
||||
final repControllerText = tester.widget<EditableText>(editableFields.at(0)).controller.text;
|
||||
final weightControllerText = tester
|
||||
.widget<EditableText>(editableFields.at(1))
|
||||
.controller
|
||||
.text;
|
||||
|
||||
expect(repControllerText, contains('10'));
|
||||
expect(weightControllerText, contains('10'));
|
||||
expect(find.byType(SnackBar), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('save button calls addLog on RoutinesProvider', (tester) async {
|
||||
// Arrange
|
||||
final notifier = container.read(gymStateProvider.notifier);
|
||||
final routine = testdata.getTestRoutine();
|
||||
notifier.state = notifier.state.copyWith(
|
||||
dayId: routine.days.first.id,
|
||||
routine: routine,
|
||||
iteration: 1,
|
||||
);
|
||||
notifier.calculatePages();
|
||||
notifier.setCurrentPage(2);
|
||||
notifier.state = notifier.state.copyWith(currentPage: 2);
|
||||
final mockRoutines = MockRoutinesProvider();
|
||||
|
||||
// Act
|
||||
await pumpLogPage(tester, routinesProvider: mockRoutines);
|
||||
|
||||
final editableFields = find.byType(EditableText);
|
||||
expect(editableFields, findsWidgets);
|
||||
|
||||
await tester.enterText(editableFields.at(0), '12'); // Reps
|
||||
await tester.enterText(editableFields.at(1), '34'); // Weight
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
Log? capturedLog;
|
||||
when(mockRoutines.addLog(any)).thenAnswer((invocation) async {
|
||||
capturedLog = invocation.positionalArguments[0] as Log;
|
||||
capturedLog!.id = 42;
|
||||
return capturedLog!;
|
||||
});
|
||||
|
||||
final saveButton = find.byKey(const ValueKey('save-log-button'));
|
||||
expect(saveButton, findsOneWidget);
|
||||
|
||||
await tester.tap(saveButton);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Assert
|
||||
verify(mockRoutines.addLog(any)).called(1);
|
||||
expect(capturedLog, isNotNull);
|
||||
expect(capturedLog!.repetitions, equals(12));
|
||||
expect(capturedLog!.weight, equals(34));
|
||||
|
||||
final currentSlotPage = notifier.state.getSlotEntryPageByIndex()!;
|
||||
expect(capturedLog!.slotEntryId, equals(currentSlotPage.setConfigData!.slotEntryId));
|
||||
expect(capturedLog!.routineId, equals(notifier.state.routine.id));
|
||||
expect(capturedLog!.iteration, equals(notifier.state.iteration));
|
||||
});
|
||||
});
|
||||
}
|
||||
1051
test/widgets/routines/gym_mode/log_page_test.mocks.dart
Normal file
1051
test/widgets/routines/gym_mode/log_page_test.mocks.dart
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user