mirror of
https://github.com/wger-project/flutter.git
synced 2026-02-18 00:17:48 +01:00
Add video player widget for exercise details
This commit is contained in:
@@ -20,6 +20,7 @@ Exercise _$ExerciseFromJson(Map<String, dynamic> json) {
|
||||
'muscles_secondary',
|
||||
'equipment',
|
||||
'images',
|
||||
'videos',
|
||||
'comments'
|
||||
],
|
||||
);
|
||||
@@ -41,6 +42,9 @@ Exercise _$ExerciseFromJson(Map<String, dynamic> json) {
|
||||
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(),
|
||||
tips: (json['comments'] as List<dynamic>?)
|
||||
?.map((e) => Comment.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
@@ -60,5 +64,6 @@ Map<String, dynamic> _$ExerciseToJson(Exercise instance) => <String, dynamic>{
|
||||
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(),
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
56
lib/models/exercises/video.g.dart
Normal file
56
lib/models/exercises/video.g.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'video.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
Video _$VideoFromJson(Map<String, dynamic> 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<String, dynamic> _$VideoToJson(Video instance) => <String, dynamic>{
|
||||
'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,
|
||||
};
|
||||
@@ -122,7 +122,7 @@ class ExercisesProvider extends WgerBaseProvider with ChangeNotifier {
|
||||
Future<void> 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)));
|
||||
|
||||
@@ -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<Widget> getImages() {
|
||||
// TODO: add carousel for the other images
|
||||
final List<Widget> 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<Widget> getVideos() {
|
||||
// TODO: add carousel for the other videos
|
||||
final List<Widget> 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;
|
||||
}
|
||||
|
||||
74
lib/widgets/exercises/videos.dart
Normal file
74
lib/widgets/exercises/videos.dart
Normal file
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<ExerciseVideoWidget> createState() => _ExerciseVideoWidgetState();
|
||||
}
|
||||
|
||||
class _ExerciseVideoWidgetState extends State<ExerciseVideoWidget> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
10
pubspec.lock
10
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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user