mirror of
https://github.com/wger-project/flutter.git
synced 2026-02-18 00:17:48 +01:00
Merge branch 'master' into feature/day-type
# Conflicts: # lib/models/workouts/day.g.dart # lib/models/workouts/day_data.g.dart # lib/models/workouts/set_config_data.g.dart # lib/models/workouts/slot_data.g.dart # lib/models/workouts/slot_entry.g.dart # lib/models/workouts/weight_unit.g.dart # test/auth/auth_screen_test.mocks.dart # test/core/settings_test.mocks.dart # test/exercises/contribute_exercise_test.mocks.dart # test/gallery/gallery_form_test.mocks.dart # test/gallery/gallery_screen_test.mocks.dart # test/measurements/measurement_categories_screen_test.mocks.dart # test/measurements/measurement_provider_test.mocks.dart # test/nutrition/nutritional_meal_form_test.mocks.dart # test/nutrition/nutritional_plan_form_test.mocks.dart # test/nutrition/nutritional_plan_screen_test.mocks.dart # test/nutrition/nutritional_plans_screen_test.mocks.dart # test/other/base_provider_test.mocks.dart # test/routine/day_form_test.mocks.dart # test/routine/gym_mode_screen_test.mocks.dart # test/routine/gym_mode_session_screen_test.mocks.dart # test/routine/repetition_unit_form_widget_test.mocks.dart # test/routine/routine_edit_screen_test.mocks.dart # test/routine/routine_edit_test.mocks.dart # test/routine/routine_form_test.mocks.dart # test/routine/routine_logs_screen_test.mocks.dart # test/routine/routine_screen_test.mocks.dart # test/routine/routines_provider_test.mocks.dart # test/routine/routines_screen_test.mocks.dart # test/routine/slot_entry_form_test.mocks.dart # test/routine/weight_unit_form_widget_test.mocks.dart # test/user/provider_test.mocks.dart # test/weight/weight_provider_test.mocks.dart # test/weight/weight_screen_test.mocks.dart
This commit is contained in:
2
.github/workflows/bump-version.yml
vendored
2
.github/workflows/bump-version.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
uses: ./.github/actions/flutter-common
|
||||
|
||||
- name: Setup uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
uses: astral-sh/setup-uv@v7
|
||||
|
||||
- name: Validate version
|
||||
run: |
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -30,6 +30,8 @@
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
/**/build/
|
||||
/android/app/.cxx
|
||||
**/failures/*.png
|
||||
|
||||
|
||||
@@ -62,3 +64,4 @@ ios/fastlane/Preview.html
|
||||
ios/fastlane/output
|
||||
ios/Runner.ipa
|
||||
ios/Runner.app.dSYM.zip
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ analyzer:
|
||||
|
||||
formatter:
|
||||
page_width: 100
|
||||
trailing_commas: preserve
|
||||
|
||||
linter:
|
||||
rules:
|
||||
|
||||
@@ -25,23 +25,27 @@ import '../test_data/routines.dart';
|
||||
Widget createDashboardScreen({String locale = 'en'}) {
|
||||
final mockWorkoutProvider = MockRoutinesProvider();
|
||||
when(mockWorkoutProvider.items).thenReturn([getTestRoutine(exercises: getScreenshotExercises())]);
|
||||
when(mockWorkoutProvider.currentRoutine)
|
||||
.thenReturn(getTestRoutine(exercises: getScreenshotExercises()));
|
||||
when(
|
||||
mockWorkoutProvider.currentRoutine,
|
||||
).thenReturn(getTestRoutine(exercises: getScreenshotExercises()));
|
||||
|
||||
when(mockWorkoutProvider.fetchSessionData()).thenAnswer((a) => Future.value([
|
||||
WorkoutSession(
|
||||
routineId: 1,
|
||||
date: DateTime.now().add(const Duration(days: -1)),
|
||||
timeStart: const TimeOfDay(hour: 17, minute: 34),
|
||||
timeEnd: const TimeOfDay(hour: 19, minute: 3),
|
||||
impression: 3,
|
||||
),
|
||||
]));
|
||||
when(mockWorkoutProvider.fetchSessionData()).thenAnswer(
|
||||
(a) => Future.value([
|
||||
WorkoutSession(
|
||||
routineId: 1,
|
||||
date: DateTime.now().add(const Duration(days: -1)),
|
||||
timeStart: const TimeOfDay(hour: 17, minute: 34),
|
||||
timeEnd: const TimeOfDay(hour: 19, minute: 3),
|
||||
impression: 3,
|
||||
),
|
||||
]),
|
||||
);
|
||||
|
||||
final mockNutritionProvider = weight.MockNutritionPlansProvider();
|
||||
|
||||
when(mockNutritionProvider.currentPlan)
|
||||
.thenAnswer((realInvocation) => getNutritionalPlanScreenshot());
|
||||
when(
|
||||
mockNutritionProvider.currentPlan,
|
||||
).thenAnswer((realInvocation) => getNutritionalPlanScreenshot());
|
||||
when(mockNutritionProvider.items).thenReturn([getNutritionalPlanScreenshot()]);
|
||||
|
||||
final mockWeightProvider = weight.MockBodyWeightProvider();
|
||||
|
||||
@@ -17,7 +17,7 @@ enum DeviceType {
|
||||
sevenInchScreenshots,
|
||||
tenInchScreenshots,
|
||||
tvScreenshots,
|
||||
wearScreenshots
|
||||
wearScreenshots,
|
||||
}
|
||||
|
||||
final destination = DeviceType.phoneScreenshots.name;
|
||||
@@ -42,7 +42,6 @@ Future<void> takeScreenshot(
|
||||
const languages = [
|
||||
// Note: it seems if too many languages are processed at once, some processes
|
||||
// disappear and no images are written. Doing this in smaller steps works fine
|
||||
|
||||
'ar',
|
||||
'ca',
|
||||
'cs-CZ',
|
||||
|
||||
29
lib/core/validators.dart
Normal file
29
lib/core/validators.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
|
||||
String? validateUrl(String? value, AppLocalizations i18n, {bool required = true}) {
|
||||
// Required?
|
||||
if (required && (value == null || value.trim().isEmpty)) {
|
||||
return i18n.enterValue;
|
||||
}
|
||||
|
||||
if (!required && (value == null || value.trim().isEmpty)) {
|
||||
return null;
|
||||
}
|
||||
value = value!.trim();
|
||||
|
||||
if (!value.startsWith('http://') && !value.startsWith('https://')) {
|
||||
return i18n.invalidUrl;
|
||||
}
|
||||
|
||||
// Try to parse as URI
|
||||
try {
|
||||
final uri = Uri.parse(value);
|
||||
if (!uri.hasScheme || !uri.hasAuthority) {
|
||||
return i18n.invalidUrl;
|
||||
}
|
||||
} catch (e) {
|
||||
return i18n.invalidUrl;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -85,20 +85,20 @@ class ExerciseDatabase extends _$ExerciseDatabase {
|
||||
/// will fetch everything as needed from the server
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
onUpgrade: (m, from, to) async {
|
||||
// no-op, but needs to be defined
|
||||
return;
|
||||
},
|
||||
beforeOpen: (openingDetails) async {
|
||||
if (openingDetails.hadUpgrade) {
|
||||
final m = createMigrator();
|
||||
for (final table in allTables) {
|
||||
await m.deleteTable(table.actualTableName);
|
||||
await m.createTable(table);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
onUpgrade: (m, from, to) async {
|
||||
// no-op, but needs to be defined
|
||||
return;
|
||||
},
|
||||
beforeOpen: (openingDetails) async {
|
||||
if (openingDetails.hadUpgrade) {
|
||||
final m = createMigrator();
|
||||
for (final table in allTables) {
|
||||
await m.deleteTable(table.actualTableName);
|
||||
await m.createTable(table);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
Future<void> deleteEverything() {
|
||||
return transaction(() async {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -38,20 +38,20 @@ class IngredientDatabase extends _$IngredientDatabase {
|
||||
/// will fetch everything as needed from the server
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
onUpgrade: (m, from, to) async {
|
||||
// no-op, but needs to be defined
|
||||
return;
|
||||
},
|
||||
beforeOpen: (openingDetails) async {
|
||||
if (openingDetails.hadUpgrade) {
|
||||
final m = createMigrator();
|
||||
for (final table in allTables) {
|
||||
await m.deleteTable(table.actualTableName);
|
||||
await m.createTable(table);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
onUpgrade: (m, from, to) async {
|
||||
// no-op, but needs to be defined
|
||||
return;
|
||||
},
|
||||
beforeOpen: (openingDetails) async {
|
||||
if (openingDetails.hadUpgrade) {
|
||||
final m = createMigrator();
|
||||
for (final table in allTables) {
|
||||
await m.deleteTable(table.actualTableName);
|
||||
await m.createTable(table);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
Future<void> deleteEverything() {
|
||||
return transaction(() async {
|
||||
|
||||
@@ -7,30 +7,52 @@ class $IngredientsTable extends Ingredients with TableInfo<$IngredientsTable, In
|
||||
@override
|
||||
final GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
|
||||
$IngredientsTable(this.attachedDatabase, [this._alias]);
|
||||
|
||||
static const VerificationMeta _idMeta = const VerificationMeta('id');
|
||||
@override
|
||||
late final GeneratedColumn<int> id = GeneratedColumn<int>('id', aliasedName, false,
|
||||
type: DriftSqlType.int, requiredDuringInsert: true);
|
||||
late final GeneratedColumn<int> id = GeneratedColumn<int>(
|
||||
'id',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.int,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
static const VerificationMeta _dataMeta = const VerificationMeta('data');
|
||||
@override
|
||||
late final GeneratedColumn<String> data = GeneratedColumn<String>('data', aliasedName, false,
|
||||
type: DriftSqlType.string, requiredDuringInsert: true);
|
||||
late final GeneratedColumn<String> data = GeneratedColumn<String>(
|
||||
'data',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
static const VerificationMeta _lastFetchedMeta = const VerificationMeta('lastFetched');
|
||||
@override
|
||||
late final GeneratedColumn<DateTime> lastFetched = GeneratedColumn<DateTime>(
|
||||
'last_fetched', aliasedName, false,
|
||||
type: DriftSqlType.dateTime, requiredDuringInsert: true);
|
||||
'last_fetched',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.dateTime,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => [id, data, lastFetched];
|
||||
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
|
||||
@override
|
||||
String get actualTableName => $name;
|
||||
static const String $name = 'ingredients';
|
||||
|
||||
@override
|
||||
VerificationContext validateIntegrity(Insertable<IngredientTable> instance,
|
||||
{bool isInserting = false}) {
|
||||
VerificationContext validateIntegrity(
|
||||
Insertable<IngredientTable> instance, {
|
||||
bool isInserting = false,
|
||||
}) {
|
||||
final context = VerificationContext();
|
||||
final data = instance.toColumns(true);
|
||||
if (data.containsKey('id')) {
|
||||
@@ -44,8 +66,10 @@ class $IngredientsTable extends Ingredients with TableInfo<$IngredientsTable, In
|
||||
context.missing(_dataMeta);
|
||||
}
|
||||
if (data.containsKey('last_fetched')) {
|
||||
context.handle(_lastFetchedMeta,
|
||||
lastFetched.isAcceptableOrUnknown(data['last_fetched']!, _lastFetchedMeta));
|
||||
context.handle(
|
||||
_lastFetchedMeta,
|
||||
lastFetched.isAcceptableOrUnknown(data['last_fetched']!, _lastFetchedMeta),
|
||||
);
|
||||
} else if (isInserting) {
|
||||
context.missing(_lastFetchedMeta);
|
||||
}
|
||||
@@ -54,14 +78,17 @@ class $IngredientsTable extends Ingredients with TableInfo<$IngredientsTable, In
|
||||
|
||||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => const {};
|
||||
|
||||
@override
|
||||
IngredientTable map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return IngredientTable(
|
||||
id: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}id'])!,
|
||||
data: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}data'])!,
|
||||
lastFetched: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.dateTime, data['${effectivePrefix}last_fetched'])!,
|
||||
lastFetched: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.dateTime,
|
||||
data['${effectivePrefix}last_fetched'],
|
||||
)!,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -77,7 +104,9 @@ class IngredientTable extends DataClass implements Insertable<IngredientTable> {
|
||||
|
||||
/// The date when the ingredient was last fetched from the server
|
||||
final DateTime lastFetched;
|
||||
|
||||
const IngredientTable({required this.id, required this.data, required this.lastFetched});
|
||||
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
@@ -88,11 +117,7 @@ class IngredientTable extends DataClass implements Insertable<IngredientTable> {
|
||||
}
|
||||
|
||||
IngredientsCompanion toCompanion(bool nullToAbsent) {
|
||||
return IngredientsCompanion(
|
||||
id: Value(id),
|
||||
data: Value(data),
|
||||
lastFetched: Value(lastFetched),
|
||||
);
|
||||
return IngredientsCompanion(id: Value(id), data: Value(data), lastFetched: Value(lastFetched));
|
||||
}
|
||||
|
||||
factory IngredientTable.fromJson(Map<String, dynamic> json, {ValueSerializer? serializer}) {
|
||||
@@ -103,6 +128,7 @@ class IngredientTable extends DataClass implements Insertable<IngredientTable> {
|
||||
lastFetched: serializer.fromJson<DateTime>(json['lastFetched']),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
@@ -114,10 +140,11 @@ class IngredientTable extends DataClass implements Insertable<IngredientTable> {
|
||||
}
|
||||
|
||||
IngredientTable copyWith({int? id, String? data, DateTime? lastFetched}) => IngredientTable(
|
||||
id: id ?? this.id,
|
||||
data: data ?? this.data,
|
||||
lastFetched: lastFetched ?? this.lastFetched,
|
||||
);
|
||||
id: id ?? this.id,
|
||||
data: data ?? this.data,
|
||||
lastFetched: lastFetched ?? this.lastFetched,
|
||||
);
|
||||
|
||||
IngredientTable copyWithCompanion(IngredientsCompanion data) {
|
||||
return IngredientTable(
|
||||
id: data.id.present ? data.id.value : this.id,
|
||||
@@ -138,6 +165,7 @@ class IngredientTable extends DataClass implements Insertable<IngredientTable> {
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(id, data, lastFetched);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
@@ -152,20 +180,23 @@ class IngredientsCompanion extends UpdateCompanion<IngredientTable> {
|
||||
final Value<String> data;
|
||||
final Value<DateTime> lastFetched;
|
||||
final Value<int> rowid;
|
||||
|
||||
const IngredientsCompanion({
|
||||
this.id = const Value.absent(),
|
||||
this.data = const Value.absent(),
|
||||
this.lastFetched = const Value.absent(),
|
||||
this.rowid = const Value.absent(),
|
||||
});
|
||||
|
||||
IngredientsCompanion.insert({
|
||||
required int id,
|
||||
required String data,
|
||||
required DateTime lastFetched,
|
||||
this.rowid = const Value.absent(),
|
||||
}) : id = Value(id),
|
||||
data = Value(data),
|
||||
lastFetched = Value(lastFetched);
|
||||
}) : id = Value(id),
|
||||
data = Value(data),
|
||||
lastFetched = Value(lastFetched);
|
||||
|
||||
static Insertable<IngredientTable> custom({
|
||||
Expression<int>? id,
|
||||
Expression<String>? data,
|
||||
@@ -180,8 +211,12 @@ class IngredientsCompanion extends UpdateCompanion<IngredientTable> {
|
||||
});
|
||||
}
|
||||
|
||||
IngredientsCompanion copyWith(
|
||||
{Value<int>? id, Value<String>? data, Value<DateTime>? lastFetched, Value<int>? rowid}) {
|
||||
IngredientsCompanion copyWith({
|
||||
Value<int>? id,
|
||||
Value<String>? data,
|
||||
Value<DateTime>? lastFetched,
|
||||
Value<int>? rowid,
|
||||
}) {
|
||||
return IngredientsCompanion(
|
||||
id: id ?? this.id,
|
||||
data: data ?? this.data,
|
||||
@@ -222,27 +257,32 @@ class IngredientsCompanion extends UpdateCompanion<IngredientTable> {
|
||||
|
||||
abstract class _$IngredientDatabase extends GeneratedDatabase {
|
||||
_$IngredientDatabase(QueryExecutor e) : super(e);
|
||||
|
||||
$IngredientDatabaseManager get managers => $IngredientDatabaseManager(this);
|
||||
late final $IngredientsTable ingredients = $IngredientsTable(this);
|
||||
|
||||
@override
|
||||
Iterable<TableInfo<Table, Object?>> get allTables =>
|
||||
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
||||
|
||||
@override
|
||||
List<DatabaseSchemaEntity> get allSchemaEntities => [ingredients];
|
||||
}
|
||||
|
||||
typedef $$IngredientsTableCreateCompanionBuilder = IngredientsCompanion Function({
|
||||
required int id,
|
||||
required String data,
|
||||
required DateTime lastFetched,
|
||||
Value<int> rowid,
|
||||
});
|
||||
typedef $$IngredientsTableUpdateCompanionBuilder = IngredientsCompanion Function({
|
||||
Value<int> id,
|
||||
Value<String> data,
|
||||
Value<DateTime> lastFetched,
|
||||
Value<int> rowid,
|
||||
});
|
||||
typedef $$IngredientsTableCreateCompanionBuilder =
|
||||
IngredientsCompanion Function({
|
||||
required int id,
|
||||
required String data,
|
||||
required DateTime lastFetched,
|
||||
Value<int> rowid,
|
||||
});
|
||||
typedef $$IngredientsTableUpdateCompanionBuilder =
|
||||
IngredientsCompanion Function({
|
||||
Value<int> id,
|
||||
Value<String> data,
|
||||
Value<DateTime> lastFetched,
|
||||
Value<int> rowid,
|
||||
});
|
||||
|
||||
class $$IngredientsTableFilterComposer extends Composer<_$IngredientDatabase, $IngredientsTable> {
|
||||
$$IngredientsTableFilterComposer({
|
||||
@@ -252,6 +292,7 @@ class $$IngredientsTableFilterComposer extends Composer<_$IngredientDatabase, $I
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
|
||||
ColumnFilters<int> get id =>
|
||||
$composableBuilder(column: $table.id, builder: (column) => ColumnFilters(column));
|
||||
|
||||
@@ -270,6 +311,7 @@ class $$IngredientsTableOrderingComposer extends Composer<_$IngredientDatabase,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
|
||||
ColumnOrderings<int> get id =>
|
||||
$composableBuilder(column: $table.id, builder: (column) => ColumnOrderings(column));
|
||||
|
||||
@@ -289,6 +331,7 @@ class $$IngredientsTableAnnotationComposer
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
|
||||
GeneratedColumn<int> get id => $composableBuilder(column: $table.id, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<String> get data =>
|
||||
@@ -298,72 +341,80 @@ class $$IngredientsTableAnnotationComposer
|
||||
$composableBuilder(column: $table.lastFetched, builder: (column) => column);
|
||||
}
|
||||
|
||||
class $$IngredientsTableTableManager extends RootTableManager<
|
||||
_$IngredientDatabase,
|
||||
$IngredientsTable,
|
||||
IngredientTable,
|
||||
$$IngredientsTableFilterComposer,
|
||||
$$IngredientsTableOrderingComposer,
|
||||
$$IngredientsTableAnnotationComposer,
|
||||
$$IngredientsTableCreateCompanionBuilder,
|
||||
$$IngredientsTableUpdateCompanionBuilder,
|
||||
(IngredientTable, BaseReferences<_$IngredientDatabase, $IngredientsTable, IngredientTable>),
|
||||
IngredientTable,
|
||||
PrefetchHooks Function()> {
|
||||
class $$IngredientsTableTableManager
|
||||
extends
|
||||
RootTableManager<
|
||||
_$IngredientDatabase,
|
||||
$IngredientsTable,
|
||||
IngredientTable,
|
||||
$$IngredientsTableFilterComposer,
|
||||
$$IngredientsTableOrderingComposer,
|
||||
$$IngredientsTableAnnotationComposer,
|
||||
$$IngredientsTableCreateCompanionBuilder,
|
||||
$$IngredientsTableUpdateCompanionBuilder,
|
||||
(
|
||||
IngredientTable,
|
||||
BaseReferences<_$IngredientDatabase, $IngredientsTable, IngredientTable>,
|
||||
),
|
||||
IngredientTable,
|
||||
PrefetchHooks Function()
|
||||
> {
|
||||
$$IngredientsTableTableManager(_$IngredientDatabase db, $IngredientsTable table)
|
||||
: super(TableManagerState(
|
||||
: super(
|
||||
TableManagerState(
|
||||
db: db,
|
||||
table: table,
|
||||
createFilteringComposer: () => $$IngredientsTableFilterComposer($db: db, $table: table),
|
||||
createOrderingComposer: () => $$IngredientsTableOrderingComposer($db: db, $table: table),
|
||||
createComputedFieldComposer: () =>
|
||||
$$IngredientsTableAnnotationComposer($db: db, $table: table),
|
||||
updateCompanionCallback: ({
|
||||
Value<int> id = const Value.absent(),
|
||||
Value<String> data = const Value.absent(),
|
||||
Value<DateTime> lastFetched = const Value.absent(),
|
||||
Value<int> rowid = const Value.absent(),
|
||||
}) =>
|
||||
IngredientsCompanion(
|
||||
id: id,
|
||||
data: data,
|
||||
lastFetched: lastFetched,
|
||||
rowid: rowid,
|
||||
),
|
||||
createCompanionCallback: ({
|
||||
required int id,
|
||||
required String data,
|
||||
required DateTime lastFetched,
|
||||
Value<int> rowid = const Value.absent(),
|
||||
}) =>
|
||||
IngredientsCompanion.insert(
|
||||
id: id,
|
||||
data: data,
|
||||
lastFetched: lastFetched,
|
||||
rowid: rowid,
|
||||
),
|
||||
updateCompanionCallback:
|
||||
({
|
||||
Value<int> id = const Value.absent(),
|
||||
Value<String> data = const Value.absent(),
|
||||
Value<DateTime> lastFetched = const Value.absent(),
|
||||
Value<int> rowid = const Value.absent(),
|
||||
}) =>
|
||||
IngredientsCompanion(id: id, data: data, lastFetched: lastFetched, rowid: rowid),
|
||||
createCompanionCallback:
|
||||
({
|
||||
required int id,
|
||||
required String data,
|
||||
required DateTime lastFetched,
|
||||
Value<int> rowid = const Value.absent(),
|
||||
}) => IngredientsCompanion.insert(
|
||||
id: id,
|
||||
data: data,
|
||||
lastFetched: lastFetched,
|
||||
rowid: rowid,
|
||||
),
|
||||
withReferenceMapper: (p0) =>
|
||||
p0.map((e) => (e.readTable(table), BaseReferences(db, table, e))).toList(),
|
||||
prefetchHooksCallback: null,
|
||||
));
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
typedef $$IngredientsTableProcessedTableManager = ProcessedTableManager<
|
||||
_$IngredientDatabase,
|
||||
$IngredientsTable,
|
||||
IngredientTable,
|
||||
$$IngredientsTableFilterComposer,
|
||||
$$IngredientsTableOrderingComposer,
|
||||
$$IngredientsTableAnnotationComposer,
|
||||
$$IngredientsTableCreateCompanionBuilder,
|
||||
$$IngredientsTableUpdateCompanionBuilder,
|
||||
(IngredientTable, BaseReferences<_$IngredientDatabase, $IngredientsTable, IngredientTable>),
|
||||
IngredientTable,
|
||||
PrefetchHooks Function()>;
|
||||
typedef $$IngredientsTableProcessedTableManager =
|
||||
ProcessedTableManager<
|
||||
_$IngredientDatabase,
|
||||
$IngredientsTable,
|
||||
IngredientTable,
|
||||
$$IngredientsTableFilterComposer,
|
||||
$$IngredientsTableOrderingComposer,
|
||||
$$IngredientsTableAnnotationComposer,
|
||||
$$IngredientsTableCreateCompanionBuilder,
|
||||
$$IngredientsTableUpdateCompanionBuilder,
|
||||
(IngredientTable, BaseReferences<_$IngredientDatabase, $IngredientsTable, IngredientTable>),
|
||||
IngredientTable,
|
||||
PrefetchHooks Function()
|
||||
>;
|
||||
|
||||
class $IngredientDatabaseManager {
|
||||
final _$IngredientDatabase _db;
|
||||
|
||||
$IngredientDatabaseManager(this._db);
|
||||
|
||||
$$IngredientsTableTableManager get ingredients =>
|
||||
$$IngredientsTableTableManager(_db, _db.ingredients);
|
||||
}
|
||||
|
||||
@@ -143,3 +143,6 @@ const String API_RESULTS_PAGE_SIZE = '100';
|
||||
/// Marker used for identifying interpolated values in a list, e.g. for measurements
|
||||
/// the milliseconds in the entry date are set to this value
|
||||
const INTERPOLATION_MARKER = 123;
|
||||
|
||||
/// Creative Commons license IDs
|
||||
const CC_BY_SA_4_ID = 2;
|
||||
|
||||
@@ -101,7 +101,8 @@ void showGeneralErrorDialog(dynamic error, StackTrace? stackTrace, {BuildContext
|
||||
|
||||
if (error is TimeoutException) {
|
||||
issueTitle = 'Network Timeout';
|
||||
issueErrorMessage = 'The connection to the server timed out. Please check your '
|
||||
issueErrorMessage =
|
||||
'The connection to the server timed out. Please check your '
|
||||
'internet connection and try again.';
|
||||
} else if (error is FlutterErrorDetails) {
|
||||
issueTitle = 'Application Error';
|
||||
@@ -116,7 +117,7 @@ void showGeneralErrorDialog(dynamic error, StackTrace? stackTrace, {BuildContext
|
||||
}
|
||||
|
||||
final String fullStackTrace = stackTrace?.toString() ?? 'No stack trace available.';
|
||||
final applicationLogs = InMemoryLogStore().formattedLogs;
|
||||
final applicationLogs = InMemoryLogStore().getFormattedLogs();
|
||||
|
||||
showDialog(
|
||||
context: dialogContext,
|
||||
@@ -127,15 +128,9 @@ void showGeneralErrorDialog(dynamic error, StackTrace? stackTrace, {BuildContext
|
||||
spacing: 8,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
Icon(icon, color: Theme.of(context).colorScheme.error),
|
||||
Expanded(
|
||||
child: Text(
|
||||
errorTitle,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
child: Text(errorTitle, style: TextStyle(color: Theme.of(context).colorScheme.error)),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -150,10 +145,7 @@ 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),
|
||||
@@ -166,15 +158,13 @@ void showGeneralErrorDialog(dynamic error, StackTrace? stackTrace, {BuildContext
|
||||
),
|
||||
),
|
||||
CopyToClipboardButton(
|
||||
text: 'Error Title: $issueTitle\n'
|
||||
text:
|
||||
'Error Title: $issueTitle\n'
|
||||
'Error Message: $issueErrorMessage\n\n'
|
||||
'Stack Trace:\n$fullStackTrace',
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
i18n.applicationLogs,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(i18n.applicationLogs, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
Container(
|
||||
alignment: Alignment.topLeft,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
@@ -182,10 +172,12 @@ void showGeneralErrorDialog(dynamic error, StackTrace? stackTrace, {BuildContext
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
...applicationLogs.map((entry) => Text(
|
||||
entry,
|
||||
style: TextStyle(fontSize: 12.0, color: Colors.grey[700]),
|
||||
))
|
||||
...applicationLogs.map(
|
||||
(entry) => Text(
|
||||
entry,
|
||||
style: TextStyle(fontSize: 12.0, color: Colors.grey[700]),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -215,7 +207,8 @@ void showGeneralErrorDialog(dynamic error, StackTrace? stackTrace, {BuildContext
|
||||
'App logs (last ${applicationLogs.length} entries):\n'
|
||||
'```\n$logText\n```',
|
||||
);
|
||||
final githubIssueUrl = '$GITHUB_ISSUES_BUG_URL'
|
||||
final githubIssueUrl =
|
||||
'$GITHUB_ISSUES_BUG_URL'
|
||||
'&title=$issueTitle'
|
||||
'&description=$description';
|
||||
final Uri reportUri = Uri.parse(githubIssueUrl);
|
||||
@@ -226,9 +219,9 @@ void showGeneralErrorDialog(dynamic error, StackTrace? stackTrace, {BuildContext
|
||||
if (kDebugMode) {
|
||||
logger.warning('Error launching URL: $e');
|
||||
}
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Error opening issue tracker: $e')),
|
||||
);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Error opening issue tracker: $e')));
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -248,10 +241,7 @@ class CopyToClipboardButton extends StatelessWidget {
|
||||
final logger = Logger('CopyToClipboardButton');
|
||||
final String text;
|
||||
|
||||
CopyToClipboardButton({
|
||||
required this.text,
|
||||
super.key,
|
||||
});
|
||||
CopyToClipboardButton({required this.text, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -265,21 +255,23 @@ class CopyToClipboardButton extends StatelessWidget {
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: text)).then((_) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Details copied to clipboard!')),
|
||||
);
|
||||
}
|
||||
}).catchError((copyError) {
|
||||
logger.warning('Error copying to clipboard: $copyError');
|
||||
Clipboard.setData(ClipboardData(text: text))
|
||||
.then((_) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('Details copied to clipboard!')));
|
||||
}
|
||||
})
|
||||
.catchError((copyError) {
|
||||
logger.warning('Error copying to clipboard: $copyError');
|
||||
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Could not copy details.')),
|
||||
);
|
||||
}
|
||||
});
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('Could not copy details.')));
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -290,9 +282,7 @@ void showDeleteDialog(BuildContext context, String confirmDeleteName, Log log) a
|
||||
context: context,
|
||||
builder: (BuildContext contextDialog) {
|
||||
return AlertDialog(
|
||||
content: Text(
|
||||
AppLocalizations.of(context).confirmDelete(confirmDeleteName),
|
||||
),
|
||||
content: Text(AppLocalizations.of(context).confirmDelete(confirmDeleteName)),
|
||||
actions: [
|
||||
TextButton(
|
||||
key: const ValueKey('cancel-button'),
|
||||
@@ -387,7 +377,10 @@ List<Widget> formatApiErrors(List<ApiError> errors, {Color? color}) {
|
||||
|
||||
for (final error in errors) {
|
||||
errorList.add(
|
||||
Text(error.key, style: TextStyle(fontWeight: FontWeight.bold, color: textColor)),
|
||||
Text(
|
||||
error.key,
|
||||
style: TextStyle(fontWeight: FontWeight.bold, color: textColor),
|
||||
),
|
||||
);
|
||||
|
||||
print(error.errorMessages);
|
||||
@@ -408,7 +401,10 @@ List<Widget> formatTextErrors(List<String> errors, {String? title, Color? color}
|
||||
|
||||
if (title != null) {
|
||||
errorList.add(
|
||||
Text(title, style: TextStyle(fontWeight: FontWeight.bold, color: textColor)),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(fontWeight: FontWeight.bold, color: textColor),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -458,11 +454,7 @@ class GeneralErrorsWidget extends StatelessWidget {
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(Icons.error_outline, color: Theme.of(context).colorScheme.error),
|
||||
...formatTextErrors(
|
||||
widgets,
|
||||
title: title,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
...formatTextErrors(widgets, title: title, color: Theme.of(context).colorScheme.error),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -14,10 +14,17 @@ String? validateName(String? name, BuildContext context) {
|
||||
}
|
||||
|
||||
if (name.length < MIN_CHARS_NAME || name.length > MAX_CHARS_NAME) {
|
||||
return AppLocalizations.of(context).enterCharacters(
|
||||
MIN_CHARS_NAME.toString(),
|
||||
MAX_CHARS_NAME.toString(),
|
||||
);
|
||||
return AppLocalizations.of(
|
||||
context,
|
||||
).enterCharacters(MIN_CHARS_NAME.toString(), MAX_CHARS_NAME.toString());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
String? validateAuthorName(String? name, BuildContext context) {
|
||||
if (name!.isEmpty) {
|
||||
return AppLocalizations.of(context).enterValue;
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -41,10 +41,16 @@ class InMemoryLogStore {
|
||||
|
||||
List<LogRecord> get logs => List.unmodifiable(_logs);
|
||||
|
||||
List<String> get formattedLogs => _logs
|
||||
.map((log) =>
|
||||
'${log.time.toIso8601String()} ${log.level.name} [${log.loggerName}] ${log.message}')
|
||||
.toList();
|
||||
List<String> getFormattedLogs({Level? minLevel}) {
|
||||
final level = minLevel ?? Logger.root.level;
|
||||
return _logs
|
||||
.where((log) => log.level >= level)
|
||||
.map(
|
||||
(log) =>
|
||||
'${log.time.toIso8601String()} ${log.level.name} [${log.loggerName}] ${log.message}',
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
void clear() => _logs.clear();
|
||||
}
|
||||
|
||||
@@ -25,11 +25,11 @@ extension MeasurementChartEntryListExtensions on List<MeasurementChartEntry> {
|
||||
return where((e) => e.date.isAfter(start) && (end == null || e.date.isBefore(end))).toList();
|
||||
}
|
||||
|
||||
// assures values on the start (and optionally end) dates exist, by interpolating if needed
|
||||
// this is used for when you are looking at a specific time frame (e.g. for a nutrition plan)
|
||||
// while gaps in the middle of a chart can be "visually interpolated", it's good to have a clearer
|
||||
// explicit interpolation for the start and end dates (if needed)
|
||||
// this also helps with computing delta's across the entire window
|
||||
// assures values on the start (and optionally end) dates exist, by interpolating if needed
|
||||
// this is used for when you are looking at a specific time frame (e.g. for a nutrition plan)
|
||||
// while gaps in the middle of a chart can be "visually interpolated", it's good to have a clearer
|
||||
// explicit interpolation for the start and end dates (if needed)
|
||||
// this also helps with computing delta's across the entire window
|
||||
List<MeasurementChartEntry> whereDateWithInterpolation(DateTime start, DateTime? end) {
|
||||
// Make sure our list is sorted by date
|
||||
sort((a, b) => a.date.compareTo(b.date));
|
||||
@@ -90,7 +90,10 @@ extension MeasurementChartEntryListExtensions on List<MeasurementChartEntry> {
|
||||
|
||||
// caller needs to make sure that before.date < date < after.date
|
||||
MeasurementChartEntry interpolateBetween(
|
||||
MeasurementChartEntry before, MeasurementChartEntry after, DateTime date) {
|
||||
MeasurementChartEntry before,
|
||||
MeasurementChartEntry after,
|
||||
DateTime date,
|
||||
) {
|
||||
final totalDuration = after.date.difference(before.date).inMilliseconds;
|
||||
final startDuration = date.difference(before.date).inMilliseconds;
|
||||
|
||||
|
||||
@@ -744,6 +744,52 @@
|
||||
"identicalExercisePleaseDiscard": "If you notice an exercise that is identical to the one you're adding, please discard your draft and edit that exercise instead.",
|
||||
"checkInformationBeforeSubmitting": "Please check that the information you entered is correct before submitting the exercise",
|
||||
"add_exercise_image_license": "Images must be compatible with the CC BY SA license. If in doubt, upload only photos you've taken yourself.",
|
||||
"imageDetailsTitle": "Image details",
|
||||
"@imageDetailsTitle": {
|
||||
"description": "Title for image details form"
|
||||
},
|
||||
"imageDetailsLicenseTitle": "Title",
|
||||
"@imageDetailsLicenseTitle": {
|
||||
"description": "Label for image title field"
|
||||
},
|
||||
"imageDetailsLicenseTitleHint": "Enter image title",
|
||||
"@imageDetailsLicenseTitleHint": {
|
||||
"description": "Hint text for image title field"
|
||||
},
|
||||
"imageDetailsSourceLink": "Link to the source website",
|
||||
"@imageDetailsSourceLink": {
|
||||
"description": "Label for source link field"
|
||||
},
|
||||
"author": "Author(s)",
|
||||
"@Author": {
|
||||
"description": "Label for author field"
|
||||
},
|
||||
"authorHint": "Enter author name",
|
||||
"@authorHint": {
|
||||
"description": "Hint text for author field"
|
||||
},
|
||||
"imageDetailsAuthorLink": "Link to author website or profile",
|
||||
"@imageDetailsAuthorLink": {
|
||||
"description": "Label for author link field"
|
||||
},
|
||||
"imageDetailsDerivativeSource": "Link to the original source, if this is a derivative work",
|
||||
"@imageDetailsDerivativeSource": {
|
||||
"description": "Label for derivative source field"
|
||||
},
|
||||
"imageDetailsDerivativeHelp": "A derivative work is based on a previous work but contains sufficient new, creative content to entitle it to its own copyright.",
|
||||
"@imageDetailsDerivativeHelp": {
|
||||
"description": "Helper text explaining derivative works"
|
||||
},
|
||||
"imageDetailsImageType": "Image Type",
|
||||
"@imageDetailsImageType": {
|
||||
"description": "Label for image type selector"
|
||||
},
|
||||
"imageDetailsLicenseNotice": "By submitting this image, you agree to release it under the CC-BY-SA-4. The image must be either your own work or the author must have released it under a license compatible with it.",
|
||||
"imageDetailsLicenseNoticeLinkToLicense": "See license text.",
|
||||
"add": "add",
|
||||
"@add": {
|
||||
"description": "Add button text"
|
||||
},
|
||||
"variations": "Variations",
|
||||
"@variations": {
|
||||
"description": "Variations of one exercise (e.g. benchpress and benchpress narrow)"
|
||||
|
||||
@@ -146,8 +146,8 @@ class MainApp extends StatelessWidget {
|
||||
future: auth.tryAutoLogin(),
|
||||
builder: (ctx, authResultSnapshot) =>
|
||||
authResultSnapshot.connectionState == ConnectionState.waiting
|
||||
? const SplashScreen()
|
||||
: const AuthScreen(),
|
||||
? const SplashScreen()
|
||||
: const AuthScreen(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,10 +41,10 @@ class WeightEntry {
|
||||
}
|
||||
|
||||
WeightEntry copyWith({int? id, int? weight, DateTime? date}) => WeightEntry(
|
||||
id: id,
|
||||
weight: weight ?? this.weight,
|
||||
date: date ?? this.date,
|
||||
);
|
||||
id: id,
|
||||
weight: weight ?? this.weight,
|
||||
date: date ?? this.date,
|
||||
);
|
||||
|
||||
// Boilerplate
|
||||
factory WeightEntry.fromJson(Map<String, dynamic> json) => _$WeightEntryFromJson(json);
|
||||
|
||||
@@ -7,10 +7,7 @@ part of 'weight_entry.dart';
|
||||
// **************************************************************************
|
||||
|
||||
WeightEntry _$WeightEntryFromJson(Map<String, dynamic> json) {
|
||||
$checkKeys(
|
||||
json,
|
||||
requiredKeys: const ['id', 'weight', 'date'],
|
||||
);
|
||||
$checkKeys(json, requiredKeys: const ['id', 'weight', 'date']);
|
||||
return WeightEntry(
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
weight: stringToNum(json['weight'] as String?),
|
||||
@@ -19,7 +16,7 @@ WeightEntry _$WeightEntryFromJson(Map<String, dynamic> json) {
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$WeightEntryToJson(WeightEntry instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'weight': numToString(instance.weight),
|
||||
'date': instance.date.toIso8601String(),
|
||||
};
|
||||
'id': instance.id,
|
||||
'weight': numToString(instance.weight),
|
||||
'date': instance.date.toIso8601String(),
|
||||
};
|
||||
|
||||
@@ -7,10 +7,7 @@ part of 'alias.dart';
|
||||
// **************************************************************************
|
||||
|
||||
Alias _$AliasFromJson(Map<String, dynamic> json) {
|
||||
$checkKeys(
|
||||
json,
|
||||
requiredKeys: const ['id', 'alias'],
|
||||
);
|
||||
$checkKeys(json, requiredKeys: const ['id', 'alias']);
|
||||
return Alias(
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
translationId: (json['translation'] as num?)?.toInt(),
|
||||
@@ -19,7 +16,7 @@ Alias _$AliasFromJson(Map<String, dynamic> json) {
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$AliasToJson(Alias instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'translation': instance.translationId,
|
||||
'alias': instance.alias,
|
||||
};
|
||||
'id': instance.id,
|
||||
'translation': instance.translationId,
|
||||
'alias': instance.alias,
|
||||
};
|
||||
|
||||
@@ -7,17 +7,11 @@ part of 'category.dart';
|
||||
// **************************************************************************
|
||||
|
||||
ExerciseCategory _$ExerciseCategoryFromJson(Map<String, dynamic> json) {
|
||||
$checkKeys(
|
||||
json,
|
||||
requiredKeys: const ['id', 'name'],
|
||||
);
|
||||
return ExerciseCategory(
|
||||
id: (json['id'] as num).toInt(),
|
||||
name: json['name'] as String,
|
||||
);
|
||||
$checkKeys(json, requiredKeys: const ['id', 'name']);
|
||||
return ExerciseCategory(id: (json['id'] as num).toInt(), name: json['name'] as String);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$ExerciseCategoryToJson(ExerciseCategory instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
};
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
};
|
||||
|
||||
@@ -7,10 +7,7 @@ part of 'comment.dart';
|
||||
// **************************************************************************
|
||||
|
||||
Comment _$CommentFromJson(Map<String, dynamic> json) {
|
||||
$checkKeys(
|
||||
json,
|
||||
requiredKeys: const ['id', 'comment'],
|
||||
);
|
||||
$checkKeys(json, requiredKeys: const ['id', 'comment']);
|
||||
return Comment(
|
||||
id: (json['id'] as num).toInt(),
|
||||
translationId: (json['translation'] as num).toInt(),
|
||||
@@ -19,7 +16,7 @@ Comment _$CommentFromJson(Map<String, dynamic> json) {
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$CommentToJson(Comment instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'translation': instance.translationId,
|
||||
'comment': instance.comment,
|
||||
};
|
||||
'id': instance.id,
|
||||
'translation': instance.translationId,
|
||||
'comment': instance.comment,
|
||||
};
|
||||
|
||||
@@ -7,10 +7,7 @@ part of 'equipment.dart';
|
||||
// **************************************************************************
|
||||
|
||||
Equipment _$EquipmentFromJson(Map<String, dynamic> json) {
|
||||
$checkKeys(
|
||||
json,
|
||||
requiredKeys: const ['id', 'name'],
|
||||
);
|
||||
$checkKeys(json, requiredKeys: const ['id', 'name']);
|
||||
return Equipment(
|
||||
id: (json['id'] as num).toInt(),
|
||||
name: json['name'] as String,
|
||||
@@ -18,6 +15,6 @@ Equipment _$EquipmentFromJson(Map<String, dynamic> json) {
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$EquipmentToJson(Equipment instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
};
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
};
|
||||
|
||||
@@ -145,10 +145,10 @@ class Exercise extends Equatable {
|
||||
}
|
||||
|
||||
Exercise.fromApiDataString(String baseData, List<Language> languages)
|
||||
: this.fromApiData(ExerciseApiData.fromString(baseData), languages);
|
||||
: this.fromApiData(ExerciseApiData.fromString(baseData), languages);
|
||||
|
||||
Exercise.fromApiDataJson(Map<String, dynamic> baseData, List<Language> languages)
|
||||
: this.fromApiData(ExerciseApiData.fromJson(baseData), languages);
|
||||
: this.fromApiData(ExerciseApiData.fromJson(baseData), languages);
|
||||
|
||||
Exercise.fromApiData(ExerciseApiData exerciseData, List<Language> languages) {
|
||||
id = exerciseData.id;
|
||||
@@ -228,13 +228,13 @@ class Exercise extends Equatable {
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
uuid,
|
||||
created,
|
||||
lastUpdate,
|
||||
category,
|
||||
equipment,
|
||||
muscles,
|
||||
musclesSecondary,
|
||||
];
|
||||
id,
|
||||
uuid,
|
||||
created,
|
||||
lastUpdate,
|
||||
category,
|
||||
equipment,
|
||||
muscles,
|
||||
musclesSecondary,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -19,43 +19,46 @@ Exercise _$ExerciseFromJson(Map<String, dynamic> json) {
|
||||
'category',
|
||||
'muscles',
|
||||
'muscles_secondary',
|
||||
'equipment'
|
||||
'equipment',
|
||||
],
|
||||
);
|
||||
return Exercise(
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
uuid: json['uuid'] as String?,
|
||||
created: json['created'] == null ? null : DateTime.parse(json['created'] as String),
|
||||
lastUpdate: json['last_update'] == null ? null : DateTime.parse(json['last_update'] as String),
|
||||
lastUpdateGlobal: json['last_update_global'] == null
|
||||
? null
|
||||
: DateTime.parse(json['last_update_global'] as String),
|
||||
variationId: (json['variations'] as num?)?.toInt(),
|
||||
translations: (json['translations'] as List<dynamic>?)
|
||||
?.map((e) => Translation.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
category: json['categories'] == null
|
||||
? null
|
||||
: ExerciseCategory.fromJson(json['categories'] as Map<String, dynamic>),
|
||||
)
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
uuid: json['uuid'] as String?,
|
||||
created: json['created'] == null ? null : DateTime.parse(json['created'] as String),
|
||||
lastUpdate: json['last_update'] == null
|
||||
? null
|
||||
: DateTime.parse(json['last_update'] as String),
|
||||
lastUpdateGlobal: json['last_update_global'] == null
|
||||
? null
|
||||
: DateTime.parse(json['last_update_global'] as String),
|
||||
variationId: (json['variations'] as num?)?.toInt(),
|
||||
translations: (json['translations'] as List<dynamic>?)
|
||||
?.map((e) => Translation.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
category: json['categories'] == null
|
||||
? null
|
||||
: ExerciseCategory.fromJson(json['categories'] as Map<String, dynamic>),
|
||||
)
|
||||
..categoryId = (json['category'] as num).toInt()
|
||||
..musclesIds = (json['muscles'] as List<dynamic>).map((e) => (e as num).toInt()).toList()
|
||||
..musclesSecondaryIds =
|
||||
(json['muscles_secondary'] as List<dynamic>).map((e) => (e as num).toInt()).toList()
|
||||
..musclesSecondaryIds = (json['muscles_secondary'] as List<dynamic>)
|
||||
.map((e) => (e as num).toInt())
|
||||
.toList()
|
||||
..equipmentIds = (json['equipment'] as List<dynamic>).map((e) => (e as num).toInt()).toList();
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$ExerciseToJson(Exercise instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'uuid': instance.uuid,
|
||||
'variations': instance.variationId,
|
||||
'created': instance.created?.toIso8601String(),
|
||||
'last_update': instance.lastUpdate?.toIso8601String(),
|
||||
'last_update_global': instance.lastUpdateGlobal?.toIso8601String(),
|
||||
'category': instance.categoryId,
|
||||
'categories': instance.category?.toJson(),
|
||||
'muscles': instance.musclesIds,
|
||||
'muscles_secondary': instance.musclesSecondaryIds,
|
||||
'musclesSecondary': instance.musclesSecondary.map((e) => e.toJson()).toList(),
|
||||
'equipment': instance.equipmentIds,
|
||||
};
|
||||
'id': instance.id,
|
||||
'uuid': instance.uuid,
|
||||
'variations': instance.variationId,
|
||||
'created': instance.created?.toIso8601String(),
|
||||
'last_update': instance.lastUpdate?.toIso8601String(),
|
||||
'last_update_global': instance.lastUpdateGlobal?.toIso8601String(),
|
||||
'category': instance.categoryId,
|
||||
'categories': instance.category?.toJson(),
|
||||
'muscles': instance.musclesIds,
|
||||
'muscles_secondary': instance.musclesSecondaryIds,
|
||||
'musclesSecondary': instance.musclesSecondary.map((e) => e.toJson()).toList(),
|
||||
'equipment': instance.equipmentIds,
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,54 +7,54 @@ part of 'exercise_api.dart';
|
||||
// **************************************************************************
|
||||
|
||||
_ExerciseBaseData _$ExerciseBaseDataFromJson(Map<String, dynamic> json) => _ExerciseBaseData(
|
||||
id: (json['id'] as num).toInt(),
|
||||
uuid: json['uuid'] as String,
|
||||
variationId: (json['variations'] as num?)?.toInt() ?? null,
|
||||
created: DateTime.parse(json['created'] as String),
|
||||
lastUpdate: DateTime.parse(json['last_update'] as String),
|
||||
lastUpdateGlobal: DateTime.parse(json['last_update_global'] as String),
|
||||
category: ExerciseCategory.fromJson(json['category'] as Map<String, dynamic>),
|
||||
muscles: (json['muscles'] as List<dynamic>)
|
||||
.map((e) => Muscle.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
musclesSecondary: (json['muscles_secondary'] as List<dynamic>)
|
||||
.map((e) => Muscle.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
equipment: (json['equipment'] as List<dynamic>)
|
||||
.map((e) => Equipment.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
translations: (json['translations'] as List<dynamic>?)
|
||||
?.map((e) => Translation.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
images: (json['images'] as List<dynamic>)
|
||||
.map((e) => ExerciseImage.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
videos: (json['videos'] as List<dynamic>)
|
||||
.map((e) => Video.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
authors: (json['author_history'] as List<dynamic>).map((e) => e as String).toList(),
|
||||
authorsGlobal:
|
||||
(json['total_authors_history'] as List<dynamic>).map((e) => e as String).toList(),
|
||||
);
|
||||
id: (json['id'] as num).toInt(),
|
||||
uuid: json['uuid'] as String,
|
||||
variationId: (json['variations'] as num?)?.toInt() ?? null,
|
||||
created: DateTime.parse(json['created'] as String),
|
||||
lastUpdate: DateTime.parse(json['last_update'] as String),
|
||||
lastUpdateGlobal: DateTime.parse(json['last_update_global'] as String),
|
||||
category: ExerciseCategory.fromJson(json['category'] as Map<String, dynamic>),
|
||||
muscles: (json['muscles'] as List<dynamic>)
|
||||
.map((e) => Muscle.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
musclesSecondary: (json['muscles_secondary'] as List<dynamic>)
|
||||
.map((e) => Muscle.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
equipment: (json['equipment'] as List<dynamic>)
|
||||
.map((e) => Equipment.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
translations:
|
||||
(json['translations'] as List<dynamic>?)
|
||||
?.map((e) => Translation.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
images: (json['images'] as List<dynamic>)
|
||||
.map((e) => ExerciseImage.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
videos: (json['videos'] as List<dynamic>)
|
||||
.map((e) => Video.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
authors: (json['author_history'] as List<dynamic>).map((e) => e as String).toList(),
|
||||
authorsGlobal: (json['total_authors_history'] as List<dynamic>).map((e) => e as String).toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ExerciseBaseDataToJson(_ExerciseBaseData instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'uuid': instance.uuid,
|
||||
'variations': instance.variationId,
|
||||
'created': instance.created.toIso8601String(),
|
||||
'last_update': instance.lastUpdate.toIso8601String(),
|
||||
'last_update_global': instance.lastUpdateGlobal.toIso8601String(),
|
||||
'category': instance.category,
|
||||
'muscles': instance.muscles,
|
||||
'muscles_secondary': instance.musclesSecondary,
|
||||
'equipment': instance.equipment,
|
||||
'translations': instance.translations,
|
||||
'images': instance.images,
|
||||
'videos': instance.videos,
|
||||
'author_history': instance.authors,
|
||||
'total_authors_history': instance.authorsGlobal,
|
||||
};
|
||||
'id': instance.id,
|
||||
'uuid': instance.uuid,
|
||||
'variations': instance.variationId,
|
||||
'created': instance.created.toIso8601String(),
|
||||
'last_update': instance.lastUpdate.toIso8601String(),
|
||||
'last_update_global': instance.lastUpdateGlobal.toIso8601String(),
|
||||
'category': instance.category,
|
||||
'muscles': instance.muscles,
|
||||
'muscles_secondary': instance.musclesSecondary,
|
||||
'equipment': instance.equipment,
|
||||
'translations': instance.translations,
|
||||
'images': instance.images,
|
||||
'videos': instance.videos,
|
||||
'author_history': instance.authors,
|
||||
'total_authors_history': instance.authorsGlobal,
|
||||
};
|
||||
|
||||
_ExerciseSearchDetails _$ExerciseSearchDetailsFromJson(Map<String, dynamic> json) =>
|
||||
_ExerciseSearchDetails(
|
||||
@@ -83,17 +83,14 @@ _ExerciseSearchEntry _$ExerciseSearchEntryFromJson(Map<String, dynamic> json) =>
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ExerciseSearchEntryToJson(_ExerciseSearchEntry instance) =>
|
||||
<String, dynamic>{
|
||||
'value': instance.value,
|
||||
'data': instance.data,
|
||||
};
|
||||
<String, dynamic>{'value': instance.value, 'data': instance.data};
|
||||
|
||||
_ExerciseApiSearch _$ExerciseApiSearchFromJson(Map<String, dynamic> json) => _ExerciseApiSearch(
|
||||
suggestions: (json['suggestions'] as List<dynamic>)
|
||||
.map((e) => ExerciseSearchEntry.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
suggestions: (json['suggestions'] as List<dynamic>)
|
||||
.map((e) => ExerciseSearchEntry.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ExerciseApiSearchToJson(_ExerciseApiSearch instance) => <String, dynamic>{
|
||||
'suggestions': instance.suggestions,
|
||||
};
|
||||
'suggestions': instance.suggestions,
|
||||
};
|
||||
|
||||
@@ -660,6 +660,8 @@ abstract mixin class $ExerciseTranslationSubmissionApiCopyWith<$Res> {
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$ExerciseTranslationSubmissionApiCopyWithImpl<$Res>
|
||||
@@ -941,6 +943,8 @@ abstract mixin class _$ExerciseTranslationSubmissionApiCopyWith<$Res>
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$ExerciseTranslationSubmissionApiCopyWithImpl<$Res>
|
||||
@@ -1066,6 +1070,8 @@ abstract mixin class $ExerciseSubmissionApiCopyWith<$Res> {
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$ExerciseSubmissionApiCopyWithImpl<$Res>
|
||||
@@ -1385,6 +1391,8 @@ abstract mixin class _$ExerciseSubmissionApiCopyWith<$Res>
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$ExerciseSubmissionApiCopyWithImpl<$Res>
|
||||
|
||||
67
lib/models/exercises/exercise_submission_images.dart
Normal file
67
lib/models/exercises/exercise_submission_images.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum ImageType {
|
||||
photo(id: 1, label: 'Photo', icon: Icons.photo_camera),
|
||||
threeD(id: 2, label: '3D', icon: Icons.view_in_ar),
|
||||
line(id: 3, label: 'Line', icon: Icons.show_chart),
|
||||
lowPoly(id: 4, label: 'Low-Poly', icon: Icons.filter_vintage),
|
||||
other(id: 5, label: 'Other', icon: Icons.more_horiz);
|
||||
|
||||
const ImageType({required this.id, required this.label, required this.icon});
|
||||
|
||||
final int id;
|
||||
final String label;
|
||||
final IconData icon;
|
||||
}
|
||||
|
||||
class ExerciseSubmissionImage {
|
||||
final File imageFile;
|
||||
|
||||
String? title;
|
||||
String? author;
|
||||
String? authorUrl;
|
||||
String? sourceUrl;
|
||||
String? derivativeSourceUrl;
|
||||
ImageType type = ImageType.photo;
|
||||
|
||||
ExerciseSubmissionImage({
|
||||
this.title,
|
||||
this.author,
|
||||
this.authorUrl,
|
||||
this.sourceUrl,
|
||||
this.derivativeSourceUrl,
|
||||
this.type = ImageType.photo,
|
||||
required this.imageFile,
|
||||
});
|
||||
|
||||
Map<String, String> toJson() {
|
||||
return {
|
||||
'license_title': title ?? '',
|
||||
'license_author': author ?? '',
|
||||
'license_author_url': authorUrl ?? '',
|
||||
'license_object_url': sourceUrl ?? '',
|
||||
'license_derivative_source_url': derivativeSourceUrl ?? '',
|
||||
'style': type.id.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,7 @@ part of 'image.dart';
|
||||
// **************************************************************************
|
||||
|
||||
ExerciseImage _$ExerciseImageFromJson(Map<String, dynamic> json) {
|
||||
$checkKeys(
|
||||
json,
|
||||
requiredKeys: const ['id', 'uuid', 'exercise', 'image'],
|
||||
);
|
||||
$checkKeys(json, requiredKeys: const ['id', 'uuid', 'exercise', 'image']);
|
||||
return ExerciseImage(
|
||||
id: (json['id'] as num).toInt(),
|
||||
uuid: json['uuid'] as String,
|
||||
@@ -21,9 +18,9 @@ ExerciseImage _$ExerciseImageFromJson(Map<String, dynamic> json) {
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$ExerciseImageToJson(ExerciseImage instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'uuid': instance.uuid,
|
||||
'exercise': instance.exerciseId,
|
||||
'image': instance.url,
|
||||
'is_main': instance.isMain,
|
||||
};
|
||||
'id': instance.id,
|
||||
'uuid': instance.uuid,
|
||||
'exercise': instance.exerciseId,
|
||||
'image': instance.url,
|
||||
'is_main': instance.isMain,
|
||||
};
|
||||
|
||||
@@ -7,10 +7,7 @@ part of 'language.dart';
|
||||
// **************************************************************************
|
||||
|
||||
Language _$LanguageFromJson(Map<String, dynamic> json) {
|
||||
$checkKeys(
|
||||
json,
|
||||
requiredKeys: const ['id', 'short_name', 'full_name'],
|
||||
);
|
||||
$checkKeys(json, requiredKeys: const ['id', 'short_name', 'full_name']);
|
||||
return Language(
|
||||
id: (json['id'] as num).toInt(),
|
||||
shortName: json['short_name'] as String,
|
||||
@@ -19,7 +16,7 @@ Language _$LanguageFromJson(Map<String, dynamic> json) {
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$LanguageToJson(Language instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'short_name': instance.shortName,
|
||||
'full_name': instance.fullName,
|
||||
};
|
||||
'id': instance.id,
|
||||
'short_name': instance.shortName,
|
||||
'full_name': instance.fullName,
|
||||
};
|
||||
|
||||
@@ -7,10 +7,7 @@ part of 'muscle.dart';
|
||||
// **************************************************************************
|
||||
|
||||
Muscle _$MuscleFromJson(Map<String, dynamic> json) {
|
||||
$checkKeys(
|
||||
json,
|
||||
requiredKeys: const ['id', 'name', 'name_en', 'is_front'],
|
||||
);
|
||||
$checkKeys(json, requiredKeys: const ['id', 'name', 'name_en', 'is_front']);
|
||||
return Muscle(
|
||||
id: (json['id'] as num).toInt(),
|
||||
name: json['name'] as String,
|
||||
@@ -20,8 +17,8 @@ Muscle _$MuscleFromJson(Map<String, dynamic> json) {
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$MuscleToJson(Muscle instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'name_en': instance.nameEn,
|
||||
'is_front': instance.isFront,
|
||||
};
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'name_en': instance.nameEn,
|
||||
'is_front': instance.isFront,
|
||||
};
|
||||
|
||||
@@ -92,12 +92,12 @@ class Translation extends Equatable {
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
exerciseId,
|
||||
uuid,
|
||||
languageId,
|
||||
created,
|
||||
name,
|
||||
description,
|
||||
];
|
||||
id,
|
||||
exerciseId,
|
||||
uuid,
|
||||
languageId,
|
||||
created,
|
||||
name,
|
||||
description,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -12,13 +12,13 @@ Translation _$TranslationFromJson(Map<String, dynamic> json) {
|
||||
requiredKeys: const ['id', 'uuid', 'language', 'created', 'exercise', 'name', 'description'],
|
||||
);
|
||||
return Translation(
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
uuid: json['uuid'] as String?,
|
||||
created: json['created'] == null ? null : DateTime.parse(json['created'] as String),
|
||||
name: json['name'] as String,
|
||||
description: json['description'] as String,
|
||||
exerciseId: (json['exercise'] as num?)?.toInt(),
|
||||
)
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
uuid: json['uuid'] as String?,
|
||||
created: json['created'] == null ? null : DateTime.parse(json['created'] as String),
|
||||
name: json['name'] as String,
|
||||
description: json['description'] as String,
|
||||
exerciseId: (json['exercise'] as num?)?.toInt(),
|
||||
)
|
||||
..languageId = (json['language'] as num).toInt()
|
||||
..notes = (json['notes'] as List<dynamic>)
|
||||
.map((e) => Comment.fromJson(e as Map<String, dynamic>))
|
||||
@@ -29,11 +29,11 @@ Translation _$TranslationFromJson(Map<String, dynamic> json) {
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$TranslationToJson(Translation instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'uuid': instance.uuid,
|
||||
'language': instance.languageId,
|
||||
'created': instance.created?.toIso8601String(),
|
||||
'exercise': instance.exerciseId,
|
||||
'name': instance.name,
|
||||
'description': instance.description,
|
||||
};
|
||||
'id': instance.id,
|
||||
'uuid': instance.uuid,
|
||||
'language': instance.languageId,
|
||||
'created': instance.created?.toIso8601String(),
|
||||
'exercise': instance.exerciseId,
|
||||
'name': instance.name,
|
||||
'description': instance.description,
|
||||
};
|
||||
|
||||
@@ -7,15 +7,10 @@ part of 'variation.dart';
|
||||
// **************************************************************************
|
||||
|
||||
Variation _$VariationFromJson(Map<String, dynamic> json) {
|
||||
$checkKeys(
|
||||
json,
|
||||
requiredKeys: const ['id'],
|
||||
);
|
||||
return Variation(
|
||||
id: (json['id'] as num).toInt(),
|
||||
);
|
||||
$checkKeys(json, requiredKeys: const ['id']);
|
||||
return Variation(id: (json['id'] as num).toInt());
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$VariationToJson(Variation instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
};
|
||||
'id': instance.id,
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ Video _$VideoFromJson(Map<String, dynamic> json) {
|
||||
'codec',
|
||||
'codec_long',
|
||||
'license',
|
||||
'license_author'
|
||||
'license_author',
|
||||
],
|
||||
);
|
||||
return Video(
|
||||
@@ -41,16 +41,16 @@ Video _$VideoFromJson(Map<String, dynamic> json) {
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$VideoToJson(Video instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'uuid': instance.uuid,
|
||||
'video': instance.url,
|
||||
'exercise': instance.exerciseId,
|
||||
'size': instance.size,
|
||||
'duration': numToString(instance.duration),
|
||||
'width': instance.width,
|
||||
'height': instance.height,
|
||||
'codec': instance.codec,
|
||||
'codec_long': instance.codecLong,
|
||||
'license': instance.license,
|
||||
'license_author': instance.licenseAuthor,
|
||||
};
|
||||
'id': instance.id,
|
||||
'uuid': instance.uuid,
|
||||
'video': instance.url,
|
||||
'exercise': instance.exerciseId,
|
||||
'size': instance.size,
|
||||
'duration': numToString(instance.duration),
|
||||
'width': instance.width,
|
||||
'height': instance.height,
|
||||
'codec': instance.codec,
|
||||
'codec_long': instance.codecLong,
|
||||
'license': instance.license,
|
||||
'license_author': instance.licenseAuthor,
|
||||
};
|
||||
|
||||
@@ -7,10 +7,7 @@ part of 'image.dart';
|
||||
// **************************************************************************
|
||||
|
||||
Image _$ImageFromJson(Map<String, dynamic> json) {
|
||||
$checkKeys(
|
||||
json,
|
||||
requiredKeys: const ['id', 'date', 'image'],
|
||||
);
|
||||
$checkKeys(json, requiredKeys: const ['id', 'date', 'image']);
|
||||
return Image(
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
date: DateTime.parse(json['date'] as String),
|
||||
@@ -20,8 +17,8 @@ Image _$ImageFromJson(Map<String, dynamic> json) {
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$ImageToJson(Image instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'date': dateToYYYYMMDD(instance.date),
|
||||
'image': instance.url,
|
||||
'description': instance.description,
|
||||
};
|
||||
'id': instance.id,
|
||||
'date': dateToYYYYMMDD(instance.date),
|
||||
'image': instance.url,
|
||||
'description': instance.description,
|
||||
};
|
||||
|
||||
@@ -7,15 +7,13 @@ part of 'measurement_category.dart';
|
||||
// **************************************************************************
|
||||
|
||||
MeasurementCategory _$MeasurementCategoryFromJson(Map<String, dynamic> json) {
|
||||
$checkKeys(
|
||||
json,
|
||||
requiredKeys: const ['id', 'name', 'unit'],
|
||||
);
|
||||
$checkKeys(json, requiredKeys: const ['id', 'name', 'unit']);
|
||||
return MeasurementCategory(
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
name: json['name'] as String,
|
||||
unit: json['unit'] as String,
|
||||
entries: (json['entries'] as List<dynamic>?)
|
||||
entries:
|
||||
(json['entries'] as List<dynamic>?)
|
||||
?.map((e) => MeasurementEntry.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
@@ -23,8 +21,8 @@ MeasurementCategory _$MeasurementCategoryFromJson(Map<String, dynamic> json) {
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$MeasurementCategoryToJson(MeasurementCategory instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'unit': instance.unit,
|
||||
'entries': MeasurementCategory._nullValue(instance.entries),
|
||||
};
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'unit': instance.unit,
|
||||
'entries': MeasurementCategory._nullValue(instance.entries),
|
||||
};
|
||||
|
||||
@@ -21,9 +21,9 @@ MeasurementEntry _$MeasurementEntryFromJson(Map<String, dynamic> json) {
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$MeasurementEntryToJson(MeasurementEntry instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'category': instance.category,
|
||||
'date': dateToYYYYMMDD(instance.date),
|
||||
'value': instance.value,
|
||||
'notes': instance.notes,
|
||||
};
|
||||
'id': instance.id,
|
||||
'category': instance.category,
|
||||
'date': dateToYYYYMMDD(instance.date),
|
||||
'value': instance.value,
|
||||
'notes': instance.notes,
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ Ingredient _$IngredientFromJson(Map<String, dynamic> json) {
|
||||
'fat',
|
||||
'fat_saturated',
|
||||
'fiber',
|
||||
'sodium'
|
||||
'sodium',
|
||||
],
|
||||
);
|
||||
return Ingredient(
|
||||
@@ -55,22 +55,22 @@ Ingredient _$IngredientFromJson(Map<String, dynamic> json) {
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$IngredientToJson(Ingredient instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'remote_id': instance.remoteId,
|
||||
'source_name': instance.sourceName,
|
||||
'source_url': instance.sourceUrl,
|
||||
'license_object_url': instance.licenseObjectURl,
|
||||
'code': instance.code,
|
||||
'name': instance.name,
|
||||
'created': instance.created.toIso8601String(),
|
||||
'energy': instance.energy,
|
||||
'carbohydrates': numToString(instance.carbohydrates),
|
||||
'carbohydrates_sugar': numToString(instance.carbohydratesSugar),
|
||||
'protein': numToString(instance.protein),
|
||||
'fat': numToString(instance.fat),
|
||||
'fat_saturated': numToString(instance.fatSaturated),
|
||||
'fiber': numToString(instance.fiber),
|
||||
'sodium': numToString(instance.sodium),
|
||||
'image': instance.image,
|
||||
'thumbnails': instance.thumbnails,
|
||||
};
|
||||
'id': instance.id,
|
||||
'remote_id': instance.remoteId,
|
||||
'source_name': instance.sourceName,
|
||||
'source_url': instance.sourceUrl,
|
||||
'license_object_url': instance.licenseObjectURl,
|
||||
'code': instance.code,
|
||||
'name': instance.name,
|
||||
'created': instance.created.toIso8601String(),
|
||||
'energy': instance.energy,
|
||||
'carbohydrates': numToString(instance.carbohydrates),
|
||||
'carbohydrates_sugar': numToString(instance.carbohydratesSugar),
|
||||
'protein': numToString(instance.protein),
|
||||
'fat': numToString(instance.fat),
|
||||
'fat_saturated': numToString(instance.fatSaturated),
|
||||
'fiber': numToString(instance.fiber),
|
||||
'sodium': numToString(instance.sodium),
|
||||
'image': instance.image,
|
||||
'thumbnails': instance.thumbnails,
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@ IngredientImage _$IngredientImageFromJson(Map<String, dynamic> json) {
|
||||
'license_author_url',
|
||||
'license_title',
|
||||
'license_object_url',
|
||||
'license_derivative_source_url'
|
||||
'license_derivative_source_url',
|
||||
],
|
||||
);
|
||||
return IngredientImage(
|
||||
@@ -39,15 +39,15 @@ IngredientImage _$IngredientImageFromJson(Map<String, dynamic> json) {
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$IngredientImageToJson(IngredientImage instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'uuid': instance.uuid,
|
||||
'ingredient_id': instance.ingredientId,
|
||||
'image': instance.url,
|
||||
'size': instance.size,
|
||||
'license': instance.licenseId,
|
||||
'license_author': instance.author,
|
||||
'license_author_url': instance.authorUrl,
|
||||
'license_title': instance.title,
|
||||
'license_object_url': instance.objectUrl,
|
||||
'license_derivative_source_url': instance.derivativeSourceUrl,
|
||||
};
|
||||
'id': instance.id,
|
||||
'uuid': instance.uuid,
|
||||
'ingredient_id': instance.ingredientId,
|
||||
'image': instance.url,
|
||||
'size': instance.size,
|
||||
'license': instance.licenseId,
|
||||
'license_author': instance.author,
|
||||
'license_author_url': instance.authorUrl,
|
||||
'license_title': instance.title,
|
||||
'license_object_url': instance.objectUrl,
|
||||
'license_derivative_source_url': instance.derivativeSourceUrl,
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ IngredientImageThumbnails _$IngredientImageThumbnailsFromJson(Map<String, dynami
|
||||
'medium',
|
||||
'medium_cropped',
|
||||
'large',
|
||||
'large_cropped'
|
||||
'large_cropped',
|
||||
],
|
||||
);
|
||||
return IngredientImageThumbnails(
|
||||
|
||||
@@ -7,10 +7,7 @@ part of 'ingredient_weight_unit.dart';
|
||||
// **************************************************************************
|
||||
|
||||
IngredientWeightUnit _$IngredientWeightUnitFromJson(Map<String, dynamic> json) {
|
||||
$checkKeys(
|
||||
json,
|
||||
requiredKeys: const ['id', 'weight_unit', 'ingredient', 'grams', 'amount'],
|
||||
);
|
||||
$checkKeys(json, requiredKeys: const ['id', 'weight_unit', 'ingredient', 'grams', 'amount']);
|
||||
return IngredientWeightUnit(
|
||||
id: (json['id'] as num).toInt(),
|
||||
weightUnit: WeightUnit.fromJson(json['weight_unit'] as Map<String, dynamic>),
|
||||
|
||||
@@ -84,8 +84,9 @@ class Log {
|
||||
NutritionalValues get nutritionalValues {
|
||||
// This is already done on the server. It might be better to read it from there.
|
||||
|
||||
final weight =
|
||||
weightUnitObj == null ? amount : amount * weightUnitObj!.amount * weightUnitObj!.grams;
|
||||
final weight = weightUnitObj == null
|
||||
? amount
|
||||
: amount * weightUnitObj!.amount * weightUnitObj!.grams;
|
||||
|
||||
return ingredient.nutritionalValues / (100 / weight);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,14 @@ part of 'log.dart';
|
||||
Log _$LogFromJson(Map<String, dynamic> json) {
|
||||
$checkKeys(
|
||||
json,
|
||||
requiredKeys: const ['id', 'plan', 'datetime', 'ingredient', 'weight_unit', 'amount'],
|
||||
requiredKeys: const [
|
||||
'id',
|
||||
'plan',
|
||||
'datetime',
|
||||
'ingredient',
|
||||
'weight_unit',
|
||||
'amount',
|
||||
],
|
||||
);
|
||||
return Log(
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
@@ -24,12 +31,12 @@ Log _$LogFromJson(Map<String, dynamic> json) {
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$LogToJson(Log instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'meal': instance.mealId,
|
||||
'plan': instance.planId,
|
||||
'datetime': dateToUtcIso8601(instance.datetime),
|
||||
'comment': instance.comment,
|
||||
'ingredient': instance.ingredientId,
|
||||
'weight_unit': instance.weightUnitId,
|
||||
'amount': instance.amount,
|
||||
};
|
||||
'id': instance.id,
|
||||
'meal': instance.mealId,
|
||||
'plan': instance.planId,
|
||||
'datetime': dateToUtcIso8601(instance.datetime),
|
||||
'comment': instance.comment,
|
||||
'ingredient': instance.ingredientId,
|
||||
'weight_unit': instance.weightUnitId,
|
||||
'amount': instance.amount,
|
||||
};
|
||||
|
||||
@@ -7,14 +7,14 @@ part of 'meal.dart';
|
||||
// **************************************************************************
|
||||
|
||||
Meal _$MealFromJson(Map<String, dynamic> json) => Meal(
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
time: stringToTimeNull(json['time'] as String?),
|
||||
name: json['name'] as String?,
|
||||
)..planId = (json['plan'] as num).toInt();
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
time: stringToTimeNull(json['time'] as String?),
|
||||
name: json['name'] as String?,
|
||||
)..planId = (json['plan'] as num).toInt();
|
||||
|
||||
Map<String, dynamic> _$MealToJson(Meal instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'plan': instance.planId,
|
||||
'time': timeToString(instance.time),
|
||||
'name': instance.name,
|
||||
};
|
||||
'id': instance.id,
|
||||
'plan': instance.planId,
|
||||
'time': timeToString(instance.time),
|
||||
'name': instance.name,
|
||||
};
|
||||
|
||||
@@ -7,10 +7,7 @@ part of 'meal_item.dart';
|
||||
// **************************************************************************
|
||||
|
||||
MealItem _$MealItemFromJson(Map<String, dynamic> json) {
|
||||
$checkKeys(
|
||||
json,
|
||||
requiredKeys: const ['id', 'amount'],
|
||||
);
|
||||
$checkKeys(json, requiredKeys: const ['id', 'amount']);
|
||||
return MealItem(
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
mealId: (json['meal'] as num?)?.toInt(),
|
||||
@@ -21,9 +18,9 @@ MealItem _$MealItemFromJson(Map<String, dynamic> json) {
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$MealItemToJson(MealItem instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'meal': instance.mealId,
|
||||
'ingredient': instance.ingredientId,
|
||||
'weight_unit': instance.weightUnitId,
|
||||
'amount': numToString(instance.amount),
|
||||
};
|
||||
'id': instance.id,
|
||||
'meal': instance.mealId,
|
||||
'ingredient': instance.ingredientId,
|
||||
'weight_unit': instance.weightUnitId,
|
||||
'amount': numToString(instance.amount),
|
||||
};
|
||||
|
||||
@@ -57,7 +57,8 @@ class NutritionalGoals {
|
||||
(energy! - protein! * ENERGY_PROTEIN - fat! * ENERGY_FAT) / ENERGY_CARBOHYDRATES;
|
||||
assert(carbohydrates! > 0);
|
||||
} else if (fat == null && protein != null && carbohydrates != null) {
|
||||
fat = (energy! - protein! * ENERGY_PROTEIN - carbohydrates! * ENERGY_CARBOHYDRATES) /
|
||||
fat =
|
||||
(energy! - protein! * ENERGY_PROTEIN - carbohydrates! * ENERGY_CARBOHYDRATES) /
|
||||
ENERGY_FAT;
|
||||
assert(fat! > 0);
|
||||
}
|
||||
@@ -141,15 +142,15 @@ class NutritionalGoals {
|
||||
@override
|
||||
//ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
int get hashCode => Object.hash(
|
||||
energy,
|
||||
protein,
|
||||
carbohydrates,
|
||||
carbohydratesSugar,
|
||||
fat,
|
||||
fatSaturated,
|
||||
fiber,
|
||||
sodium,
|
||||
);
|
||||
energy,
|
||||
protein,
|
||||
carbohydrates,
|
||||
carbohydratesSugar,
|
||||
fat,
|
||||
fatSaturated,
|
||||
fiber,
|
||||
sodium,
|
||||
);
|
||||
|
||||
@override
|
||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
|
||||
@@ -20,14 +20,15 @@ NutritionalPlan _$NutritionalPlanFromJson(Map<String, dynamic> json) {
|
||||
'goal_protein',
|
||||
'goal_carbohydrates',
|
||||
'goal_fat',
|
||||
'goal_fiber'
|
||||
'goal_fiber',
|
||||
],
|
||||
);
|
||||
return NutritionalPlan(
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
description: json['description'] as String,
|
||||
creationDate:
|
||||
json['creation_date'] == null ? null : DateTime.parse(json['creation_date'] as String),
|
||||
creationDate: json['creation_date'] == null
|
||||
? null
|
||||
: DateTime.parse(json['creation_date'] as String),
|
||||
startDate: DateTime.parse(json['start'] as String),
|
||||
endDate: json['end'] == null ? null : DateTime.parse(json['end'] as String),
|
||||
onlyLogging: json['only_logging'] as bool? ?? false,
|
||||
@@ -40,15 +41,15 @@ NutritionalPlan _$NutritionalPlanFromJson(Map<String, dynamic> json) {
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$NutritionalPlanToJson(NutritionalPlan instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'description': instance.description,
|
||||
'creation_date': dateToUtcIso8601(instance.creationDate),
|
||||
'start': dateToYYYYMMDD(instance.startDate),
|
||||
'end': dateToYYYYMMDD(instance.endDate),
|
||||
'only_logging': instance.onlyLogging,
|
||||
'goal_energy': instance.goalEnergy,
|
||||
'goal_protein': instance.goalProtein,
|
||||
'goal_carbohydrates': instance.goalCarbohydrates,
|
||||
'goal_fat': instance.goalFat,
|
||||
'goal_fiber': instance.goalFiber,
|
||||
};
|
||||
'id': instance.id,
|
||||
'description': instance.description,
|
||||
'creation_date': dateToUtcIso8601(instance.creationDate),
|
||||
'start': dateToYYYYMMDD(instance.startDate),
|
||||
'end': dateToYYYYMMDD(instance.endDate),
|
||||
'only_logging': instance.onlyLogging,
|
||||
'goal_energy': instance.goalEnergy,
|
||||
'goal_protein': instance.goalProtein,
|
||||
'goal_carbohydrates': instance.goalCarbohydrates,
|
||||
'goal_fat': instance.goalFat,
|
||||
'goal_fiber': instance.goalFiber,
|
||||
};
|
||||
|
||||
@@ -132,13 +132,13 @@ class NutritionalValues {
|
||||
@override
|
||||
//ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
int get hashCode => Object.hash(
|
||||
energy,
|
||||
protein,
|
||||
carbohydrates,
|
||||
carbohydratesSugar,
|
||||
fat,
|
||||
fatSaturated,
|
||||
fiber,
|
||||
sodium,
|
||||
);
|
||||
energy,
|
||||
protein,
|
||||
carbohydrates,
|
||||
carbohydratesSugar,
|
||||
fat,
|
||||
fatSaturated,
|
||||
fiber,
|
||||
sodium,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,10 +7,7 @@ part of 'weight_unit.dart';
|
||||
// **************************************************************************
|
||||
|
||||
WeightUnit _$WeightUnitFromJson(Map<String, dynamic> json) {
|
||||
$checkKeys(
|
||||
json,
|
||||
requiredKeys: const ['id', 'name'],
|
||||
);
|
||||
$checkKeys(json, requiredKeys: const ['id', 'name']);
|
||||
return WeightUnit(
|
||||
id: (json['id'] as num).toInt(),
|
||||
name: json['name'] as String,
|
||||
@@ -18,6 +15,6 @@ WeightUnit _$WeightUnitFromJson(Map<String, dynamic> json) {
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$WeightUnitToJson(WeightUnit instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
};
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
};
|
||||
|
||||
@@ -9,7 +9,13 @@ part of 'profile.dart';
|
||||
Profile _$ProfileFromJson(Map<String, dynamic> json) {
|
||||
$checkKeys(
|
||||
json,
|
||||
requiredKeys: const ['username', 'email_verified', 'is_trustworthy', 'weight_unit', 'email'],
|
||||
requiredKeys: const [
|
||||
'username',
|
||||
'email_verified',
|
||||
'is_trustworthy',
|
||||
'weight_unit',
|
||||
'email',
|
||||
],
|
||||
);
|
||||
return Profile(
|
||||
username: json['username'] as String,
|
||||
@@ -21,9 +27,9 @@ Profile _$ProfileFromJson(Map<String, dynamic> json) {
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$ProfileToJson(Profile instance) => <String, dynamic>{
|
||||
'username': instance.username,
|
||||
'email_verified': instance.emailVerified,
|
||||
'is_trustworthy': instance.isTrustworthy,
|
||||
'weight_unit': instance.weightUnitStr,
|
||||
'email': instance.email,
|
||||
};
|
||||
'username': instance.username,
|
||||
'email_verified': instance.emailVerified,
|
||||
'is_trustworthy': instance.isTrustworthy,
|
||||
'weight_unit': instance.weightUnitStr,
|
||||
'email': instance.email,
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ BaseConfig _$BaseConfigFromJson(Map<String, dynamic> json) {
|
||||
'operation',
|
||||
'step',
|
||||
'repeat',
|
||||
'requirements'
|
||||
'requirements',
|
||||
],
|
||||
);
|
||||
return BaseConfig(
|
||||
@@ -33,11 +33,11 @@ BaseConfig _$BaseConfigFromJson(Map<String, dynamic> json) {
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$BaseConfigToJson(BaseConfig instance) => <String, dynamic>{
|
||||
'slot_entry': instance.slotEntryId,
|
||||
'iteration': instance.iteration,
|
||||
'value': instance.value,
|
||||
'operation': instance.operation,
|
||||
'step': instance.step,
|
||||
'repeat': instance.repeat,
|
||||
'requirements': instance.requirements,
|
||||
};
|
||||
'slot_entry': instance.slotEntryId,
|
||||
'iteration': instance.iteration,
|
||||
'value': instance.value,
|
||||
'operation': instance.operation,
|
||||
'step': instance.step,
|
||||
'repeat': instance.repeat,
|
||||
'requirements': instance.requirements,
|
||||
};
|
||||
|
||||
@@ -142,8 +142,13 @@ class Log {
|
||||
|
||||
/// Returns the text representation for a single setting, used in the gym mode
|
||||
String get singleLogRepTextNoNl {
|
||||
return repText(repetitions, repetitionsUnitObj, weight, weightUnitObj, rir)
|
||||
.replaceAll('\n', '');
|
||||
return repText(
|
||||
repetitions,
|
||||
repetitionsUnitObj,
|
||||
weight,
|
||||
weightUnitObj,
|
||||
rir,
|
||||
).replaceAll('\n', '');
|
||||
}
|
||||
|
||||
/// Override the equals operator
|
||||
|
||||
@@ -22,7 +22,7 @@ Log _$LogFromJson(Map<String, dynamic> json) {
|
||||
'weight',
|
||||
'weight_target',
|
||||
'weight_unit',
|
||||
'date'
|
||||
'date',
|
||||
],
|
||||
);
|
||||
return Log(
|
||||
@@ -44,19 +44,19 @@ Log _$LogFromJson(Map<String, dynamic> json) {
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$LogToJson(Log instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'exercise': instance.exerciseId,
|
||||
'routine': instance.routineId,
|
||||
'session': instance.sessionId,
|
||||
'iteration': instance.iteration,
|
||||
'slot_entry': instance.slotEntryId,
|
||||
'rir': instance.rir,
|
||||
'rir_target': instance.rirTarget,
|
||||
'repetitions': instance.repetitions,
|
||||
'repetitions_target': instance.repetitionsTarget,
|
||||
'repetitions_unit': instance.repetitionsUnitId,
|
||||
'weight': numToString(instance.weight),
|
||||
'weight_target': numToString(instance.weightTarget),
|
||||
'weight_unit': instance.weightUnitId,
|
||||
'date': dateToUtcIso8601(instance.date),
|
||||
};
|
||||
'id': instance.id,
|
||||
'exercise': instance.exerciseId,
|
||||
'routine': instance.routineId,
|
||||
'session': instance.sessionId,
|
||||
'iteration': instance.iteration,
|
||||
'slot_entry': instance.slotEntryId,
|
||||
'rir': instance.rir,
|
||||
'rir_target': instance.rirTarget,
|
||||
'repetitions': instance.repetitions,
|
||||
'repetitions_target': instance.repetitionsTarget,
|
||||
'repetitions_unit': instance.repetitionsUnitId,
|
||||
'weight': numToString(instance.weight),
|
||||
'weight_target': numToString(instance.weightTarget),
|
||||
'weight_unit': instance.weightUnitId,
|
||||
'date': dateToUtcIso8601(instance.date),
|
||||
};
|
||||
|
||||
@@ -7,10 +7,7 @@ part of 'repetition_unit.dart';
|
||||
// **************************************************************************
|
||||
|
||||
RepetitionUnit _$RepetitionUnitFromJson(Map<String, dynamic> json) {
|
||||
$checkKeys(
|
||||
json,
|
||||
requiredKeys: const ['id', 'name'],
|
||||
);
|
||||
$checkKeys(json, requiredKeys: const ['id', 'name']);
|
||||
return RepetitionUnit(
|
||||
id: (json['id'] as num).toInt(),
|
||||
name: json['name'] as String,
|
||||
@@ -18,6 +15,6 @@ RepetitionUnit _$RepetitionUnitFromJson(Map<String, dynamic> json) {
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$RepetitionUnitToJson(RepetitionUnit instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
};
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
};
|
||||
|
||||
@@ -19,11 +19,13 @@ Routine _$RoutineFromJson(Map<String, dynamic> json) {
|
||||
end: json['end'] == null ? null : DateTime.parse(json['end'] as String),
|
||||
fitInWeek: json['fit_in_week'] as bool? ?? false,
|
||||
description: json['description'] as String?,
|
||||
days: (json['days'] as List<dynamic>?)
|
||||
days:
|
||||
(json['days'] as List<dynamic>?)
|
||||
?.map((e) => Day.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const [],
|
||||
sessions: (json['sessions'] as List<dynamic>?)
|
||||
sessions:
|
||||
(json['sessions'] as List<dynamic>?)
|
||||
?.map((e) => WorkoutSessionApi.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
@@ -31,10 +33,10 @@ Routine _$RoutineFromJson(Map<String, dynamic> json) {
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$RoutineToJson(Routine instance) => <String, dynamic>{
|
||||
'created': dateToUtcIso8601(instance.created),
|
||||
'name': instance.name,
|
||||
'description': instance.description,
|
||||
'fit_in_week': instance.fitInWeek,
|
||||
'start': dateToYYYYMMDD(instance.start),
|
||||
'end': dateToYYYYMMDD(instance.end),
|
||||
};
|
||||
'created': dateToUtcIso8601(instance.created),
|
||||
'name': instance.name,
|
||||
'description': instance.description,
|
||||
'fit_in_week': instance.fitInWeek,
|
||||
'start': dateToYYYYMMDD(instance.start),
|
||||
'end': dateToYYYYMMDD(instance.end),
|
||||
};
|
||||
|
||||
@@ -19,7 +19,8 @@ WorkoutSession _$WorkoutSessionFromJson(Map<String, dynamic> json) {
|
||||
notes: json['notes'] as String? ?? '',
|
||||
timeStart: stringToTimeNull(json['time_start'] as String?),
|
||||
timeEnd: stringToTimeNull(json['time_end'] as String?),
|
||||
logs: (json['logs'] as List<dynamic>?)
|
||||
logs:
|
||||
(json['logs'] as List<dynamic>?)
|
||||
?.map((e) => Log.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
@@ -28,12 +29,12 @@ WorkoutSession _$WorkoutSessionFromJson(Map<String, dynamic> json) {
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$WorkoutSessionToJson(WorkoutSession instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'routine': instance.routineId,
|
||||
'day': instance.dayId,
|
||||
'date': dateToYYYYMMDD(instance.date),
|
||||
'impression': numToString(instance.impression),
|
||||
'notes': instance.notes,
|
||||
'time_start': timeToString(instance.timeStart),
|
||||
'time_end': timeToString(instance.timeEnd),
|
||||
};
|
||||
'id': instance.id,
|
||||
'routine': instance.routineId,
|
||||
'day': instance.dayId,
|
||||
'date': dateToYYYYMMDD(instance.date),
|
||||
'impression': numToString(instance.impression),
|
||||
'notes': instance.notes,
|
||||
'time_start': timeToString(instance.timeStart),
|
||||
'time_end': timeToString(instance.timeEnd),
|
||||
};
|
||||
|
||||
@@ -7,13 +7,11 @@ part of 'session_api.dart';
|
||||
// **************************************************************************
|
||||
|
||||
WorkoutSessionApi _$WorkoutSessionApiFromJson(Map<String, dynamic> json) {
|
||||
$checkKeys(
|
||||
json,
|
||||
requiredKeys: const ['session'],
|
||||
);
|
||||
$checkKeys(json, requiredKeys: const ['session']);
|
||||
return WorkoutSessionApi(
|
||||
session: WorkoutSession.fromJson(json['session'] as Map<String, dynamic>),
|
||||
logs: (json['logs'] as List<dynamic>?)
|
||||
logs:
|
||||
(json['logs'] as List<dynamic>?)
|
||||
?.map((e) => Log.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
@@ -21,5 +19,5 @@ WorkoutSessionApi _$WorkoutSessionApiFromJson(Map<String, dynamic> json) {
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$WorkoutSessionApiToJson(WorkoutSessionApi instance) => <String, dynamic>{
|
||||
'session': instance.session,
|
||||
};
|
||||
'session': instance.session,
|
||||
};
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:wger/helpers/consts.dart';
|
||||
import 'package:wger/models/exercises/category.dart';
|
||||
import 'package:wger/models/exercises/equipment.dart';
|
||||
import 'package:wger/models/exercises/exercise.dart';
|
||||
import 'package:wger/models/exercises/exercise_submission.dart';
|
||||
import 'package:wger/models/exercises/exercise_submission_images.dart';
|
||||
import 'package:wger/models/exercises/language.dart';
|
||||
import 'package:wger/models/exercises/muscle.dart';
|
||||
import 'package:wger/models/exercises/variation.dart';
|
||||
@@ -16,11 +14,16 @@ import 'base_provider.dart';
|
||||
|
||||
class AddExerciseProvider with ChangeNotifier {
|
||||
final WgerBaseProvider baseProvider;
|
||||
static final _logger = Logger('AddExerciseProvider');
|
||||
|
||||
AddExerciseProvider(this.baseProvider);
|
||||
|
||||
List<File> get exerciseImages => [..._exerciseImages];
|
||||
List<File> _exerciseImages = [];
|
||||
// Images and their metadata (license info, style)
|
||||
final List<ExerciseSubmissionImage> _exerciseImages = [];
|
||||
|
||||
List<ExerciseSubmissionImage> get exerciseImages => [..._exerciseImages];
|
||||
|
||||
String author = '';
|
||||
String? exerciseNameEn;
|
||||
String? exerciseNameTrans;
|
||||
String? descriptionEn;
|
||||
@@ -32,7 +35,6 @@ class AddExerciseProvider with ChangeNotifier {
|
||||
List<String> alternateNamesEn = [];
|
||||
List<String> alternateNamesTrans = [];
|
||||
ExerciseCategory? category;
|
||||
List<Exercise> _variations = [];
|
||||
List<Equipment> _equipment = [];
|
||||
List<Muscle> _primaryMuscles = [];
|
||||
List<Muscle> _secondaryMuscles = [];
|
||||
@@ -42,7 +44,7 @@ class AddExerciseProvider with ChangeNotifier {
|
||||
static const _checkLanguageUrlPath = 'check-language';
|
||||
|
||||
void clear() {
|
||||
_exerciseImages = [];
|
||||
_exerciseImages.clear();
|
||||
languageTranslation = null;
|
||||
category = null;
|
||||
exerciseNameEn = null;
|
||||
@@ -51,7 +53,6 @@ class AddExerciseProvider with ChangeNotifier {
|
||||
descriptionTrans = null;
|
||||
alternateNamesEn = [];
|
||||
alternateNamesTrans = [];
|
||||
_variations = [];
|
||||
_equipment = [];
|
||||
_primaryMuscles = [];
|
||||
_secondaryMuscles = [];
|
||||
@@ -135,53 +136,40 @@ class AddExerciseProvider with ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
void addExerciseImages(List<File> exercises) {
|
||||
_exerciseImages.addAll(exercises);
|
||||
/// Add images with optional license metadata
|
||||
void addExerciseImages(List<ExerciseSubmissionImage> images) {
|
||||
_exerciseImages.addAll(images);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void removeExercise(String path) {
|
||||
final file = _exerciseImages.where((element) => element.path == path).first;
|
||||
void removeImage(String path) {
|
||||
final file = _exerciseImages.where((element) => element.imageFile.path == path).first;
|
||||
_exerciseImages.remove(file);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
//Just to Debug Provider
|
||||
void printValues() {
|
||||
log('Collected exercise data');
|
||||
log('------------------------');
|
||||
/// Main method to submit exercise with images
|
||||
///
|
||||
/// Returns the ID of the created exercise
|
||||
/// Throws exception if submission fails
|
||||
Future<int> postExerciseToServer() async {
|
||||
try {
|
||||
// 1. Create the exercise
|
||||
final exerciseId = await addExerciseSubmission();
|
||||
|
||||
log('Base data...');
|
||||
log('Target area : $category');
|
||||
log('Primary muscles: $_primaryMuscles');
|
||||
log('Secondary muscles: $_secondaryMuscles');
|
||||
log('Equipment: $_equipment');
|
||||
log('Variations: $_variations');
|
||||
// 2. Upload images if any exist
|
||||
if (_exerciseImages.isNotEmpty) {
|
||||
await addImages(exerciseId);
|
||||
}
|
||||
|
||||
log('');
|
||||
log('Language specific...');
|
||||
log('Language: ${languageTranslation?.shortName}');
|
||||
log('Name: en/$exerciseNameEn translation/$exerciseNameTrans');
|
||||
log('Description: en/$descriptionEn translation/$descriptionTrans');
|
||||
log('Alternate names: en/$alternateNamesEn translation/$alternateNamesTrans');
|
||||
}
|
||||
// 3. Clear all data after successful upload
|
||||
clear();
|
||||
|
||||
Future<int> addExercise() async {
|
||||
printValues();
|
||||
|
||||
// Create the variations if needed
|
||||
// if (newVariation) {
|
||||
// await addVariation();
|
||||
// }
|
||||
|
||||
// Create the exercise
|
||||
final exerciseId = await addExerciseSubmission();
|
||||
|
||||
// Clear everything
|
||||
clear();
|
||||
|
||||
// Return exercise ID
|
||||
return exerciseId;
|
||||
return exerciseId;
|
||||
} catch (e) {
|
||||
// Don't clear on error so user can retry
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> addExerciseSubmission() async {
|
||||
@@ -194,16 +182,34 @@ class AddExerciseProvider with ChangeNotifier {
|
||||
return result['id'];
|
||||
}
|
||||
|
||||
/// Upload exercise images with license metadata
|
||||
Future<void> addImages(int exerciseId) async {
|
||||
for (final image in _exerciseImages) {
|
||||
final request = http.MultipartRequest('POST', baseProvider.makeUrl(_imagesUrlPath));
|
||||
request.headers.addAll(baseProvider.getDefaultHeaders(includeAuth: true));
|
||||
|
||||
request.files.add(await http.MultipartFile.fromPath('image', image.path));
|
||||
request.files.add(await http.MultipartFile.fromPath('image', image.imageFile.path));
|
||||
request.fields['exercise'] = exerciseId.toString();
|
||||
request.fields['style'] = EXERCISE_IMAGE_ART_STYLE.PHOTO.index.toString();
|
||||
request.fields['license'] = CC_BY_SA_4_ID.toString();
|
||||
request.fields['is_main'] = 'false';
|
||||
|
||||
await request.send();
|
||||
final details = image.toJson();
|
||||
if (details.isNotEmpty) {
|
||||
request.fields.addAll(details);
|
||||
}
|
||||
|
||||
try {
|
||||
final streamedResponse = await request.send();
|
||||
|
||||
if (streamedResponse.statusCode == 201 || streamedResponse.statusCode == 200) {
|
||||
_logger.fine('Image uploaded successfully');
|
||||
} else {
|
||||
final response = await http.Response.fromStream(streamedResponse);
|
||||
throw Exception('Upload failed: ${streamedResponse.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
@@ -215,64 +221,7 @@ class AddExerciseProvider with ChangeNotifier {
|
||||
'language_code': languageCode,
|
||||
}, baseProvider.makeUrl(_checkLanguageUrlPath));
|
||||
notifyListeners();
|
||||
print(result);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
Note: all this logic is not needed now since we are using the /exercise-submission
|
||||
endpoint, however, if we ever want to implement editing of exercises, we will
|
||||
need basically all of it again, so this is kept here for reference.
|
||||
|
||||
|
||||
|
||||
|
||||
Future<Variation> addVariation() async {
|
||||
final Uri postUri = baseProvider.makeUrl(_exerciseVariationPath);
|
||||
|
||||
// We send an empty dictionary since at the moment the variations only have an ID
|
||||
final Map<String, dynamic> variationMap = await baseProvider.post({}, postUri);
|
||||
final Variation newVariation = Variation.fromJson(variationMap);
|
||||
_variationId = newVariation.id;
|
||||
notifyListeners();
|
||||
return newVariation;
|
||||
}
|
||||
|
||||
Future<void> addImages(Exercise exercise) async {
|
||||
for (final image in _exerciseImages) {
|
||||
final request = http.MultipartRequest('POST', baseProvider.makeUrl(_imagesUrlPath));
|
||||
request.headers.addAll(baseProvider.getDefaultHeaders(includeAuth: true));
|
||||
|
||||
request.files.add(await http.MultipartFile.fromPath('image', image.path));
|
||||
request.fields['exercise'] = exercise.id!.toString();
|
||||
request.fields['style'] = EXERCISE_IMAGE_ART_STYLE.PHOTO.index.toString();
|
||||
|
||||
await request.send();
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<Translation> addExerciseTranslation(Translation exercise) async {
|
||||
final Uri postUri = baseProvider.makeUrl(_exerciseTranslationUrlPath);
|
||||
|
||||
final Map<String, dynamic> newTranslation = await baseProvider.post(exercise.toJson(), postUri);
|
||||
final Translation newExercise = Translation.fromJson(newTranslation);
|
||||
notifyListeners();
|
||||
|
||||
return newExercise;
|
||||
}
|
||||
|
||||
Future<Alias> addExerciseAlias(String name, int exerciseId) async {
|
||||
final alias = Alias(translationId: exerciseId, alias: name);
|
||||
final Uri postUri = baseProvider.makeUrl(_exerciseAliasPath);
|
||||
|
||||
final Alias newAlias = Alias.fromJson(await baseProvider.post(alias.toJson(), postUri));
|
||||
notifyListeners();
|
||||
|
||||
return newAlias;
|
||||
}
|
||||
|
||||
*/
|
||||
}
|
||||
|
||||
@@ -251,7 +251,7 @@ class AuthProvider with ChangeNotifier {
|
||||
headers: {
|
||||
HttpHeaders.contentTypeHeader: 'application/json; charset=UTF-8',
|
||||
HttpHeaders.userAgentHeader: getAppNameHeader(),
|
||||
HttpHeaders.authorizationHeader: 'Token $token'
|
||||
HttpHeaders.authorizationHeader: 'Token $token',
|
||||
},
|
||||
);
|
||||
if (response.statusCode != 200) {
|
||||
@@ -294,7 +294,8 @@ class AuthProvider with ChangeNotifier {
|
||||
String getAppNameHeader() {
|
||||
String out = '';
|
||||
if (applicationVersion != null) {
|
||||
out = '/${applicationVersion!.version} '
|
||||
out =
|
||||
'/${applicationVersion!.version} '
|
||||
'(${applicationVersion!.packageName}; '
|
||||
'build: ${applicationVersion!.buildNumber})'
|
||||
' - https://github.com/wger-project';
|
||||
|
||||
@@ -68,10 +68,12 @@ class BodyWeightProvider with ChangeNotifier {
|
||||
_logger.info('Fetching all body weight entries');
|
||||
|
||||
// Process the response
|
||||
final data = await baseProvider.fetchPaginated(baseProvider.makeUrl(
|
||||
BODY_WEIGHT_URL,
|
||||
query: {'ordering': '-date', 'limit': API_MAX_PAGE_SIZE},
|
||||
));
|
||||
final data = await baseProvider.fetchPaginated(
|
||||
baseProvider.makeUrl(
|
||||
BODY_WEIGHT_URL,
|
||||
query: {'ordering': '-date', 'limit': API_MAX_PAGE_SIZE},
|
||||
),
|
||||
);
|
||||
_entries = [];
|
||||
for (final entry in data) {
|
||||
_entries.add(WeightEntry.fromJson(entry));
|
||||
|
||||
@@ -42,7 +42,7 @@ class ExercisesProvider with ChangeNotifier {
|
||||
ExerciseDatabase database;
|
||||
|
||||
ExercisesProvider(this.baseProvider, {ExerciseDatabase? database})
|
||||
: database = database ?? locator<ExerciseDatabase>();
|
||||
: database = database ?? locator<ExerciseDatabase>();
|
||||
|
||||
static const EXERCISE_CACHE_DAYS = 7;
|
||||
static const CACHE_VERSION = 4;
|
||||
@@ -118,17 +118,13 @@ class ExercisesProvider with ChangeNotifier {
|
||||
exerciseCategories: FilterCategory(
|
||||
title: 'Category',
|
||||
items: Map.fromEntries(
|
||||
_categories.map(
|
||||
(category) => MapEntry<ExerciseCategory, bool>(category, false),
|
||||
),
|
||||
_categories.map((category) => MapEntry<ExerciseCategory, bool>(category, false)),
|
||||
),
|
||||
),
|
||||
equipment: FilterCategory(
|
||||
title: 'Equipment',
|
||||
items: Map.fromEntries(
|
||||
_equipment.map(
|
||||
(singleEquipment) => MapEntry<Equipment, bool>(singleEquipment, false),
|
||||
),
|
||||
_equipment.map((singleEquipment) => MapEntry<Equipment, bool>(singleEquipment, false)),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -191,10 +187,7 @@ class ExercisesProvider with ChangeNotifier {
|
||||
/// returned exercises. Since this is typically called by one exercise, we are
|
||||
/// not interested in seeing that same exercise returned in the list of variations.
|
||||
/// If this parameter is not passed, all exercises are returned.
|
||||
List<Exercise> findExercisesByVariationId(
|
||||
int? variationId, {
|
||||
int? exerciseIdToExclude,
|
||||
}) {
|
||||
List<Exercise> findExercisesByVariationId(int? variationId, {int? exerciseIdToExclude}) {
|
||||
if (variationId == null) {
|
||||
return [];
|
||||
}
|
||||
@@ -317,18 +310,16 @@ class ExercisesProvider with ChangeNotifier {
|
||||
/// -> yes: Do we need to re-fetch?
|
||||
/// -> no: just return what we have in the DB
|
||||
/// -> yes: fetch data and update if necessary
|
||||
Future<Exercise> handleUpdateExerciseFromApi(
|
||||
ExerciseDatabase database,
|
||||
int exerciseId,
|
||||
) async {
|
||||
Future<Exercise> handleUpdateExerciseFromApi(ExerciseDatabase database, int exerciseId) async {
|
||||
Exercise exercise;
|
||||
|
||||
// NOTE: this should not be necessary anymore. We had a bug that would
|
||||
// create duplicate entries in the database and should be fixed now.
|
||||
// However, we keep it here for now to be on the safe side.
|
||||
// In the future this can be replaced by a .getSingleOrNull()
|
||||
final exerciseResult =
|
||||
await (database.select(database.exercises)..where((e) => e.id.equals(exerciseId))).get();
|
||||
final exerciseResult = await (database.select(
|
||||
database.exercises,
|
||||
)..where((e) => e.id.equals(exerciseId))).get();
|
||||
|
||||
ExerciseTable? exerciseDb;
|
||||
if (exerciseResult.isNotEmpty) {
|
||||
@@ -383,7 +374,9 @@ class ExercisesProvider with ChangeNotifier {
|
||||
exercise = Exercise.fromApiDataJson(exerciseData, _languages);
|
||||
|
||||
if (exerciseDb == null) {
|
||||
await database.into(database.exercises).insert(
|
||||
await database
|
||||
.into(database.exercises)
|
||||
.insert(
|
||||
ExercisesCompanion.insert(
|
||||
id: exercise.id!,
|
||||
data: jsonEncode(exerciseData),
|
||||
@@ -426,6 +419,7 @@ class ExercisesProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<void> clearAllCachesAndPrefs() async {
|
||||
clear();
|
||||
await database.deleteEverything();
|
||||
await initCacheTimesLocalPrefs(forceInit: true);
|
||||
}
|
||||
@@ -480,13 +474,15 @@ class ExercisesProvider with ChangeNotifier {
|
||||
|
||||
// Insert new entries and update ones that have been edited
|
||||
Future.forEach(data, (exerciseData) async {
|
||||
final exercise = await (database.select(database.exercises)
|
||||
..where((e) => e.id.equals(exerciseData['id'])))
|
||||
.getSingleOrNull();
|
||||
final exercise = await (database.select(
|
||||
database.exercises,
|
||||
)..where((e) => e.id.equals(exerciseData['id']))).getSingleOrNull();
|
||||
|
||||
// New exercise, insert
|
||||
if (exercise == null) {
|
||||
database.into(database.exercises).insert(
|
||||
database
|
||||
.into(database.exercises)
|
||||
.insert(
|
||||
ExercisesCompanion.insert(
|
||||
id: exerciseData['id'],
|
||||
data: jsonEncode(exerciseData),
|
||||
@@ -537,15 +533,10 @@ class ExercisesProvider with ChangeNotifier {
|
||||
await fetchAndSetMusclesFromApi();
|
||||
await database.delete(database.muscles).go();
|
||||
await Future.forEach(_muscles, (e) async {
|
||||
await database.into(database.muscles).insert(
|
||||
MusclesCompanion.insert(id: e.id, data: e),
|
||||
);
|
||||
await database.into(database.muscles).insert(MusclesCompanion.insert(id: e.id, data: e));
|
||||
});
|
||||
validTill = DateTime.now().add(const Duration(days: EXERCISE_CACHE_DAYS));
|
||||
await prefs.setString(
|
||||
PREFS_LAST_UPDATED_MUSCLES,
|
||||
validTill.toIso8601String(),
|
||||
);
|
||||
await prefs.setString(PREFS_LAST_UPDATED_MUSCLES, validTill.toIso8601String());
|
||||
_logger.fine('Saved ${_muscles.length} muscles to cache (valid till $validTill)');
|
||||
}
|
||||
|
||||
@@ -571,15 +562,12 @@ class ExercisesProvider with ChangeNotifier {
|
||||
await fetchAndSetCategoriesFromApi();
|
||||
await database.delete(database.categories).go();
|
||||
await Future.forEach(_categories, (e) async {
|
||||
await database.into(database.categories).insert(
|
||||
CategoriesCompanion.insert(id: e.id, data: e),
|
||||
);
|
||||
await database
|
||||
.into(database.categories)
|
||||
.insert(CategoriesCompanion.insert(id: e.id, data: e));
|
||||
});
|
||||
validTill = DateTime.now().add(const Duration(days: EXERCISE_CACHE_DAYS));
|
||||
await prefs.setString(
|
||||
PREFS_LAST_UPDATED_CATEGORIES,
|
||||
validTill.toIso8601String(),
|
||||
);
|
||||
await prefs.setString(PREFS_LAST_UPDATED_CATEGORIES, validTill.toIso8601String());
|
||||
_logger.fine('Saved ${_categories.length} categories to cache (valid till $validTill)');
|
||||
}
|
||||
|
||||
@@ -605,16 +593,11 @@ class ExercisesProvider with ChangeNotifier {
|
||||
await fetchAndSetLanguagesFromApi();
|
||||
await database.delete(database.languages).go();
|
||||
await Future.forEach(_languages, (e) async {
|
||||
await database.into(database.languages).insert(
|
||||
LanguagesCompanion.insert(id: e.id, data: e),
|
||||
);
|
||||
await database.into(database.languages).insert(LanguagesCompanion.insert(id: e.id, data: e));
|
||||
});
|
||||
|
||||
validTill = DateTime.now().add(const Duration(days: EXERCISE_CACHE_DAYS));
|
||||
await prefs.setString(
|
||||
PREFS_LAST_UPDATED_LANGUAGES,
|
||||
validTill.toIso8601String(),
|
||||
);
|
||||
await prefs.setString(PREFS_LAST_UPDATED_LANGUAGES, validTill.toIso8601String());
|
||||
_logger.info('Saved ${languages.length} languages to cache (valid till $validTill)');
|
||||
}
|
||||
|
||||
@@ -640,15 +623,12 @@ class ExercisesProvider with ChangeNotifier {
|
||||
await fetchAndSetEquipmentsFromApi();
|
||||
await database.delete(database.equipments).go();
|
||||
await Future.forEach(_equipment, (e) async {
|
||||
await database.into(database.equipments).insert(
|
||||
EquipmentsCompanion.insert(id: e.id, data: e),
|
||||
);
|
||||
await database
|
||||
.into(database.equipments)
|
||||
.insert(EquipmentsCompanion.insert(id: e.id, data: e));
|
||||
});
|
||||
validTill = DateTime.now().add(const Duration(days: EXERCISE_CACHE_DAYS));
|
||||
await prefs.setString(
|
||||
PREFS_LAST_UPDATED_EQUIPMENT,
|
||||
validTill.toIso8601String(),
|
||||
);
|
||||
await prefs.setString(PREFS_LAST_UPDATED_EQUIPMENT, validTill.toIso8601String());
|
||||
_logger.fine('Saved ${_equipment.length} equipment entries to cache (valid till $validTill)');
|
||||
}
|
||||
|
||||
@@ -703,17 +683,9 @@ class FilterCategory<T> {
|
||||
|
||||
List<T> get selected => [...items.keys].where((key) => items[key]!).toList();
|
||||
|
||||
FilterCategory({
|
||||
required this.title,
|
||||
required this.items,
|
||||
this.isExpanded = false,
|
||||
});
|
||||
FilterCategory({required this.title, required this.items, this.isExpanded = false});
|
||||
|
||||
FilterCategory<T> copyWith({
|
||||
bool? isExpanded,
|
||||
Map<T, bool>? items,
|
||||
String? title,
|
||||
}) {
|
||||
FilterCategory<T> copyWith({bool? isExpanded, Map<T, bool>? items, String? title}) {
|
||||
return FilterCategory(
|
||||
isExpanded: isExpanded ?? this.isExpanded,
|
||||
items: items ?? this.items,
|
||||
|
||||
@@ -8,8 +8,8 @@ const DEFAULT_DURATION = Duration(hours: 5);
|
||||
|
||||
final StateNotifierProvider<GymStateNotifier, GymState> gymStateProvider =
|
||||
StateNotifierProvider<GymStateNotifier, GymState>((ref) {
|
||||
return GymStateNotifier();
|
||||
});
|
||||
return GymStateNotifier();
|
||||
});
|
||||
|
||||
class GymState {
|
||||
final Map<Exercise, int> exercisePages;
|
||||
@@ -19,13 +19,14 @@ class GymState {
|
||||
late TimeOfDay startTime;
|
||||
late DateTime validUntil;
|
||||
|
||||
GymState(
|
||||
{this.exercisePages = const {},
|
||||
this.showExercisePages = true,
|
||||
this.currentPage = 0,
|
||||
this.dayId,
|
||||
DateTime? validUntil,
|
||||
TimeOfDay? startTime}) {
|
||||
GymState({
|
||||
this.exercisePages = const {},
|
||||
this.showExercisePages = true,
|
||||
this.currentPage = 0,
|
||||
this.dayId,
|
||||
DateTime? validUntil,
|
||||
TimeOfDay? startTime,
|
||||
}) {
|
||||
this.validUntil = validUntil ?? DateTime.now().add(DEFAULT_DURATION);
|
||||
this.startTime = startTime ?? TimeOfDay.fromDateTime(clock.now());
|
||||
}
|
||||
|
||||
@@ -133,8 +133,9 @@ class MeasurementProvider with ChangeNotifier {
|
||||
tempNewCategory.toJson(),
|
||||
baseProvider.makeUrl(_categoryUrl, id: id),
|
||||
);
|
||||
final MeasurementCategory newCategory =
|
||||
MeasurementCategory.fromJson(response).copyWith(entries: oldCategory.entries);
|
||||
final MeasurementCategory newCategory = MeasurementCategory.fromJson(
|
||||
response,
|
||||
).copyWith(entries: oldCategory.entries);
|
||||
_categories.removeAt(categoryIndex);
|
||||
_categories.insert(categoryIndex, newCategory);
|
||||
notifyListeners();
|
||||
@@ -192,8 +193,10 @@ class MeasurementProvider with ChangeNotifier {
|
||||
date: newDate,
|
||||
);
|
||||
|
||||
final Map<String, dynamic> response =
|
||||
await baseProvider.patch(tempNewEntry.toJson(), baseProvider.makeUrl(_entryUrl, id: id));
|
||||
final Map<String, dynamic> response = await baseProvider.patch(
|
||||
tempNewEntry.toJson(),
|
||||
baseProvider.makeUrl(_entryUrl, id: id),
|
||||
);
|
||||
|
||||
final MeasurementEntry newEntry = MeasurementEntry.fromJson(response);
|
||||
category.entries.removeAt(entryIndex);
|
||||
|
||||
@@ -49,9 +49,11 @@ class NutritionPlansProvider with ChangeNotifier {
|
||||
List<NutritionalPlan> _plans = [];
|
||||
List<Ingredient> ingredients = [];
|
||||
|
||||
NutritionPlansProvider(this.baseProvider, List<NutritionalPlan> entries,
|
||||
{IngredientDatabase? database})
|
||||
: _plans = entries {
|
||||
NutritionPlansProvider(
|
||||
this.baseProvider,
|
||||
List<NutritionalPlan> entries, {
|
||||
IngredientDatabase? database,
|
||||
}) : _plans = entries {
|
||||
this.database = database ?? locator<IngredientDatabase>();
|
||||
}
|
||||
|
||||
@@ -73,8 +75,10 @@ class NutritionPlansProvider with ChangeNotifier {
|
||||
NutritionalPlan? get currentPlan {
|
||||
final now = DateTime.now();
|
||||
return _plans
|
||||
.where((plan) =>
|
||||
plan.startDate.isBefore(now) && (plan.endDate == null || plan.endDate!.isAfter(now)))
|
||||
.where(
|
||||
(plan) =>
|
||||
plan.startDate.isBefore(now) && (plan.endDate == null || plan.endDate!.isAfter(now)),
|
||||
)
|
||||
.toList()
|
||||
.sorted((a, b) => b.creationDate.compareTo(a.creationDate))
|
||||
.firstOrNull;
|
||||
@@ -114,10 +118,9 @@ class NutritionPlansProvider with ChangeNotifier {
|
||||
|
||||
/// Fetches and sets all plans fully, i.e. with all corresponding child objects
|
||||
Future<void> fetchAndSetAllPlansFull() async {
|
||||
final data = await baseProvider.fetchPaginated(baseProvider.makeUrl(
|
||||
_nutritionalPlansPath,
|
||||
query: {'limit': API_MAX_PAGE_SIZE},
|
||||
));
|
||||
final data = await baseProvider.fetchPaginated(
|
||||
baseProvider.makeUrl(_nutritionalPlansPath, query: {'limit': API_MAX_PAGE_SIZE}),
|
||||
);
|
||||
await Future.wait(data.map((e) => fetchAndSetPlanFull(e['id'])).toList());
|
||||
}
|
||||
|
||||
@@ -225,10 +228,7 @@ class NutritionPlansProvider with ChangeNotifier {
|
||||
/// Adds a meal to a plan
|
||||
Future<Meal> addMeal(Meal meal, int planId) async {
|
||||
final plan = findById(planId);
|
||||
final data = await baseProvider.post(
|
||||
meal.toJson(),
|
||||
baseProvider.makeUrl(_mealPath),
|
||||
);
|
||||
final data = await baseProvider.post(meal.toJson(), baseProvider.makeUrl(_mealPath));
|
||||
|
||||
meal = Meal.fromJson(data);
|
||||
plan.meals.add(meal);
|
||||
@@ -269,10 +269,7 @@ class NutritionPlansProvider with ChangeNotifier {
|
||||
|
||||
/// Adds a meal item to a meal
|
||||
Future<MealItem> addMealItem(MealItem mealItem, Meal meal) async {
|
||||
final data = await baseProvider.post(
|
||||
mealItem.toJson(),
|
||||
baseProvider.makeUrl(_mealItemPath),
|
||||
);
|
||||
final data = await baseProvider.post(mealItem.toJson(), baseProvider.makeUrl(_mealItemPath));
|
||||
|
||||
mealItem = MealItem.fromJson(data);
|
||||
mealItem.ingredient = await fetchIngredient(mealItem.ingredientId);
|
||||
@@ -301,6 +298,7 @@ class NutritionPlansProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<void> clearIngredientCache() async {
|
||||
ingredients = [];
|
||||
await database.deleteEverything();
|
||||
}
|
||||
|
||||
@@ -314,9 +312,9 @@ class NutritionPlansProvider with ChangeNotifier {
|
||||
try {
|
||||
ingredient = ingredients.firstWhere((e) => e.id == ingredientId);
|
||||
} on StateError {
|
||||
final ingredientDb = await (database.select(database.ingredients)
|
||||
..where((e) => e.id.equals(ingredientId)))
|
||||
.getSingleOrNull();
|
||||
final ingredientDb = await (database.select(
|
||||
database.ingredients,
|
||||
)..where((e) => e.id.equals(ingredientId))).getSingleOrNull();
|
||||
|
||||
// Try to fetch from local db
|
||||
if (ingredientDb != null) {
|
||||
@@ -325,8 +323,9 @@ class NutritionPlansProvider with ChangeNotifier {
|
||||
_logger.info("Loaded ingredient '${ingredient.name}' from db cache");
|
||||
|
||||
// Prune old entries
|
||||
if (DateTime.now()
|
||||
.isAfter(ingredientDb.lastFetched.add(const Duration(days: DAYS_TO_CACHE)))) {
|
||||
if (DateTime.now().isAfter(
|
||||
ingredientDb.lastFetched.add(const Duration(days: DAYS_TO_CACHE)),
|
||||
)) {
|
||||
(database.delete(database.ingredients)..where((i) => i.id.equals(ingredientId))).go();
|
||||
}
|
||||
} else {
|
||||
@@ -336,7 +335,9 @@ class NutritionPlansProvider with ChangeNotifier {
|
||||
ingredient = Ingredient.fromJson(data);
|
||||
ingredients.add(ingredient);
|
||||
|
||||
database.into(database.ingredients).insert(
|
||||
database
|
||||
.into(database.ingredients)
|
||||
.insert(
|
||||
IngredientsCompanion.insert(
|
||||
id: ingredientId,
|
||||
data: jsonEncode(data),
|
||||
@@ -415,10 +416,7 @@ class NutritionPlansProvider with ChangeNotifier {
|
||||
final plan = findById(meal.planId);
|
||||
final Log log = Log.fromMealItem(item, plan.id!, meal.id, mealDateTime);
|
||||
|
||||
final data = await baseProvider.post(
|
||||
log.toJson(),
|
||||
baseProvider.makeUrl(_nutritionDiaryPath),
|
||||
);
|
||||
final data = await baseProvider.post(log.toJson(), baseProvider.makeUrl(_nutritionDiaryPath));
|
||||
log.id = data['id'];
|
||||
plan.diaryEntries.add(log);
|
||||
}
|
||||
@@ -426,19 +424,12 @@ class NutritionPlansProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
/// Log custom ingredient to nutrition diary
|
||||
Future<void> logIngredientToDiary(
|
||||
MealItem mealItem,
|
||||
int planId, [
|
||||
DateTime? dateTime,
|
||||
]) async {
|
||||
Future<void> logIngredientToDiary(MealItem mealItem, int planId, [DateTime? dateTime]) async {
|
||||
final plan = findById(planId);
|
||||
mealItem.ingredient = await fetchIngredient(mealItem.ingredientId);
|
||||
final log = Log.fromMealItem(mealItem, plan.id!, null, dateTime);
|
||||
|
||||
final data = await baseProvider.post(
|
||||
log.toJson(),
|
||||
baseProvider.makeUrl(_nutritionDiaryPath),
|
||||
);
|
||||
final data = await baseProvider.post(log.toJson(), baseProvider.makeUrl(_nutritionDiaryPath));
|
||||
log.id = data['id'];
|
||||
plan.diaryEntries.add(log);
|
||||
notifyListeners();
|
||||
@@ -458,11 +449,7 @@ class NutritionPlansProvider with ChangeNotifier {
|
||||
final data = await baseProvider.fetchPaginated(
|
||||
baseProvider.makeUrl(
|
||||
_nutritionDiaryPath,
|
||||
query: {
|
||||
'plan': plan.id?.toString(),
|
||||
'limit': API_MAX_PAGE_SIZE,
|
||||
'ordering': 'datetime',
|
||||
},
|
||||
query: {'plan': plan.id?.toString(), 'limit': API_MAX_PAGE_SIZE, 'ordering': 'datetime'},
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ const PREFS_KEY_PLATES = 'selectedPlates';
|
||||
|
||||
final plateCalculatorProvider =
|
||||
StateNotifierProvider<PlateCalculatorNotifier, PlateCalculatorState>((ref) {
|
||||
return PlateCalculatorNotifier();
|
||||
});
|
||||
return PlateCalculatorNotifier();
|
||||
});
|
||||
|
||||
class PlateCalculatorState {
|
||||
final _logger = Logger('PlateWeightsState');
|
||||
@@ -65,17 +65,18 @@ class PlateCalculatorState {
|
||||
this.isMetric = true,
|
||||
this.totalWeight = 0,
|
||||
List<num>? selectedPlates,
|
||||
}) : barWeight = barWeight ?? (isMetric ? DEFAULT_BAR_WEIGHT_KG : DEFAULT_BAR_WEIGHT_LB),
|
||||
selectedPlates =
|
||||
selectedPlates ?? (isMetric ? [...DEFAULT_KG_PLATES] : [...DEFAULT_LB_PLATES]);
|
||||
}) : barWeight = barWeight ?? (isMetric ? DEFAULT_BAR_WEIGHT_KG : DEFAULT_BAR_WEIGHT_LB),
|
||||
selectedPlates =
|
||||
selectedPlates ?? (isMetric ? [...DEFAULT_KG_PLATES] : [...DEFAULT_LB_PLATES]);
|
||||
|
||||
PlateCalculatorState.fromJson(Map<String, dynamic> plateData)
|
||||
: useColors = plateData['useColors'] ?? true,
|
||||
isMetric = plateData['isMetric'] ?? true,
|
||||
selectedPlates = plateData['selectedPlates']?.cast<num>() ?? [...DEFAULT_KG_PLATES],
|
||||
barWeight = plateData['barWeight'] ??
|
||||
((plateData['isMetric'] ?? true) ? DEFAULT_BAR_WEIGHT_KG : DEFAULT_BAR_WEIGHT_LB),
|
||||
totalWeight = 0;
|
||||
: useColors = plateData['useColors'] ?? true,
|
||||
isMetric = plateData['isMetric'] ?? true,
|
||||
selectedPlates = plateData['selectedPlates']?.cast<num>() ?? [...DEFAULT_KG_PLATES],
|
||||
barWeight =
|
||||
plateData['barWeight'] ??
|
||||
((plateData['isMetric'] ?? true) ? DEFAULT_BAR_WEIGHT_KG : DEFAULT_BAR_WEIGHT_LB),
|
||||
totalWeight = 0;
|
||||
|
||||
PlateCalculatorState copyWith({
|
||||
bool? useColors,
|
||||
|
||||
@@ -279,8 +279,9 @@ class RoutinesProvider with ChangeNotifier {
|
||||
final dayDataEntriesGym = dayDataGym.map((entry) => DayData.fromJson(entry)).toList();
|
||||
await setExercisesAndUnits(dayDataEntriesGym, exercises: exercises);
|
||||
|
||||
final sessionDataEntries =
|
||||
sessionData.map((entry) => WorkoutSessionApi.fromJson(entry)).toList();
|
||||
final sessionDataEntries = sessionData
|
||||
.map((entry) => WorkoutSessionApi.fromJson(entry))
|
||||
.toList();
|
||||
|
||||
for (final day in routine.days) {
|
||||
for (final slot in day.slots) {
|
||||
@@ -322,8 +323,9 @@ class RoutinesProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
if (!exercises.containsKey(log.exerciseId)) {
|
||||
exercises[log.exerciseId] =
|
||||
(await _exerciseProvider.fetchAndSetExercise(log.exerciseId))!;
|
||||
exercises[log.exerciseId] = (await _exerciseProvider.fetchAndSetExercise(
|
||||
log.exerciseId,
|
||||
))!;
|
||||
}
|
||||
|
||||
log.exerciseBase = exercises[log.exerciseId]!;
|
||||
@@ -378,8 +380,9 @@ class RoutinesProvider with ChangeNotifier {
|
||||
|
||||
/// Fetch and set weight units for workout (kg, lb, plate, etc.)
|
||||
Future<void> fetchAndSetRepetitionUnits() async {
|
||||
final response =
|
||||
await baseProvider.fetchPaginated(baseProvider.makeUrl(_repetitionUnitUrlPath));
|
||||
final response = await baseProvider.fetchPaginated(
|
||||
baseProvider.makeUrl(_repetitionUnitUrlPath),
|
||||
);
|
||||
for (final unit in response) {
|
||||
_repetitionUnits.add(RepetitionUnit.fromJson(unit));
|
||||
}
|
||||
|
||||
@@ -101,10 +101,12 @@ class UserProvider with ChangeNotifier {
|
||||
|
||||
/// Verify the user's email
|
||||
Future<void> verifyEmail() async {
|
||||
await baseProvider.fetch(baseProvider.makeUrl(
|
||||
PROFILE_URL,
|
||||
objectMethod: VERIFY_EMAIL,
|
||||
));
|
||||
await baseProvider.fetch(
|
||||
baseProvider.makeUrl(
|
||||
PROFILE_URL,
|
||||
objectMethod: VERIFY_EMAIL,
|
||||
),
|
||||
);
|
||||
//log(verificationData.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,12 @@ import 'package:wger/providers/add_exercise.dart';
|
||||
import 'package:wger/providers/exercises.dart';
|
||||
import 'package:wger/providers/user.dart';
|
||||
import 'package:wger/screens/exercise_screen.dart';
|
||||
import 'package:wger/widgets/add_exercise/steps/step1basics.dart';
|
||||
import 'package:wger/widgets/add_exercise/steps/step2variations.dart';
|
||||
import 'package:wger/widgets/add_exercise/steps/step3description.dart';
|
||||
import 'package:wger/widgets/add_exercise/steps/step4translations.dart';
|
||||
import 'package:wger/widgets/add_exercise/steps/step5images.dart';
|
||||
import 'package:wger/widgets/add_exercise/steps/step6Overview.dart';
|
||||
import 'package:wger/widgets/add_exercise/steps/step_1_basics.dart';
|
||||
import 'package:wger/widgets/add_exercise/steps/step_2_variations.dart';
|
||||
import 'package:wger/widgets/add_exercise/steps/step_3_description.dart';
|
||||
import 'package:wger/widgets/add_exercise/steps/step_4_translations.dart';
|
||||
import 'package:wger/widgets/add_exercise/steps/step_5_images.dart';
|
||||
import 'package:wger/widgets/add_exercise/steps/step_6_overview.dart';
|
||||
import 'package:wger/widgets/core/app_bar.dart';
|
||||
import 'package:wger/widgets/user/forms.dart';
|
||||
|
||||
@@ -85,10 +85,8 @@ class _AddExerciseStepperState extends State<AddExerciseStepper> {
|
||||
|
||||
Exercise? exercise;
|
||||
try {
|
||||
final exerciseId = await addExerciseProvider.addExercise();
|
||||
await addExerciseProvider.addImages(exerciseId);
|
||||
final exerciseId = await addExerciseProvider.postExerciseToServer();
|
||||
exercise = await exerciseProvider.fetchAndSetExercise(exerciseId);
|
||||
addExerciseProvider.clear();
|
||||
} on WgerHttpException catch (error) {
|
||||
if (context.mounted) {
|
||||
setState(() {
|
||||
|
||||
@@ -182,11 +182,11 @@ class _AuthCardState extends State<AuthCard> {
|
||||
late LoginActions res;
|
||||
if (_authMode == AuthMode.Login) {
|
||||
res = await context.read<AuthProvider>().login(
|
||||
_authData['username']!,
|
||||
_authData['password']!,
|
||||
_authData['serverUrl']!,
|
||||
_authData['apiToken'],
|
||||
);
|
||||
_authData['username']!,
|
||||
_authData['password']!,
|
||||
_authData['serverUrl']!,
|
||||
_authData['apiToken'],
|
||||
);
|
||||
|
||||
// Register new user
|
||||
} else {
|
||||
@@ -287,8 +287,9 @@ class _AuthCardState extends State<AuthCard> {
|
||||
labelText: i18n.confirmPassword,
|
||||
prefixIcon: const Icon(Icons.password),
|
||||
suffixIcon: IconButton(
|
||||
icon:
|
||||
Icon(confirmIsObscure ? Icons.visibility_off : Icons.visibility),
|
||||
icon: Icon(
|
||||
confirmIsObscure ? Icons.visibility_off : Icons.visibility,
|
||||
),
|
||||
onPressed: () {
|
||||
updateState(() {
|
||||
confirmIsObscure = !confirmIsObscure;
|
||||
@@ -374,8 +375,9 @@ class _AuthCardState extends State<AuthCard> {
|
||||
Builder(
|
||||
key: const Key('toggleActionButton'),
|
||||
builder: (context) {
|
||||
final String text =
|
||||
_authMode != AuthMode.Register ? i18n.registerInstead : i18n.loginInstead;
|
||||
final String text = _authMode != AuthMode.Register
|
||||
? i18n.registerInstead
|
||||
: i18n.loginInstead;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => _switchAuthMode(),
|
||||
|
||||
@@ -40,8 +40,9 @@ class GymModeScreen extends StatelessWidget {
|
||||
|
||||
final routinesProvider = context.read<RoutinesProvider>();
|
||||
final routine = routinesProvider.findById(args.routineId);
|
||||
final dayDataDisplay =
|
||||
routine.dayData.firstWhere((e) => e.iteration == args.iteration && e.day?.id == args.dayId);
|
||||
final dayDataDisplay = routine.dayData.firstWhere(
|
||||
(e) => e.iteration == args.iteration && e.day?.id == args.dayId,
|
||||
);
|
||||
final dayDataGym = routine.dayDataGym
|
||||
.where((e) => e.iteration == args.iteration && e.day?.id == args.dayId)
|
||||
.first;
|
||||
|
||||
@@ -126,8 +126,10 @@ class NutritionalPlanScreen extends StatelessWidget {
|
||||
);
|
||||
break;
|
||||
case NutritionalPlanOptions.delete:
|
||||
Provider.of<NutritionPlansProvider>(context, listen: false)
|
||||
.deletePlan(nutritionalPlan.id!);
|
||||
Provider.of<NutritionPlansProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
).deletePlan(nutritionalPlan.id!);
|
||||
Navigator.of(context).pop();
|
||||
break;
|
||||
}
|
||||
@@ -165,22 +167,22 @@ class NutritionalPlanScreen extends StatelessWidget {
|
||||
future: _loadFullPlan(context, nutritionalPlan.id!),
|
||||
builder: (context, AsyncSnapshot<NutritionalPlan> snapshot) =>
|
||||
snapshot.connectionState == ConnectionState.waiting
|
||||
? SliverList(
|
||||
delegate: SliverChildListDelegate(
|
||||
[
|
||||
const SizedBox(
|
||||
height: 200,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
],
|
||||
? SliverList(
|
||||
delegate: SliverChildListDelegate(
|
||||
[
|
||||
const SizedBox(
|
||||
height: 200,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
)
|
||||
: Consumer<NutritionPlansProvider>(
|
||||
builder: (context, value, child) =>
|
||||
NutritionalPlanDetailWidget(nutritionalPlan),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Consumer<NutritionPlansProvider>(
|
||||
builder: (context, value, child) =>
|
||||
NutritionalPlanDetailWidget(nutritionalPlan),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -33,8 +33,9 @@ class _AddExerciseMultiselectButtonState<T> extends State<AddExerciseMultiselect
|
||||
child: MultiSelectDialogField(
|
||||
initialValue: widget.initialItems,
|
||||
onSaved: widget.onSaved,
|
||||
items:
|
||||
widget.items.map((item) => MultiSelectItem<T>(item, widget.displayName(item))).toList(),
|
||||
items: widget.items
|
||||
.map((item) => MultiSelectItem<T>(item, widget.displayName(item)))
|
||||
.toList(),
|
||||
onConfirm: (value) {
|
||||
setState(() {
|
||||
_selectedItems = value.cast<T>();
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AddExerciseTextArea extends StatelessWidget {
|
||||
const AddExerciseTextArea({
|
||||
AddExerciseTextArea({
|
||||
super.key,
|
||||
required this.onChange,
|
||||
required this.title,
|
||||
ValueChanged<String>? onChange,
|
||||
this.helperText = '',
|
||||
this.isRequired = true,
|
||||
this.isMultiline = false,
|
||||
this.initialValue = '',
|
||||
this.validator,
|
||||
this.onSaved,
|
||||
});
|
||||
}) : onChange = onChange ?? ((String value) {});
|
||||
|
||||
final ValueChanged<String> onChange;
|
||||
final bool isRequired;
|
||||
final bool isMultiline;
|
||||
final String title;
|
||||
final String helperText;
|
||||
final String? initialValue;
|
||||
final FormFieldValidator<String?>? validator;
|
||||
final FormFieldSetter<String?>? onSaved;
|
||||
|
||||
@@ -28,6 +28,7 @@ class AddExerciseTextArea extends StatelessWidget {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: TextFormField(
|
||||
initialValue: initialValue,
|
||||
keyboardType: isMultiline ? TextInputType.multiline : TextInputType.text,
|
||||
maxLines: isMultiline ? null : DEFAULT_LINES,
|
||||
minLines: isMultiline ? MULTILINE_MIN_LINES : DEFAULT_LINES,
|
||||
@@ -35,12 +36,11 @@ class AddExerciseTextArea extends StatelessWidget {
|
||||
onSaved: onSaved,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
border: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(10))),
|
||||
labelText: title,
|
||||
alignLabelWithHint: true,
|
||||
helperText: helperText,
|
||||
helperMaxLines: 3,
|
||||
),
|
||||
onChanged: onChange,
|
||||
),
|
||||
|
||||
261
lib/widgets/add_exercise/image_details_form.dart
Normal file
261
lib/widgets/add_exercise/image_details_form.dart
Normal file
@@ -0,0 +1,261 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:wger/core/validators.dart';
|
||||
import 'package:wger/helpers/exercises/validators.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/models/exercises/exercise_submission_images.dart';
|
||||
import 'package:wger/widgets/add_exercise/license_info_widget.dart';
|
||||
|
||||
import 'add_exercise_text_area.dart';
|
||||
|
||||
/// Form for collecting CC BY-SA 4.0 license metadata for exercise images
|
||||
///
|
||||
/// This form is displayed after image selection in Step 5 of exercise creation.
|
||||
/// It collects all required and optional license attribution fields:
|
||||
///
|
||||
/// Required by CC BY-SA 4.0:
|
||||
/// - Author name
|
||||
/// - License type (implicitly CC BY-SA 4.0)
|
||||
///
|
||||
/// Optional but recommended:
|
||||
/// - Title (helps identify the image)
|
||||
/// - Source URL (where image was found)
|
||||
/// - Author URL (author's website/profile)
|
||||
/// - Derivative source URL (if modified from another work)
|
||||
/// - Image style (PHOTO, 3D, LINE, LOW-POLY, OTHER)
|
||||
///
|
||||
/// All metadata is sent to the API's /exerciseimage endpoint along with
|
||||
/// the image file when the exercise is submitted.
|
||||
class ImageDetailsForm extends StatefulWidget {
|
||||
final Function(ExerciseSubmissionImage image) onAdd;
|
||||
final VoidCallback onCancel;
|
||||
final ExerciseSubmissionImage submissionImage;
|
||||
|
||||
const ImageDetailsForm({
|
||||
super.key,
|
||||
required this.submissionImage,
|
||||
required this.onAdd,
|
||||
required this.onCancel,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ImageDetailsForm> createState() => _ImageDetailsFormState();
|
||||
}
|
||||
|
||||
class _ImageDetailsFormState extends State<ImageDetailsForm> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
/// Currently selected image type
|
||||
ImageType _selectedImageType = ImageType.photo;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final i18n = AppLocalizations.of(context);
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context).imageDetailsTitle,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
ImagePreview(imageFile: widget.submissionImage.imageFile),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Author name - required for proper CC BY-SA attribution
|
||||
AddExerciseTextArea(
|
||||
title: '${AppLocalizations.of(context).author}*',
|
||||
initialValue: widget.submissionImage.author,
|
||||
onSaved: (value) => widget.submissionImage.author = value,
|
||||
validator: (name) => validateAuthorName(name, context),
|
||||
),
|
||||
|
||||
// License title field - helps identify the image
|
||||
AddExerciseTextArea(
|
||||
title: AppLocalizations.of(context).imageDetailsLicenseTitle,
|
||||
helperText: AppLocalizations.of(context).imageDetailsLicenseTitleHint,
|
||||
initialValue: widget.submissionImage.title,
|
||||
onSaved: (value) => widget.submissionImage.title = value,
|
||||
),
|
||||
|
||||
// Source URL - where the image was found (license_object_url in API)
|
||||
AddExerciseTextArea(
|
||||
title: AppLocalizations.of(context).imageDetailsSourceLink,
|
||||
initialValue: widget.submissionImage.sourceUrl,
|
||||
onSaved: (value) => widget.submissionImage.sourceUrl = value,
|
||||
validator: (value) => validateUrl(value, i18n, required: false),
|
||||
),
|
||||
|
||||
// Author's website/profile URL
|
||||
AddExerciseTextArea(
|
||||
title: AppLocalizations.of(context).imageDetailsAuthorLink,
|
||||
initialValue: widget.submissionImage.authorUrl,
|
||||
onSaved: (value) => widget.submissionImage.authorUrl = value,
|
||||
validator: (value) => validateUrl(value, i18n, required: false),
|
||||
),
|
||||
|
||||
// Original source if this is a derivative work (modified from another image)
|
||||
AddExerciseTextArea(
|
||||
title: AppLocalizations.of(context).imageDetailsDerivativeSource,
|
||||
helperText: AppLocalizations.of(context).imageDetailsDerivativeHelp,
|
||||
initialValue: widget.submissionImage.derivativeSourceUrl,
|
||||
onSaved: (value) => widget.submissionImage.derivativeSourceUrl = value,
|
||||
validator: (value) => validateUrl(value, i18n, required: false),
|
||||
),
|
||||
|
||||
ImageTypeSelector(
|
||||
selectedType: _selectedImageType,
|
||||
onTypeSelected: (type) {
|
||||
setState(() {
|
||||
_selectedImageType = type;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// License info as separate widget for better optimization
|
||||
const LicenseInfoWidget(),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
_buildButtons(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildButtons() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: widget.onCancel,
|
||||
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
_formKey.currentState?.save();
|
||||
|
||||
// Pass image and metadata back to parent
|
||||
widget.onAdd(widget.submissionImage);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12),
|
||||
),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).add,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ImagePreview extends StatelessWidget {
|
||||
final File imageFile;
|
||||
|
||||
const ImagePreview({super.key, required this.imageFile});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 300, maxHeight: 200),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.file(imageFile, fit: BoxFit.contain),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ImageTypeSelector extends StatelessWidget {
|
||||
final ImageType selectedType;
|
||||
final ValueChanged<ImageType> onTypeSelected;
|
||||
|
||||
const ImageTypeSelector({super.key, required this.selectedType, required this.onTypeSelected});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final i18n = AppLocalizations.of(context);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
i18n.imageDetailsImageType,
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: ImageType.values.map((type) {
|
||||
final isSelected = selectedType == type;
|
||||
return InkWell(
|
||||
onTap: () => onTypeSelected(type),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Container(
|
||||
width: 90,
|
||||
height: 90,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? theme.buttonTheme.colorScheme!.primary
|
||||
: theme.buttonTheme.colorScheme!.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
type.icon,
|
||||
size: 32,
|
||||
color: isSelected
|
||||
? theme.buttonTheme.colorScheme!.onPrimary
|
||||
: theme.buttonTheme.colorScheme!.primary,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
type.label,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: isSelected
|
||||
? theme.buttonTheme.colorScheme!.onPrimary
|
||||
: theme.buttonTheme.colorScheme!.primary,
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
67
lib/widgets/add_exercise/license_info_widget.dart
Normal file
67
lib/widgets/add_exercise/license_info_widget.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
|
||||
/// Static widget displaying CC BY-SA 4.0 license notice for image uploads
|
||||
///
|
||||
/// This widget informs users that by uploading images, they agree to release
|
||||
/// them under the CC BY-SA 4.0 license. The license name is clickable and
|
||||
/// opens the Creative Commons license page.
|
||||
///
|
||||
/// Being a separate widget allows Flutter to optimize rendering since
|
||||
/// this content never changes.
|
||||
class LicenseInfoWidget extends StatelessWidget {
|
||||
const LicenseInfoWidget({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final i18n = AppLocalizations.of(context);
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: colorScheme.outline),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Icons.info_outline, size: 20, color: colorScheme.onSurfaceVariant),
|
||||
const SizedBox(width: 8),
|
||||
|
||||
Expanded(
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
style: TextStyle(fontSize: 12, color: colorScheme.onSurfaceVariant),
|
||||
children: [
|
||||
TextSpan(text: i18n.imageDetailsLicenseNotice),
|
||||
WidgetSpan(
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
final url = Uri.parse('https://creativecommons.org/licenses/by-sa/4.0/');
|
||||
if (await canLaunchUrl(url)) {
|
||||
await launchUrl(url, mode: LaunchMode.externalApplication);
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
i18n.imageDetailsLicenseNoticeLinkToLicense,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/models/exercises/exercise_submission_images.dart';
|
||||
import 'package:wger/providers/add_exercise.dart';
|
||||
|
||||
const validFileExtensions = ['jpg', 'jpeg', 'png', 'webp'];
|
||||
@@ -31,19 +32,21 @@ mixin ExerciseImagePickerMixin {
|
||||
images = await imagePicker.pickMultiImage();
|
||||
}
|
||||
|
||||
final selectedImages = <File>[];
|
||||
final selectedImages = <ExerciseSubmissionImage>[];
|
||||
if (images != null) {
|
||||
selectedImages.addAll(images.map((e) => File(e.path)).toList());
|
||||
selectedImages.addAll(
|
||||
images.map((e) => ExerciseSubmissionImage(imageFile: File(e.path))).toList(),
|
||||
);
|
||||
|
||||
for (final image in selectedImages) {
|
||||
bool isFileValid = true;
|
||||
String errorMessage = '';
|
||||
|
||||
if (!_validateFileType(image)) {
|
||||
if (!_validateFileType(image.imageFile)) {
|
||||
isFileValid = false;
|
||||
errorMessage = "Select only 'jpg', 'jpeg', 'png', 'webp' files";
|
||||
}
|
||||
if (_validateFileSize(image.lengthSync())) {
|
||||
if (_validateFileSize(image.imageFile.lengthSync())) {
|
||||
isFileValid = true;
|
||||
errorMessage = 'File Size should not be greater than 20 mb';
|
||||
}
|
||||
|
||||
@@ -2,72 +2,103 @@ import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/models/exercises/exercise_submission_images.dart';
|
||||
import 'package:wger/providers/add_exercise.dart';
|
||||
|
||||
import 'mixins/image_picker_mixin.dart';
|
||||
|
||||
class PreviewExerciseImages extends StatelessWidget with ExerciseImagePickerMixin {
|
||||
final List<File> selectedImages;
|
||||
/// Widget to preview selected exercise images
|
||||
///
|
||||
/// Displays images in a horizontal scrollable list with thumbnails.
|
||||
/// Each image shows a preview thumbnail and optionally a delete button.
|
||||
/// Can optionally include an "add more" button at the end of the list.
|
||||
class PreviewExerciseImages extends StatelessWidget {
|
||||
final List<ExerciseSubmissionImage> selectedImages;
|
||||
final VoidCallback? onAddMore;
|
||||
final bool allowEdit;
|
||||
|
||||
const PreviewExerciseImages({super.key, required this.selectedImages, this.allowEdit = true});
|
||||
const PreviewExerciseImages({
|
||||
super.key,
|
||||
required this.selectedImages,
|
||||
this.onAddMore,
|
||||
this.allowEdit = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Calculate item count: images + optional "add more" button
|
||||
final itemCount = selectedImages.length + (allowEdit && onAddMore != null ? 1 : 0);
|
||||
|
||||
return SizedBox(
|
||||
height: 300,
|
||||
child: ListView(scrollDirection: Axis.horizontal, children: [
|
||||
...selectedImages.map(
|
||||
(file) => SizedBox(
|
||||
height: 200,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Stack(
|
||||
children: [
|
||||
Image.file(file),
|
||||
if (allowEdit)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(3.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withValues(alpha: 0.5),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||
),
|
||||
child: IconButton(
|
||||
iconSize: 20,
|
||||
onPressed: () =>
|
||||
context.read<AddExerciseProvider>().removeExercise(file.path),
|
||||
color: Colors.white,
|
||||
icon: const Icon(Icons.delete),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
height: 120,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: itemCount,
|
||||
itemBuilder: (context, index) {
|
||||
// Show "add more" button at the end (only if editing is allowed)
|
||||
if (index == selectedImages.length) {
|
||||
return _buildAddMoreButton(context);
|
||||
}
|
||||
|
||||
// Show image thumbnail
|
||||
final image = selectedImages[index];
|
||||
return _buildImageCard(context, image.imageFile);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImageCard(BuildContext context, File image) {
|
||||
return Container(
|
||||
width: 120,
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
child: Stack(
|
||||
children: [
|
||||
// Image thumbnail
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.file(image, width: 120, height: 120, fit: BoxFit.cover),
|
||||
),
|
||||
|
||||
// Delete button overlay (only shown if editing is allowed)
|
||||
if (allowEdit)
|
||||
Positioned(
|
||||
top: 4,
|
||||
right: 4,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
color: Colors.white,
|
||||
iconSize: 20,
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: Colors.black.withOpacity(0.6),
|
||||
padding: const EdgeInsets.all(4),
|
||||
),
|
||||
onPressed: () {
|
||||
context.read<AddExerciseProvider>().removeImage(image.path);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAddMoreButton(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onAddMore,
|
||||
child: Container(
|
||||
width: 120,
|
||||
height: 120,
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
width: 2,
|
||||
style: BorderStyle.solid,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
if (allowEdit)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Container(
|
||||
color: Colors.grey,
|
||||
height: 200,
|
||||
width: 100,
|
||||
child: Center(
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: () => pickImages(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
child: Icon(Icons.add, size: 48, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/providers/add_exercise.dart';
|
||||
import 'package:wger/widgets/add_exercise/mixins/image_picker_mixin.dart';
|
||||
import 'package:wger/widgets/add_exercise/preview_images.dart';
|
||||
|
||||
class Step5Images extends StatefulWidget {
|
||||
final GlobalKey<FormState> formkey;
|
||||
|
||||
const Step5Images({required this.formkey});
|
||||
|
||||
@override
|
||||
State<Step5Images> createState() => _Step5ImagesState();
|
||||
}
|
||||
|
||||
class _Step5ImagesState extends State<Step5Images> with ExerciseImagePickerMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Form(
|
||||
key: widget.formkey,
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context).add_exercise_image_license,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
Consumer<AddExerciseProvider>(
|
||||
builder: (ctx, provider, __) => provider.exerciseImages.isNotEmpty
|
||||
? PreviewExerciseImages(selectedImages: provider.exerciseImages)
|
||||
: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () => pickImages(context, pickFromCamera: true),
|
||||
icon: const Icon(Icons.camera_alt),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => pickImages(context),
|
||||
icon: const Icon(Icons.collections),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Only JPEG, PNG and WEBP files below 20 MB are supported',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/helpers/consts.dart';
|
||||
import 'package:wger/helpers/exercises/forms.dart';
|
||||
import 'package:wger/helpers/exercises/validators.dart';
|
||||
import 'package:wger/helpers/i18n.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/models/exercises/category.dart';
|
||||
@@ -9,6 +9,7 @@ import 'package:wger/models/exercises/equipment.dart';
|
||||
import 'package:wger/models/exercises/muscle.dart';
|
||||
import 'package:wger/providers/add_exercise.dart';
|
||||
import 'package:wger/providers/exercises.dart';
|
||||
import 'package:wger/providers/user.dart';
|
||||
import 'package:wger/widgets/add_exercise/add_exercise_multiselect_button.dart';
|
||||
import 'package:wger/widgets/add_exercise/add_exercise_text_area.dart';
|
||||
import 'package:wger/widgets/exercises/exercises.dart';
|
||||
@@ -21,6 +22,7 @@ class Step1Basics extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final userProvider = context.read<UserProvider>();
|
||||
final addExerciseProvider = context.read<AddExerciseProvider>();
|
||||
final exerciseProvider = context.read<ExercisesProvider>();
|
||||
final categories = exerciseProvider.categories;
|
||||
@@ -38,21 +40,25 @@ class Step1Basics extends StatelessWidget {
|
||||
builder: (context, provider, child) => Column(
|
||||
children: [
|
||||
AddExerciseTextArea(
|
||||
onChange: (value) => {},
|
||||
title: '${AppLocalizations.of(context).name}*',
|
||||
helperText: AppLocalizations.of(context).baseNameEnglish,
|
||||
isRequired: true,
|
||||
validator: (name) => validateName(name, context),
|
||||
onSaved: (String? name) => addExerciseProvider.exerciseNameEn = name!,
|
||||
onSaved: (String? name) => addExerciseProvider.exerciseNameEn = name,
|
||||
),
|
||||
AddExerciseTextArea(
|
||||
onChange: (value) => {},
|
||||
title: AppLocalizations.of(context).alternativeNames,
|
||||
isMultiline: true,
|
||||
helperText: AppLocalizations.of(context).oneNamePerLine,
|
||||
onSaved: (String? alternateName) =>
|
||||
addExerciseProvider.alternateNamesEn = alternateName!.split('\n'),
|
||||
),
|
||||
AddExerciseTextArea(
|
||||
title: '${AppLocalizations.of(context).author}*',
|
||||
isMultiline: false,
|
||||
validator: (name) => validateAuthorName(name, context),
|
||||
initialValue: userProvider.profile!.username,
|
||||
onSaved: (String? author) => addExerciseProvider.author = author!,
|
||||
),
|
||||
ExerciseCategoryInputWidget<ExerciseCategory>(
|
||||
key: const Key('category-dropdown'),
|
||||
entries: categories,
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/helpers/exercises/forms.dart';
|
||||
import 'package:wger/helpers/exercises/validators.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/providers/add_exercise.dart';
|
||||
import 'package:wger/widgets/add_exercise/add_exercise_text_area.dart';
|
||||
@@ -23,10 +23,9 @@ class Step3Description extends StatelessWidget {
|
||||
onChange: (value) => {},
|
||||
title: '${i18n.description}*',
|
||||
helperText: i18n.enterTextInLanguage,
|
||||
isRequired: true,
|
||||
isMultiline: true,
|
||||
validator: (name) => validateExerciseDescription(name, context),
|
||||
onSaved: (String? description) => addExerciseProvider.descriptionEn = description!,
|
||||
onSaved: (String? description) => addExerciseProvider.descriptionEn = description,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/helpers/exercises/forms.dart';
|
||||
import 'package:wger/helpers/exercises/validators.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/models/exercises/language.dart';
|
||||
import 'package:wger/providers/add_exercise.dart';
|
||||
@@ -59,14 +59,11 @@ class _Step4TranslationState extends State<Step4Translation> {
|
||||
},
|
||||
),
|
||||
AddExerciseTextArea(
|
||||
onChange: (value) => {},
|
||||
title: '${i18n.name}*',
|
||||
isRequired: true,
|
||||
validator: (name) => validateName(name, context),
|
||||
onSaved: (String? name) => addExerciseProvider.exerciseNameTrans = name!,
|
||||
),
|
||||
AddExerciseTextArea(
|
||||
onChange: (value) => {},
|
||||
title: i18n.alternativeNames,
|
||||
isMultiline: true,
|
||||
helperText: i18n.oneNamePerLine,
|
||||
@@ -93,7 +90,6 @@ class _Step4TranslationState extends State<Step4Translation> {
|
||||
onChange: (value) => {},
|
||||
title: '${i18n.description}*',
|
||||
helperText: i18n.enterTextInLanguage,
|
||||
isRequired: true,
|
||||
isMultiline: true,
|
||||
validator: (name) => validateExerciseDescription(name, context),
|
||||
onSaved: (String? description) =>
|
||||
256
lib/widgets/add_exercise/steps/step_5_images.dart
Normal file
256
lib/widgets/add_exercise/steps/step_5_images.dart
Normal file
@@ -0,0 +1,256 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/models/exercises/exercise_submission_images.dart';
|
||||
import 'package:wger/providers/add_exercise.dart';
|
||||
import 'package:wger/providers/user.dart';
|
||||
import 'package:wger/widgets/add_exercise/image_details_form.dart';
|
||||
import 'package:wger/widgets/add_exercise/mixins/image_picker_mixin.dart';
|
||||
import 'package:wger/widgets/add_exercise/preview_images.dart';
|
||||
|
||||
/// Step 5 of exercise creation wizard - Image upload with license metadata
|
||||
///
|
||||
/// This step allows users to add exercise images with proper CC BY-SA 4.0 license
|
||||
/// attribution. Unlike the previous implementation that uploaded images directly,
|
||||
/// this version collects license metadata (title, author, URLs) before adding images.
|
||||
///
|
||||
/// Flow:
|
||||
/// 1. User picks image from camera/gallery
|
||||
/// 2. ImageDetailsForm is shown to collect license metadata
|
||||
/// 3. Image + metadata is stored in AddExerciseProvider
|
||||
/// 4. Final upload happens in Step 6 when user clicks "Submit"
|
||||
class Step5Images extends StatefulWidget {
|
||||
final GlobalKey<FormState> formkey;
|
||||
|
||||
const Step5Images({required this.formkey});
|
||||
|
||||
@override
|
||||
State<Step5Images> createState() => _Step5ImagesState();
|
||||
}
|
||||
|
||||
class _Step5ImagesState extends State<Step5Images> with ExerciseImagePickerMixin {
|
||||
/// Currently selected image waiting for metadata input
|
||||
/// When non-null, ImageDetailsForm is displayed instead of image picker
|
||||
ExerciseSubmissionImage? _currentImageToAdd;
|
||||
|
||||
/// Show dialog to choose between Camera and Gallery
|
||||
Future<void> _showImageSourceDialog(BuildContext context) async {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(AppLocalizations.of(context).selectImage),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.camera_alt),
|
||||
title: Text(AppLocalizations.of(context).takePicture),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
_pickAndShowImageDetails(context, pickFromCamera: true);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.collections),
|
||||
title: Text(AppLocalizations.of(context).gallery),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
_pickAndShowImageDetails(context, pickFromCamera: false);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Pick image from camera or gallery and show metadata collection form
|
||||
///
|
||||
/// Validates file format (jpg, jpeg, png, webp) and size (<20MB) before
|
||||
/// showing the form. Invalid files are rejected with a snackbar message.
|
||||
///
|
||||
/// [pickFromCamera] - If true, opens camera; otherwise opens gallery
|
||||
void _pickAndShowImageDetails(BuildContext context, {bool pickFromCamera = false}) async {
|
||||
final userProvider = context.read<UserProvider>();
|
||||
final imagePicker = ImagePicker();
|
||||
|
||||
XFile? selectedImage;
|
||||
if (pickFromCamera) {
|
||||
selectedImage = await imagePicker.pickImage(source: ImageSource.camera);
|
||||
} else {
|
||||
selectedImage = await imagePicker.pickImage(source: ImageSource.gallery);
|
||||
}
|
||||
|
||||
if (selectedImage != null) {
|
||||
final imageFile = File(selectedImage.path);
|
||||
|
||||
// Validate file type - only common image formats accepted
|
||||
bool isFileValid = true;
|
||||
String errorMessage = '';
|
||||
|
||||
final extension = imageFile.path.split('.').last;
|
||||
const validFileExtensions = ['jpg', 'jpeg', 'png', 'webp'];
|
||||
if (!validFileExtensions.any((ext) => extension.toLowerCase() == ext)) {
|
||||
isFileValid = false;
|
||||
errorMessage = "Select only 'jpg', 'jpeg', 'png', 'webp' files";
|
||||
}
|
||||
|
||||
// Validate file size - 20MB limit matches server-side restriction
|
||||
final fileSizeInMB = imageFile.lengthSync() / 1024 / 1024;
|
||||
if (fileSizeInMB > 20) {
|
||||
isFileValid = false;
|
||||
errorMessage = 'File Size should not be greater than 20 MB';
|
||||
}
|
||||
|
||||
if (!isFileValid) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(errorMessage)));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Show metadata collection form for valid image
|
||||
setState(() {
|
||||
_currentImageToAdd = ExerciseSubmissionImage(
|
||||
imageFile: imageFile,
|
||||
author: userProvider.profile?.username ?? '',
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Add image with its license metadata to the provider
|
||||
///
|
||||
/// Called when user clicks "ADD" in ImageDetailsForm. The image and metadata
|
||||
/// are stored locally in AddExerciseProvider and will be uploaded together
|
||||
/// when the exercise is submitted in Step 6.
|
||||
///
|
||||
/// [image] - The image file to add
|
||||
/// [details] - Map containing license fields (license_title, license_author, etc.)
|
||||
void _addImageWithDetails(ExerciseSubmissionImage image) {
|
||||
final provider = context.read<AddExerciseProvider>();
|
||||
|
||||
// Store image with metadata - actual upload happens in addExercise()
|
||||
provider.addExerciseImages([image]);
|
||||
|
||||
// Reset form state - image is now visible in preview list
|
||||
setState(() {
|
||||
_currentImageToAdd = null;
|
||||
});
|
||||
}
|
||||
|
||||
/// Cancel metadata input and return to image picker
|
||||
void _cancelImageAdd() {
|
||||
setState(() {
|
||||
_currentImageToAdd = null;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Form(
|
||||
key: widget.formkey,
|
||||
child: Column(
|
||||
children: [
|
||||
// License notice - shown when not entering metadata
|
||||
if (_currentImageToAdd == null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).add_exercise_image_license,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
|
||||
// Metadata collection form - shown when image is selected
|
||||
if (_currentImageToAdd != null)
|
||||
ImageDetailsForm(
|
||||
submissionImage: _currentImageToAdd!,
|
||||
onAdd: _addImageWithDetails,
|
||||
onCancel: _cancelImageAdd,
|
||||
),
|
||||
|
||||
// Image picker or preview - shown when not entering metadata
|
||||
if (_currentImageToAdd == null)
|
||||
Consumer<AddExerciseProvider>(
|
||||
builder: (ctx, provider, __) {
|
||||
if (provider.exerciseImages.isNotEmpty) {
|
||||
// Show preview of images that have been added with metadata
|
||||
return Column(
|
||||
children: [
|
||||
PreviewExerciseImages(
|
||||
selectedImages: provider.exerciseImages,
|
||||
onAddMore: () => _showImageSourceDialog(context),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => _showImageSourceDialog(context),
|
||||
icon: const Icon(Icons.add_photo_alternate),
|
||||
label: Text(AppLocalizations.of(context).addImage),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Empty state - no images added yet
|
||||
return Column(
|
||||
children: [
|
||||
const SizedBox(height: 20),
|
||||
Icon(Icons.add_photo_alternate, size: 80, color: Colors.grey.shade400),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'No images selected',
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleMedium?.copyWith(color: Colors.grey.shade600),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// Camera and Gallery buttons
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => _pickAndShowImageDetails(context, pickFromCamera: true),
|
||||
icon: const Icon(Icons.camera_alt),
|
||||
label: Text(AppLocalizations.of(context).takePicture),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => _pickAndShowImageDetails(context),
|
||||
icon: const Icon(Icons.collections),
|
||||
label: Text(AppLocalizations.of(context).gallery),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Text(
|
||||
'Only JPEG, PNG and WEBP files below 20 MB are supported',
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(color: Colors.grey.shade600),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -11,13 +11,14 @@ class Step6Overview extends StatelessWidget {
|
||||
final i18n = AppLocalizations.of(context);
|
||||
|
||||
return Consumer<AddExerciseProvider>(
|
||||
builder: (ctx, provider, __) => Column(
|
||||
builder: (ctx, provider, _) => Column(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Text(i18n.baseData, style: Theme.of(context).textTheme.headlineSmall),
|
||||
Table(
|
||||
columnWidths: const {0: FlexColumnWidth(2), 1: FlexColumnWidth(3)},
|
||||
children: [
|
||||
TableRow(children: [Text(i18n.author), Text(provider.author)]),
|
||||
TableRow(children: [Text(i18n.name), Text(provider.exerciseNameEn ?? '...')]),
|
||||
TableRow(
|
||||
children: [
|
||||
@@ -1,36 +1,26 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:wger/core/validators.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
|
||||
class ServerField extends StatelessWidget {
|
||||
final TextEditingController controller;
|
||||
final Function(String?) onSaved;
|
||||
|
||||
const ServerField({
|
||||
required this.controller,
|
||||
required this.onSaved,
|
||||
super.key,
|
||||
});
|
||||
const ServerField({required this.controller, required this.onSaved, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final i18n = AppLocalizations.of(context);
|
||||
|
||||
return TextFormField(
|
||||
key: const Key('inputServer'),
|
||||
decoration: InputDecoration(
|
||||
labelText: AppLocalizations.of(context).customServerUrl,
|
||||
helperText: AppLocalizations.of(context).customServerHint,
|
||||
labelText: i18n.customServerUrl,
|
||||
helperText: i18n.customServerHint,
|
||||
helperMaxLines: 4,
|
||||
),
|
||||
controller: controller,
|
||||
validator: (value) {
|
||||
if (Uri.tryParse(value!) == null) {
|
||||
return AppLocalizations.of(context).invalidUrl;
|
||||
}
|
||||
|
||||
if (value.isEmpty || !value.contains('http')) {
|
||||
return AppLocalizations.of(context).invalidUrl;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
validator: (value) => validateUrl(value, i18n, required: true),
|
||||
onSaved: onSaved,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -195,7 +195,8 @@ class AboutPage extends StatelessWidget {
|
||||
showLicensePage(
|
||||
context: context,
|
||||
applicationName: 'wger',
|
||||
applicationVersion: 'App: ${authProvider.applicationVersion?.version ?? 'N/A'} '
|
||||
applicationVersion:
|
||||
'App: ${authProvider.applicationVersion?.version ?? 'N/A'} '
|
||||
'Server: ${authProvider.serverVersion ?? 'N/A'}',
|
||||
applicationLegalese: '\u{a9} ${today.year} wger contributors',
|
||||
applicationIcon: Padding(
|
||||
|
||||
@@ -18,12 +18,11 @@
|
||||
|
||||
//import 'package:drift/drift.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/providers/nutrition.dart';
|
||||
import 'package:wger/providers/user.dart';
|
||||
import 'package:wger/screens/configure_plates_screen.dart';
|
||||
import 'package:wger/widgets/core/settings/exercise_cache.dart';
|
||||
import 'package:wger/widgets/core/settings/ingredient_cache.dart';
|
||||
import 'package:wger/widgets/core/settings/theme.dart';
|
||||
|
||||
class SettingsPage extends StatelessWidget {
|
||||
static String routeName = '/SettingsPage';
|
||||
@@ -33,73 +32,18 @@ class SettingsPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final i18n = AppLocalizations.of(context);
|
||||
final nutritionProvider = Provider.of<NutritionPlansProvider>(context, listen: false);
|
||||
final userProvider = Provider.of<UserProvider>(context);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(i18n.settingsTitle)),
|
||||
body: ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(
|
||||
i18n.settingsCacheTitle,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
title: Text(i18n.settingsCacheTitle, style: Theme.of(context).textTheme.headlineSmall),
|
||||
),
|
||||
const SettingsExerciseCache(),
|
||||
ListTile(
|
||||
title: Text(i18n.settingsIngredientCacheDescription),
|
||||
trailing: IconButton(
|
||||
key: const ValueKey('cacheIconIngredients'),
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () async {
|
||||
await nutritionProvider.clearIngredientCache();
|
||||
|
||||
if (context.mounted) {
|
||||
final snackBar = SnackBar(
|
||||
content: Text(i18n.settingsCacheDeletedSnackbar),
|
||||
);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(
|
||||
i18n.others,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(i18n.themeMode),
|
||||
trailing: DropdownButton<ThemeMode>(
|
||||
key: const ValueKey('themeModeDropdown'),
|
||||
value: userProvider.themeMode,
|
||||
onChanged: (ThemeMode? newValue) {
|
||||
if (newValue != null) {
|
||||
userProvider.setThemeMode(newValue);
|
||||
}
|
||||
},
|
||||
items: ThemeMode.values.map<DropdownMenuItem<ThemeMode>>((ThemeMode value) {
|
||||
final label = (() {
|
||||
switch (value) {
|
||||
case ThemeMode.system:
|
||||
return i18n.systemMode;
|
||||
case ThemeMode.light:
|
||||
return i18n.lightMode;
|
||||
case ThemeMode.dark:
|
||||
return i18n.darkMode;
|
||||
}
|
||||
})();
|
||||
|
||||
return DropdownMenuItem<ThemeMode>(
|
||||
value: value,
|
||||
child: Text(label),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
const SettingsIngredientCache(),
|
||||
ListTile(title: Text(i18n.others, style: Theme.of(context).textTheme.headlineSmall)),
|
||||
const SettingsTheme(),
|
||||
ListTile(
|
||||
title: Text(i18n.selectAvailablePlates),
|
||||
onTap: () {
|
||||
|
||||
@@ -22,68 +22,71 @@ class _SettingsExerciseCacheState extends State<SettingsExerciseCache> {
|
||||
return ListTile(
|
||||
enabled: !_isRefreshLoading,
|
||||
title: Text(i18n.settingsExerciseCacheDescription),
|
||||
subtitle: _subtitle.isNotEmpty ? Text(_subtitle) : null,
|
||||
trailing: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
IconButton(
|
||||
key: const ValueKey('cacheIconExercisesRefresh'),
|
||||
icon: _isRefreshLoading
|
||||
? const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.refresh),
|
||||
onPressed: _isRefreshLoading
|
||||
? null
|
||||
: () async {
|
||||
setState(() => _isRefreshLoading = true);
|
||||
subtitle: _subtitle.isNotEmpty
|
||||
? Text(_subtitle)
|
||||
: Text('${exerciseProvider.exercises.length} cached exercises'),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
key: const ValueKey('cacheIconExercisesRefresh'),
|
||||
icon: _isRefreshLoading
|
||||
? const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.refresh),
|
||||
onPressed: _isRefreshLoading
|
||||
? null
|
||||
: () async {
|
||||
setState(() => _isRefreshLoading = true);
|
||||
|
||||
// Note: status messages are currently left in English on purpose
|
||||
try {
|
||||
setState(() => _subtitle = 'Clearing cache...');
|
||||
await exerciseProvider.clearAllCachesAndPrefs();
|
||||
// Note: status messages are currently left in English on purpose
|
||||
try {
|
||||
setState(() => _subtitle = 'Clearing cache...');
|
||||
await exerciseProvider.clearAllCachesAndPrefs();
|
||||
|
||||
if (mounted) {
|
||||
setState(() => _subtitle = 'Loading languages and units...');
|
||||
if (mounted) {
|
||||
setState(() => _subtitle = 'Loading languages and units...');
|
||||
}
|
||||
await exerciseProvider.fetchAndSetInitialData();
|
||||
|
||||
if (mounted) {
|
||||
setState(() => _subtitle = 'Loading all exercises from server...');
|
||||
}
|
||||
await exerciseProvider.fetchAndSetAllExercises();
|
||||
|
||||
if (mounted) {
|
||||
setState(() => _subtitle = '');
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _isRefreshLoading = false);
|
||||
}
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(i18n.success)));
|
||||
}
|
||||
}
|
||||
await exerciseProvider.fetchAndSetInitialData();
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
key: const ValueKey('cacheIconExercisesDelete'),
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () async {
|
||||
await exerciseProvider.clearAllCachesAndPrefs();
|
||||
|
||||
if (mounted) {
|
||||
setState(() => _subtitle = 'Loading all exercises from server...');
|
||||
}
|
||||
await exerciseProvider.fetchAndSetAllExercises();
|
||||
if (context.mounted) {
|
||||
final snackBar = SnackBar(content: Text(i18n.settingsCacheDeletedSnackbar));
|
||||
|
||||
if (mounted) {
|
||||
setState(() => _subtitle = '');
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _isRefreshLoading = false);
|
||||
}
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(i18n.success)),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
key: const ValueKey('cacheIconExercisesDelete'),
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () async {
|
||||
await exerciseProvider.clearAllCachesAndPrefs();
|
||||
|
||||
if (context.mounted) {
|
||||
final snackBar = SnackBar(
|
||||
content: Text(i18n.settingsCacheDeletedSnackbar),
|
||||
);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
}
|
||||
},
|
||||
)
|
||||
]),
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
32
lib/widgets/core/settings/ingredient_cache.dart
Normal file
32
lib/widgets/core/settings/ingredient_cache.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/providers/nutrition.dart';
|
||||
|
||||
class SettingsIngredientCache extends StatelessWidget {
|
||||
const SettingsIngredientCache({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final i18n = AppLocalizations.of(context);
|
||||
final nutritionProvider = Provider.of<NutritionPlansProvider>(context, listen: false);
|
||||
|
||||
return ListTile(
|
||||
title: Text(i18n.settingsIngredientCacheDescription),
|
||||
subtitle: Text('${nutritionProvider.ingredients.length} cached ingredients'),
|
||||
trailing: IconButton(
|
||||
key: const ValueKey('cacheIconIngredients'),
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () async {
|
||||
await nutritionProvider.clearIngredientCache();
|
||||
|
||||
if (context.mounted) {
|
||||
final snackBar = SnackBar(content: Text(i18n.settingsCacheDeletedSnackbar));
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
41
lib/widgets/core/settings/theme.dart
Normal file
41
lib/widgets/core/settings/theme.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/providers/user.dart';
|
||||
|
||||
class SettingsTheme extends StatelessWidget {
|
||||
const SettingsTheme({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final i18n = AppLocalizations.of(context);
|
||||
final userProvider = Provider.of<UserProvider>(context);
|
||||
|
||||
return ListTile(
|
||||
title: Text(i18n.themeMode),
|
||||
trailing: DropdownButton<ThemeMode>(
|
||||
key: const ValueKey('themeModeDropdown'),
|
||||
value: userProvider.themeMode,
|
||||
onChanged: (ThemeMode? newValue) {
|
||||
if (newValue != null) {
|
||||
userProvider.setThemeMode(newValue);
|
||||
}
|
||||
},
|
||||
items: ThemeMode.values.map<DropdownMenuItem<ThemeMode>>((ThemeMode value) {
|
||||
final label = (() {
|
||||
switch (value) {
|
||||
case ThemeMode.system:
|
||||
return i18n.systemMode;
|
||||
case ThemeMode.light:
|
||||
return i18n.lightMode;
|
||||
case ThemeMode.dark:
|
||||
return i18n.darkMode;
|
||||
}
|
||||
})();
|
||||
|
||||
return DropdownMenuItem<ThemeMode>(value: value, child: Text(label));
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -31,12 +31,7 @@ import 'package:wger/providers/routines.dart';
|
||||
import 'package:wger/theme/theme.dart';
|
||||
|
||||
/// Types of events
|
||||
enum EventType {
|
||||
weight,
|
||||
measurement,
|
||||
session,
|
||||
caloriesDiary,
|
||||
}
|
||||
enum EventType { weight, measurement, session, caloriesDiary }
|
||||
|
||||
/// An event in the dashboard calendar
|
||||
class Event {
|
||||
@@ -85,6 +80,22 @@ class _DashboardCalendarWidgetState extends State<DashboardCalendarWidget>
|
||||
});
|
||||
}
|
||||
|
||||
/// Loads and organizes all events from various providers into the calendar.
|
||||
///
|
||||
/// This method asynchronously fetches and processes data from multiple sources:
|
||||
/// - **Weight entries**: Retrieves weight measurements from [BodyWeightProvider]
|
||||
/// - **Measurements**: Retrieves body measurements from [MeasurementProvider]
|
||||
/// - **Workout sessions**: Fetches workout session data from [RoutinesProvider]
|
||||
/// - **Nutritional plans**: Retrieves calorie diary entries from [NutritionPlansProvider]
|
||||
///
|
||||
/// Each event is formatted according to the current locale and stored in the
|
||||
/// [_events] map, keyed by date. The date format is determined by [DateFormatLists.format].
|
||||
///
|
||||
/// After loading all events, the [_selectedEvents] value is updated with events
|
||||
/// for the currently selected day, if any.
|
||||
///
|
||||
/// **Note**: This method checks if the widget is still mounted before updating
|
||||
/// the state after the async workout session fetch operation.
|
||||
void loadEvents() async {
|
||||
final numberFormat = NumberFormat.decimalPattern(Localizations.localeOf(context).toString());
|
||||
final i18n = AppLocalizations.of(context);
|
||||
@@ -112,10 +123,12 @@ class _DashboardCalendarWidgetState extends State<DashboardCalendarWidget>
|
||||
_events[date] = [];
|
||||
}
|
||||
|
||||
_events[date]?.add(Event(
|
||||
EventType.measurement,
|
||||
'${category.name}: ${numberFormat.format(entry.value)} ${category.unit}',
|
||||
));
|
||||
_events[date]?.add(
|
||||
Event(
|
||||
EventType.measurement,
|
||||
'${category.name}: ${numberFormat.format(entry.value)} ${category.unit}',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,16 +144,20 @@ class _DashboardCalendarWidgetState extends State<DashboardCalendarWidget>
|
||||
time = '(${timeToString(session.timeStart)} - ${timeToString(session.timeEnd)})';
|
||||
|
||||
// Add events to lists
|
||||
_events[date]?.add(Event(
|
||||
EventType.session,
|
||||
'${i18n.impression}: ${session.impressionAsString} $time',
|
||||
));
|
||||
_events[date]?.add(
|
||||
Event(EventType.session, '${i18n.impression}: ${session.impressionAsString} $time'),
|
||||
);
|
||||
}
|
||||
});
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Process nutritional plans
|
||||
final NutritionPlansProvider nutritionProvider =
|
||||
Provider.of<NutritionPlansProvider>(context, listen: false);
|
||||
final NutritionPlansProvider nutritionProvider = Provider.of<NutritionPlansProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
for (final plan in nutritionProvider.items) {
|
||||
for (final entry in plan.logEntriesValues.entries) {
|
||||
final date = DateFormatLists.format(entry.key);
|
||||
@@ -149,10 +166,9 @@ class _DashboardCalendarWidgetState extends State<DashboardCalendarWidget>
|
||||
}
|
||||
|
||||
// Add events to lists
|
||||
_events[date]?.add(Event(
|
||||
EventType.caloriesDiary,
|
||||
i18n.kcalValue(entry.value.energy.toStringAsFixed(0)),
|
||||
));
|
||||
_events[date]?.add(
|
||||
Event(EventType.caloriesDiary, i18n.kcalValue(entry.value.energy.toStringAsFixed(0))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,8 +266,10 @@ class _DashboardCalendarWidgetState extends State<DashboardCalendarWidget>
|
||||
valueListenable: _selectedEvents,
|
||||
builder: (context, value, _) => Column(
|
||||
children: [
|
||||
...value.map((event) => ListTile(
|
||||
title: Text((() {
|
||||
...value.map(
|
||||
(event) => ListTile(
|
||||
title: Text(
|
||||
(() {
|
||||
switch (event.type) {
|
||||
case EventType.caloriesDiary:
|
||||
return AppLocalizations.of(context).nutritionalDiary;
|
||||
@@ -265,10 +283,12 @@ class _DashboardCalendarWidgetState extends State<DashboardCalendarWidget>
|
||||
case EventType.measurement:
|
||||
return AppLocalizations.of(context).measurement;
|
||||
}
|
||||
})()),
|
||||
subtitle: Text(event.description),
|
||||
//onTap: () => print('$event tapped!'),
|
||||
)),
|
||||
})(),
|
||||
),
|
||||
subtitle: Text(event.description),
|
||||
//onTap: () => print('$event tapped!'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -42,8 +42,9 @@ class _DashboardMeasurementWidgetState extends State<DashboardMeasurementWidget>
|
||||
Widget build(BuildContext context) {
|
||||
final provider = Provider.of<MeasurementProvider>(context, listen: false);
|
||||
|
||||
final items =
|
||||
provider.categories.map<Widget>((item) => CategoriesCard(item, elevation: 0)).toList();
|
||||
final items = provider.categories
|
||||
.map<Widget>((item) => CategoriesCard(item, elevation: 0))
|
||||
.toList();
|
||||
if (items.isNotEmpty) {
|
||||
items.add(
|
||||
NothingFound(
|
||||
@@ -115,10 +116,10 @@ class _DashboardMeasurementWidgetState extends State<DashboardMeasurementWidget>
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color:
|
||||
Theme.of(context).textTheme.headlineSmall!.color!.withOpacity(
|
||||
_current == entry.key ? 0.9 : 0.4,
|
||||
),
|
||||
color: Theme.of(context).textTheme.headlineSmall!.color!
|
||||
.withOpacity(
|
||||
_current == entry.key ? 0.9 : 0.4,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -60,8 +60,9 @@ class _DashboardNutritionWidgetState extends State<DashboardNutritionWidget> {
|
||||
),
|
||||
subtitle: Text(
|
||||
_hasContent
|
||||
? DateFormat.yMd(Localizations.localeOf(context).languageCode)
|
||||
.format(_plan!.creationDate)
|
||||
? DateFormat.yMd(
|
||||
Localizations.localeOf(context).languageCode,
|
||||
).format(_plan!.creationDate)
|
||||
: '',
|
||||
),
|
||||
leading: Icon(
|
||||
|
||||
@@ -175,14 +175,19 @@ class DetailContentWidget extends StatelessWidget {
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(s.exercise
|
||||
.getTranslation(
|
||||
Localizations.localeOf(context).languageCode)
|
||||
.name),
|
||||
Text(
|
||||
s.exercise
|
||||
.getTranslation(
|
||||
Localizations.localeOf(context).languageCode,
|
||||
)
|
||||
.name,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child:
|
||||
MutedText(s.textRepr, overflow: TextOverflow.ellipsis),
|
||||
child: MutedText(
|
||||
s.textRepr,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -94,9 +94,12 @@ class DashboardWeightWidget extends StatelessWidget {
|
||||
FormScreen.routeName,
|
||||
arguments: FormScreenArguments(
|
||||
AppLocalizations.of(context).newEntry,
|
||||
WeightForm(weightProvider
|
||||
.getNewestEntry()
|
||||
?.copyWith(id: null, date: DateTime.now())),
|
||||
WeightForm(
|
||||
weightProvider.getNewestEntry()?.copyWith(
|
||||
id: null,
|
||||
date: DateTime.now(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -85,30 +85,32 @@ class _ExerciseAutocompleterState extends State<ExerciseAutocompleter> {
|
||||
return null;
|
||||
}
|
||||
return context.read<ExercisesProvider>().searchExercise(
|
||||
pattern,
|
||||
languageCode: Localizations.localeOf(context).languageCode,
|
||||
searchEnglish: _searchEnglish,
|
||||
);
|
||||
pattern,
|
||||
languageCode: Localizations.localeOf(context).languageCode,
|
||||
searchEnglish: _searchEnglish,
|
||||
);
|
||||
},
|
||||
itemBuilder: (
|
||||
BuildContext context,
|
||||
Exercise exerciseSuggestion,
|
||||
) =>
|
||||
ListTile(
|
||||
key: Key('exercise-${exerciseSuggestion.id}'),
|
||||
leading: SizedBox(
|
||||
width: 45,
|
||||
child: ExerciseImageWidget(
|
||||
image: exerciseSuggestion.getMainImage,
|
||||
itemBuilder:
|
||||
(
|
||||
BuildContext context,
|
||||
Exercise exerciseSuggestion,
|
||||
) => ListTile(
|
||||
key: Key('exercise-${exerciseSuggestion.id}'),
|
||||
leading: SizedBox(
|
||||
width: 45,
|
||||
child: ExerciseImageWidget(
|
||||
image: exerciseSuggestion.getMainImage,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
exerciseSuggestion
|
||||
.getTranslation(Localizations.localeOf(context).languageCode)
|
||||
.name,
|
||||
),
|
||||
subtitle: Text(
|
||||
'${exerciseSuggestion.category!.name} / ${exerciseSuggestion.equipment.map((e) => e.name).join(', ')}',
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
exerciseSuggestion.getTranslation(Localizations.localeOf(context).languageCode).name,
|
||||
),
|
||||
subtitle: Text(
|
||||
'${exerciseSuggestion.category!.name} / ${exerciseSuggestion.equipment.map((e) => e.name).join(', ')}',
|
||||
),
|
||||
),
|
||||
emptyBuilder: (context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -149,7 +151,7 @@ class _ExerciseAutocompleterState extends State<ExerciseAutocompleter> {
|
||||
});
|
||||
},
|
||||
dense: true,
|
||||
)
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -81,21 +81,23 @@ class ExerciseDetail extends StatelessWidget {
|
||||
}
|
||||
|
||||
List<Widget> getVariations(BuildContext context) {
|
||||
final variations =
|
||||
Provider.of<ExercisesProvider>(context, listen: false).findExercisesByVariationId(
|
||||
_exercise.variationId,
|
||||
exerciseIdToExclude: _exercise.id,
|
||||
);
|
||||
final variations = Provider.of<ExercisesProvider>(context, listen: false)
|
||||
.findExercisesByVariationId(
|
||||
_exercise.variationId,
|
||||
exerciseIdToExclude: _exercise.id,
|
||||
);
|
||||
|
||||
final List<Widget> out = [];
|
||||
if (_exercise.variationId == null) {
|
||||
return out;
|
||||
}
|
||||
|
||||
out.add(Text(
|
||||
AppLocalizations.of(context).variations,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
));
|
||||
out.add(
|
||||
Text(
|
||||
AppLocalizations.of(context).variations,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
);
|
||||
for (final element in variations) {
|
||||
out.add(ExerciseListTile(exercise: element));
|
||||
}
|
||||
@@ -110,10 +112,12 @@ class ExerciseDetail extends StatelessWidget {
|
||||
List<Widget> getNotes(BuildContext context) {
|
||||
final List<Widget> out = [];
|
||||
if (_translation.notes.isNotEmpty) {
|
||||
out.add(Text(
|
||||
AppLocalizations.of(context).notes,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
));
|
||||
out.add(
|
||||
Text(
|
||||
AppLocalizations.of(context).notes,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
);
|
||||
for (final e in _translation.notes) {
|
||||
out.add(Text(e.comment));
|
||||
}
|
||||
@@ -125,36 +129,40 @@ class ExerciseDetail extends StatelessWidget {
|
||||
|
||||
List<Widget> getMuscles(BuildContext context) {
|
||||
final List<Widget> out = [];
|
||||
out.add(Text(
|
||||
AppLocalizations.of(context).muscles,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
));
|
||||
out.add(Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: PADDING),
|
||||
child: MuscleWidget(
|
||||
muscles: _exercise.muscles,
|
||||
musclesSecondary: _exercise.musclesSecondary,
|
||||
isFront: true,
|
||||
out.add(
|
||||
Text(
|
||||
AppLocalizations.of(context).muscles,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
);
|
||||
out.add(
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: PADDING),
|
||||
child: MuscleWidget(
|
||||
muscles: _exercise.muscles,
|
||||
musclesSecondary: _exercise.musclesSecondary,
|
||||
isFront: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: PADDING),
|
||||
child: MuscleWidget(
|
||||
muscles: _exercise.muscles,
|
||||
musclesSecondary: _exercise.musclesSecondary,
|
||||
isFront: false,
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: PADDING),
|
||||
child: MuscleWidget(
|
||||
muscles: _exercise.muscles,
|
||||
musclesSecondary: _exercise.musclesSecondary,
|
||||
isFront: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
));
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
out.add(
|
||||
Column(
|
||||
@@ -183,10 +191,12 @@ class ExerciseDetail extends StatelessWidget {
|
||||
|
||||
List<Widget> getDescription(BuildContext context) {
|
||||
final List<Widget> out = [];
|
||||
out.add(Text(
|
||||
AppLocalizations.of(context).description,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
));
|
||||
out.add(
|
||||
Text(
|
||||
AppLocalizations.of(context).description,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
);
|
||||
out.add(Html(data: _translation.description));
|
||||
|
||||
return out;
|
||||
@@ -195,9 +205,11 @@ class ExerciseDetail extends StatelessWidget {
|
||||
List<Widget> getImages() {
|
||||
final List<Widget> out = [];
|
||||
if (_exercise.images.isNotEmpty) {
|
||||
out.add(CarouselImages(
|
||||
images: _exercise.images,
|
||||
));
|
||||
out.add(
|
||||
CarouselImages(
|
||||
images: _exercise.images,
|
||||
),
|
||||
);
|
||||
|
||||
// out.add(ExerciseImageWidget(
|
||||
// image: _exercise.getMainImage,
|
||||
@@ -225,14 +237,16 @@ class ExerciseDetail extends StatelessWidget {
|
||||
);
|
||||
if (_exercise.equipment.isNotEmpty) {
|
||||
_exercise.equipment
|
||||
.map((e) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Chip(
|
||||
label: Text(getTranslation(e.name, context)),
|
||||
padding: EdgeInsets.zero,
|
||||
backgroundColor: theme.splashColor,
|
||||
),
|
||||
))
|
||||
.map(
|
||||
(e) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Chip(
|
||||
label: Text(getTranslation(e.name, context)),
|
||||
padding: EdgeInsets.zero,
|
||||
backgroundColor: theme.splashColor,
|
||||
),
|
||||
),
|
||||
)
|
||||
.forEach((element) => out.add(element));
|
||||
}
|
||||
out.add(const SizedBox(height: PADDING));
|
||||
@@ -255,11 +269,13 @@ class ExerciseDetail extends StatelessWidget {
|
||||
List<Widget> getAliases(BuildContext context) {
|
||||
final List<Widget> out = [];
|
||||
if (_translation.aliases.isNotEmpty) {
|
||||
out.add(MutedText(
|
||||
AppLocalizations.of(context).alsoKnownAs(
|
||||
_translation.aliases.map((e) => e.alias).toList().join(', '),
|
||||
out.add(
|
||||
MutedText(
|
||||
AppLocalizations.of(context).alsoKnownAs(
|
||||
_translation.aliases.map((e) => e.alias).toList().join(', '),
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
out.add(const SizedBox(height: PADDING));
|
||||
}
|
||||
|
||||
@@ -355,9 +371,13 @@ class MuscleWidget extends StatelessWidget {
|
||||
children: [
|
||||
SvgPicture.asset('assets/images/muscles/$background.svg'),
|
||||
...muscles.map((m) => SvgPicture.asset('assets/images/muscles/main/muscle-${m.id}.svg')),
|
||||
...musclesSecondary.where((m) => !muscles.contains(m)).map((m) => SvgPicture.asset(
|
||||
'assets/images/muscles/secondary/muscle-${m.id}.svg',
|
||||
)),
|
||||
...musclesSecondary
|
||||
.where((m) => !muscles.contains(m))
|
||||
.map(
|
||||
(m) => SvgPicture.asset(
|
||||
'assets/images/muscles/secondary/muscle-${m.id}.svg',
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,22 +68,22 @@ class _ExerciseVideoWidgetState extends State<ExerciseVideoWidget> {
|
||||
return hasError
|
||||
? const GeneralErrorsWidget(
|
||||
[
|
||||
'An error happened while loading the video. If you can, please check the application logs.'
|
||||
'An error happened while loading the video. If you can, please check the application logs.',
|
||||
],
|
||||
)
|
||||
: _controller.value.isInitialized
|
||||
? AspectRatio(
|
||||
aspectRatio: _controller.value.aspectRatio,
|
||||
child: Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
VideoPlayer(_controller),
|
||||
_ControlsOverlay(controller: _controller),
|
||||
VideoProgressIndicator(_controller, allowScrubbing: true),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Container();
|
||||
? AspectRatio(
|
||||
aspectRatio: _controller.value.aspectRatio,
|
||||
child: Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
VideoPlayer(_controller),
|
||||
_ControlsOverlay(controller: _controller),
|
||||
VideoProgressIndicator(_controller, allowScrubbing: true),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Container();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user