import 'package:flutter/material.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_lucide/flutter_lucide.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:video_player/video_player.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_spacing.dart'; /// Video card for home grid - 点击播放按钮可在卡片上播放视频 /// 同时只能一个卡片处于播放状态 class VideoCard extends StatefulWidget { const VideoCard({ super.key, required this.imageUrl, this.videoUrl, this.credits = '50', required this.isActive, required this.onPlayRequested, required this.onStopRequested, this.onGenerateSimilar, }); final String imageUrl; final String? videoUrl; final String credits; final bool isActive; final VoidCallback onPlayRequested; final VoidCallback onStopRequested; final VoidCallback? onGenerateSimilar; @override State createState() => _VideoCardState(); } class _VideoCardState extends State { VideoPlayerController? _controller; bool _isLoading = false; @override void initState() { super.initState(); if (widget.isActive) { WidgetsBinding.instance.addPostFrameCallback((_) => _loadAndPlay()); } } @override void dispose() { _disposeController(); super.dispose(); } @override void didUpdateWidget(covariant VideoCard oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.isActive && !widget.isActive) { _stop(); } else if (!oldWidget.isActive && widget.isActive) { _loadAndPlay(); } } void _stop() { _controller?.removeListener(_onVideoUpdate); _controller?.pause(); if (mounted) { setState(() => _isLoading = false); } } void _disposeController() { _controller?.removeListener(_onVideoUpdate); _controller?.dispose(); _controller = null; } Future _loadAndPlay() async { if (widget.videoUrl == null || widget.videoUrl!.isEmpty) { widget.onStopRequested(); return; } if (_controller != null && _controller!.value.isInitialized) { final needSeek = _controller!.value.position >= _controller!.value.duration && _controller!.value.duration.inMilliseconds > 0; if (needSeek) { setState(() => _isLoading = true); await _controller!.seekTo(Duration.zero); } _controller!.addListener(_onVideoUpdate); await _controller!.play(); if (mounted) setState(() => _isLoading = false); return; } setState(() => _isLoading = true); try { final file = await DefaultCacheManager().getSingleFile(widget.videoUrl!); if (!mounted || !widget.isActive) { setState(() => _isLoading = false); return; } _controller = VideoPlayerController.file(file); await _controller!.initialize(); _controller!.addListener(_onVideoUpdate); if (mounted && widget.isActive) { await _controller!.play(); setState(() => _isLoading = false); } } catch (e) { if (mounted) { _disposeController(); setState(() => _isLoading = false); widget.onStopRequested(); } } } void _onVideoUpdate() { if (_controller != null && _controller!.value.position >= _controller!.value.duration && _controller!.value.duration.inMilliseconds > 0) { _controller!.removeListener(_onVideoUpdate); _controller!.seekTo(Duration.zero); if (mounted) widget.onStopRequested(); } } void _onPlayButtonTap() { if (widget.isActive) { widget.onStopRequested(); _stop(); } else { widget.onPlayRequested(); } } bool get _isPlaying => widget.isActive && _controller != null && _controller!.value.isPlaying; @override Widget build(BuildContext context) { final showVideo = widget.isActive && _controller != null && _controller!.value.isInitialized; final showLoading = widget.isActive && _isLoading; return LayoutBuilder( builder: (context, constraints) { return Container( width: constraints.maxWidth, height: constraints.maxHeight, decoration: BoxDecoration( color: AppColors.surfaceAlt, borderRadius: BorderRadius.circular(24), border: Border.all(color: AppColors.border, width: 1), boxShadow: [ BoxShadow( color: AppColors.shadowMedium, blurRadius: 12, offset: const Offset(0, 4), ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(24), child: Stack( fit: StackFit.expand, children: [ if (showVideo) Positioned.fill( child: FittedBox( fit: BoxFit.cover, child: SizedBox( width: _controller!.value.size.width > 0 ? _controller!.value.size.width : 16, height: _controller!.value.size.height > 0 ? _controller!.value.size.height : 9, child: VideoPlayer(_controller!), ), ), ) else CachedNetworkImage( imageUrl: widget.imageUrl, fit: BoxFit.cover, placeholder: (_, __) => Container( color: AppColors.surfaceAlt, ), errorWidget: (_, __, ___) => Container( color: AppColors.surfaceAlt, ), ), Positioned( top: 12, right: 12, child: Container( padding: const EdgeInsets.symmetric( horizontal: AppSpacing.md, vertical: AppSpacing.xs, ), decoration: BoxDecoration( color: AppColors.overlayDark, borderRadius: BorderRadius.circular(14), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( LucideIcons.sparkles, size: 12, color: AppColors.surface, ), const SizedBox(width: AppSpacing.sm), Text( widget.credits, style: const TextStyle( color: AppColors.surface, fontSize: 11, fontWeight: FontWeight.w600, fontFamily: 'Inter', ), ), ], ), ), ), Positioned.fill( child: Center( child: GestureDetector( onTap: widget.videoUrl != null && widget.videoUrl!.isNotEmpty ? _onPlayButtonTap : null, child: Container( width: 48, height: 48, decoration: BoxDecoration( color: AppColors.surface.withValues(alpha: 0.9), borderRadius: BorderRadius.circular(24), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.13), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: showLoading ? const Padding( padding: EdgeInsets.all(12), child: CircularProgressIndicator( color: AppColors.textPrimary, strokeWidth: 2, ), ) : Icon( _isPlaying ? LucideIcons.pause : LucideIcons.play, size: 24, color: AppColors.textPrimary, ), ), ), ), ), if (_isPlaying) Positioned( top: 8, left: 8, child: GestureDetector( onTap: () { widget.onStopRequested(); _stop(); }, child: Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: AppColors.overlayDark, borderRadius: BorderRadius.circular(20), ), child: const Icon( LucideIcons.x, size: 16, color: AppColors.surface, ), ), ), ), Positioned( bottom: 12, left: 12, right: 12, child: GestureDetector( onTap: widget.onGenerateSimilar, child: Container( height: 44, padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( color: AppColors.primary, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: AppColors.primaryShadow.withValues(alpha: 0.25), blurRadius: 6, offset: const Offset(0, 2), ), ], ), alignment: Alignment.center, child: const Text( 'Generate Similar', style: TextStyle( color: AppColors.surface, fontSize: 12, fontWeight: FontWeight.w600, fontFamily: 'Inter', ), ), ), ), ), ], ), ), ); }, ); } }