|
|
|
@ -2,6 +2,7 @@ import 'dart:io';
|
|
|
|
import 'dart:typed_data';
|
|
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:image_picker/image_picker.dart';
|
|
|
|
import 'package:image_picker/image_picker.dart';
|
|
|
|
|
|
|
|
import 'package:video_player/video_player.dart';
|
|
|
|
import '../../core/service_locator.dart';
|
|
|
|
import '../../core/service_locator.dart';
|
|
|
|
import '../../core/logger.dart';
|
|
|
|
import '../../core/logger.dart';
|
|
|
|
import '../../data/recipes/recipe_service.dart';
|
|
|
|
import '../../data/recipes/recipe_service.dart';
|
|
|
|
@ -1471,7 +1472,7 @@ class _AddRecipeScreenState extends State<AddRecipeScreen> {
|
|
|
|
child: ClipRRect(
|
|
|
|
child: ClipRRect(
|
|
|
|
borderRadius: BorderRadius.circular(12),
|
|
|
|
borderRadius: BorderRadius.circular(12),
|
|
|
|
child: isVideo
|
|
|
|
child: isVideo
|
|
|
|
? _buildVideoThumbnailForTile(mediaUrl)
|
|
|
|
? _VideoThumbnailPreview(videoUrl: mediaUrl)
|
|
|
|
: _buildImagePreview(mediaUrl),
|
|
|
|
: _buildImagePreview(mediaUrl),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
@ -1603,7 +1604,7 @@ class _AddRecipeScreenState extends State<AddRecipeScreen> {
|
|
|
|
)
|
|
|
|
)
|
|
|
|
: null,
|
|
|
|
: null,
|
|
|
|
child: media.isVideo
|
|
|
|
child: media.isVideo
|
|
|
|
? _buildVideoThumbnailForTile(media.url)
|
|
|
|
? _VideoThumbnailPreview(videoUrl: media.url)
|
|
|
|
: _buildImagePreviewForTile(media.url),
|
|
|
|
: _buildImagePreviewForTile(media.url),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
);
|
|
|
|
@ -1776,3 +1777,106 @@ class _AddRecipeScreenState extends State<AddRecipeScreen> {
|
|
|
|
);
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Widget that displays a video thumbnail preview with a few seconds of playback.
|
|
|
|
|
|
|
|
class _VideoThumbnailPreview extends StatefulWidget {
|
|
|
|
|
|
|
|
final String videoUrl;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const _VideoThumbnailPreview({
|
|
|
|
|
|
|
|
required this.videoUrl,
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
|
|
State<_VideoThumbnailPreview> createState() => _VideoThumbnailPreviewState();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _VideoThumbnailPreviewState extends State<_VideoThumbnailPreview> {
|
|
|
|
|
|
|
|
VideoPlayerController? _controller;
|
|
|
|
|
|
|
|
bool _isInitialized = false;
|
|
|
|
|
|
|
|
bool _hasError = false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
|
|
void initState() {
|
|
|
|
|
|
|
|
super.initState();
|
|
|
|
|
|
|
|
_initializeVideo();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Future<void> _initializeVideo() async {
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
_controller = VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl));
|
|
|
|
|
|
|
|
await _controller!.initialize();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (mounted) {
|
|
|
|
|
|
|
|
setState(() {
|
|
|
|
|
|
|
|
_isInitialized = true;
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Play continuously in a loop
|
|
|
|
|
|
|
|
_controller!.setLooping(true);
|
|
|
|
|
|
|
|
_controller!.play();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
|
|
Logger.warning('Failed to initialize video thumbnail: $e');
|
|
|
|
|
|
|
|
if (mounted) {
|
|
|
|
|
|
|
|
setState(() {
|
|
|
|
|
|
|
|
_hasError = true;
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
|
|
void dispose() {
|
|
|
|
|
|
|
|
_controller?.dispose();
|
|
|
|
|
|
|
|
super.dispose();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
|
|
|
if (_hasError || !_isInitialized || _controller == null) {
|
|
|
|
|
|
|
|
return Stack(
|
|
|
|
|
|
|
|
fit: StackFit.expand,
|
|
|
|
|
|
|
|
children: [
|
|
|
|
|
|
|
|
Container(
|
|
|
|
|
|
|
|
color: Colors.black87,
|
|
|
|
|
|
|
|
child: const Center(
|
|
|
|
|
|
|
|
child: Icon(
|
|
|
|
|
|
|
|
Icons.play_circle_filled,
|
|
|
|
|
|
|
|
size: 40,
|
|
|
|
|
|
|
|
color: Colors.white70,
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return Stack(
|
|
|
|
|
|
|
|
fit: StackFit.expand,
|
|
|
|
|
|
|
|
children: [
|
|
|
|
|
|
|
|
AspectRatio(
|
|
|
|
|
|
|
|
aspectRatio: _controller!.value.aspectRatio,
|
|
|
|
|
|
|
|
child: VideoPlayer(_controller!),
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
// Play icon overlay
|
|
|
|
|
|
|
|
Positioned(
|
|
|
|
|
|
|
|
top: 4,
|
|
|
|
|
|
|
|
right: 4,
|
|
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
|
|
padding: const EdgeInsets.all(4),
|
|
|
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
|
|
|
color: Colors.black.withValues(alpha: 0.6),
|
|
|
|
|
|
|
|
shape: BoxShape.circle,
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
child: const Icon(
|
|
|
|
|
|
|
|
Icons.play_circle_filled,
|
|
|
|
|
|
|
|
size: 24,
|
|
|
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|