mp4 improved with preview

master
gitea 2 months ago
parent 78d729f523
commit 222515a2b5

@ -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';
@ -1470,9 +1471,9 @@ 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,
),
),
),
],
);
}
}

@ -1,5 +1,6 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.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';
@ -834,37 +835,104 @@ class _RecipeCard extends StatelessWidget {
} }
Widget _buildVideoThumbnail(String videoUrl) { Widget _buildVideoThumbnail(String videoUrl) {
return _VideoThumbnailPreview(videoUrl: videoUrl);
}
}
/// 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( return Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
Container( AspectRatio(
color: Colors.black87, aspectRatio: _controller!.value.aspectRatio,
child: const Center( child: VideoPlayer(_controller!),
child: Icon(
Icons.play_circle_filled,
size: 40,
color: Colors.white70,
),
),
), ),
// Play icon overlay
Positioned( Positioned(
bottom: 4, top: 4,
left: 4,
right: 4, right: 4,
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), padding: const EdgeInsets.all(4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.7), color: Colors.black.withValues(alpha: 0.6),
borderRadius: BorderRadius.circular(4), shape: BoxShape.circle,
), ),
child: const Text( child: const Icon(
'MP4', Icons.play_circle_filled,
style: TextStyle( size: 24,
color: Colors.white, color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
), ),
), ),
), ),

@ -1,5 +1,6 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.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';
@ -1053,47 +1054,115 @@ class _RecipeCard extends StatelessWidget {
} }
Widget _buildVideoThumbnail(String videoUrl) { Widget _buildVideoThumbnail(String videoUrl) {
return _VideoThumbnailPreview(videoUrl: videoUrl);
}
Color _getRatingColor(int rating) {
if (rating >= 4) return Colors.green;
if (rating >= 2) return Colors.orange;
return Colors.red;
}
}
/// 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( return Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
Container( AspectRatio(
color: Colors.black87, aspectRatio: _controller!.value.aspectRatio,
child: const Center( child: VideoPlayer(_controller!),
child: Icon(
Icons.play_circle_filled,
size: 40,
color: Colors.white70,
),
),
), ),
// Play icon overlay
Positioned( Positioned(
bottom: 4, top: 4,
left: 4,
right: 4, right: 4,
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), padding: const EdgeInsets.all(4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.7), color: Colors.black.withValues(alpha: 0.6),
borderRadius: BorderRadius.circular(4), shape: BoxShape.circle,
), ),
child: const Text( child: const Icon(
'MP4', Icons.play_circle_filled,
style: TextStyle( size: 24,
color: Colors.white, color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
), ),
), ),
), ),
], ],
); );
} }
Color _getRatingColor(int rating) {
if (rating >= 4) return Colors.green;
if (rating >= 2) return Colors.orange;
return Colors.red;
}
} }

Loading…
Cancel
Save

Powered by TurnKey Linux.