diff --git a/lib/models/exercises/exercise.g.dart b/lib/models/exercises/exercise.g.dart index 44575077..614ff9b2 100644 --- a/lib/models/exercises/exercise.g.dart +++ b/lib/models/exercises/exercise.g.dart @@ -20,6 +20,7 @@ Exercise _$ExerciseFromJson(Map json) { 'muscles_secondary', 'equipment', 'images', + 'videos', 'comments' ], ); @@ -41,6 +42,9 @@ Exercise _$ExerciseFromJson(Map json) { images: (json['images'] as List?) ?.map((e) => ExerciseImage.fromJson(e as Map)) .toList(), + videos: (json['videos'] as List?) + ?.map((e) => Video.fromJson(e as Map)) + .toList(), tips: (json['comments'] as List?) ?.map((e) => Comment.fromJson(e as Map)) .toList(), @@ -60,5 +64,6 @@ Map _$ExerciseToJson(Exercise instance) => { instance.musclesSecondary.map((e) => e.toJson()).toList(), 'equipment': instance.equipment.map((e) => e.toJson()).toList(), 'images': instance.images.map((e) => e.toJson()).toList(), + 'videos': instance.videos.map((e) => e.toJson()).toList(), 'comments': instance.tips.map((e) => e.toJson()).toList(), }; diff --git a/lib/models/exercises/video.dart b/lib/models/exercises/video.dart index 08ea8222..1213a3c7 100644 --- a/lib/models/exercises/video.dart +++ b/lib/models/exercises/video.dart @@ -17,6 +17,7 @@ */ import 'package:json_annotation/json_annotation.dart'; +import 'package:wger/helpers/json.dart'; part 'video.g.dart'; @@ -28,16 +29,16 @@ class Video { @JsonKey(required: true) final String uuid; + @JsonKey(name: 'video', required: true) + final String url; + @JsonKey(name: 'exercise_base', required: true) final int base; - @JsonKey(name: 'is_front', required: true) - final bool isFront; - @JsonKey(required: true) final int size; - @JsonKey(required: true) + @JsonKey(required: true, fromJson: stringToNum, toJson: numToString) final num duration; @JsonKey(required: true) @@ -56,14 +57,14 @@ class Video { final int license; @JsonKey(name: 'license_author', required: true) - final String licenseAuthor; + final String? licenseAuthor; const Video({ required this.id, required this.uuid, required this.base, - required this.isFront, required this.size, + required this.url, required this.duration, required this.width, required this.height, diff --git a/lib/models/exercises/video.g.dart b/lib/models/exercises/video.g.dart new file mode 100644 index 00000000..16219881 --- /dev/null +++ b/lib/models/exercises/video.g.dart @@ -0,0 +1,56 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'video.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Video _$VideoFromJson(Map json) { + $checkKeys( + json, + requiredKeys: const [ + 'id', + 'uuid', + 'video', + 'exercise_base', + 'size', + 'duration', + 'width', + 'height', + 'codec', + 'codec_long', + 'license', + 'license_author' + ], + ); + return Video( + id: json['id'] as int, + uuid: json['uuid'] as String, + base: json['exercise_base'] as int, + size: json['size'] as int, + url: json['video'] as String, + duration: stringToNum(json['duration'] as String?), + width: json['width'] as int, + height: json['height'] as int, + codec: json['codec'] as String, + codecLong: json['codec_long'] as String, + license: json['license'] as int, + licenseAuthor: json['license_author'] as String?, + ); +} + +Map _$VideoToJson(Video instance) => { + 'id': instance.id, + 'uuid': instance.uuid, + 'video': instance.url, + 'exercise_base': instance.base, + '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, + }; diff --git a/lib/providers/exercises.dart b/lib/providers/exercises.dart index 02abc98f..12656330 100644 --- a/lib/providers/exercises.dart +++ b/lib/providers/exercises.dart @@ -122,7 +122,7 @@ class ExercisesProvider extends WgerBaseProvider with ChangeNotifier { Future fetchAndSetExercises() async { // Load exercises from cache, if available final prefs = await SharedPreferences.getInstance(); - if (prefs.containsKey(PREFS_EXERCISES)) { + if (false && prefs.containsKey(PREFS_EXERCISES)) { final exerciseData = json.decode(prefs.getString(PREFS_EXERCISES)!); if (DateTime.parse(exerciseData['expiresIn']).isAfter(DateTime.now())) { exerciseData['exercises'].forEach((e) => _exercises.add(Exercise.fromJson(e))); diff --git a/lib/widgets/exercises/exercises.dart b/lib/widgets/exercises/exercises.dart index a5237882..2d33b41a 100644 --- a/lib/widgets/exercises/exercises.dart +++ b/lib/widgets/exercises/exercises.dart @@ -24,6 +24,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:wger/models/exercises/exercise.dart'; import 'package:wger/models/exercises/muscle.dart'; import 'package:wger/widgets/exercises/images.dart'; +import 'package:wger/widgets/exercises/videos.dart'; class ExerciseDetail extends StatelessWidget { final Exercise _exercise; @@ -38,6 +39,9 @@ class ExerciseDetail extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Videos + ...getVideos(), + // Images ...getImages(), @@ -168,8 +172,24 @@ class ExerciseDetail extends StatelessWidget { List getImages() { // TODO: add carousel for the other images final List out = []; - out.add(ExerciseImageWidget(image: _exercise.getMainImage)); - out.add(const SizedBox(height: PADDING)); + if (_exercise.getMainImage != null) { + out.add(ExerciseImageWidget(image: _exercise.getMainImage)); + out.add(const SizedBox(height: PADDING)); + } + + return out; + } + + List getVideos() { + // TODO: add carousel for the other videos + final List out = []; + if (_exercise.videos.isNotEmpty) { + _exercise.videos.map((v) => ExerciseVideoWidget(video: v)).forEach((element) { + out.add(element); + }); + + out.add(const SizedBox(height: PADDING)); + } return out; } diff --git a/lib/widgets/exercises/videos.dart b/lib/widgets/exercises/videos.dart new file mode 100644 index 00000000..1389b928 --- /dev/null +++ b/lib/widgets/exercises/videos.dart @@ -0,0 +1,74 @@ +/* + * This file is part of wger Workout Manager . + * 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. + * + * wger Workout Manager 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 . + */ + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:video_player/video_player.dart'; +import 'package:wger/models/exercises/video.dart'; + +class ExerciseVideoWidget extends StatefulWidget { + const ExerciseVideoWidget({ + required this.video, + }); + + final Video video; + + @override + State createState() => _ExerciseVideoWidgetState(); +} + +class _ExerciseVideoWidgetState extends State { + late VideoPlayerController _controller; + + @override + void initState() { + super.initState(); + _controller = VideoPlayerController.network(widget.video.url) + ..initialize().then((_) { + // Ensure the first frame is shown after the video is initialized, even before the play button has been pressed. + setState(() {}); + }); + } + + @override + void dispose() { + super.dispose(); + _controller.dispose(); + } + + @override + Widget build(BuildContext context) { + return _controller.value.isInitialized + ? Column( + children: [ + AspectRatio( + aspectRatio: _controller.value.aspectRatio, + child: VideoPlayer(_controller), + ), + IconButton( + onPressed: () { + setState(() { + _controller.value.isPlaying ? _controller.pause() : _controller.play(); + }); + }, + icon: Icon(_controller.value.isPlaying ? Icons.stop : Icons.play_arrow)) + ], + ) + : Container(); + } +} diff --git a/pubspec.lock b/pubspec.lock index a9ef2970..f46fd575 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -112,14 +112,14 @@ packages: name: camera url: "https://pub.dartlang.org" source: hosted - version: "0.9.4+6" + version: "0.9.4+9" camera_platform_interface: dependency: transitive description: name: camera_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.1.4" + version: "2.1.5" camera_web: dependency: transitive description: @@ -475,7 +475,7 @@ packages: name: image_picker url: "https://pub.dartlang.org" source: hosted - version: "0.8.4+4" + version: "0.8.4+5" image_picker_for_web: dependency: transitive description: @@ -1000,12 +1000,12 @@ packages: source: hosted version: "0.1.0" video_player: - dependency: transitive + dependency: "direct main" description: name: video_player url: "https://pub.dartlang.org" source: hosted - version: "2.2.14" + version: "2.2.16" video_player_platform_interface: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 92e1d067..055fcc71 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,7 +32,7 @@ dependencies: sdk: flutter android_metadata: ^0.2.1 - camera: ^0.9.2+2 + camera: ^0.9.4+9 charts_flutter: ^0.12.0 collection: ^1.15.0-nullsafety.4 cupertino_icons: ^1.0.0 @@ -42,7 +42,7 @@ dependencies: flutter_typeahead: ^3.2.0 font_awesome_flutter: ^9.1.0 http: ^0.13.3 - image_picker: ^0.8.4+1 + image_picker: ^0.8.4+5 intl: ^0.17.0 json_annotation: ^4.3.0 version: ^2.0.0 @@ -53,6 +53,7 @@ dependencies: table_calendar: ^3.0.2 url_launcher: ^6.0.10 flutter_barcode_scanner: ^2.0.0 + video_player: ^2.2.16 dev_dependencies: flutter_test: