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'; /// 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; @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(() {}); } 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) { await _controller!.seekTo(Duration.zero); } _controller!.addListener(_onVideoUpdate); await _controller!.play(); if (mounted) setState(() {}); return; } setState(() {}); try { final file = await DefaultCacheManager().getSingleFile(widget.videoUrl!); if (!mounted || !widget.isActive) return; _controller = VideoPlayerController.file(file); await _controller!.initialize(); _controller!.addListener(_onVideoUpdate); if (mounted && widget.isActive) { await _controller!.play(); setState(() {}); } } catch (e) { if (mounted) { _disposeController(); setState(() {}); 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; return LayoutBuilder( builder: (context, constraints) { return Container( width: constraints.maxWidth, height: constraints.maxHeight, decoration: BoxDecoration( color: Colors.transparent, borderRadius: BorderRadius.circular(24), 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: GestureDetector( onTap: _onPlayButtonTap, behavior: HitTestBehavior.opaque, 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 Positioned.fill( child: GestureDetector( onTap: widget.videoUrl != null && widget.videoUrl!.isNotEmpty ? _onPlayButtonTap : null, behavior: HitTestBehavior.opaque, child: CachedNetworkImage( imageUrl: widget.imageUrl, fit: BoxFit.cover, placeholder: (_, __) => Container( color: AppColors.surfaceAlt, ), errorWidget: (_, __, ___) => Container( color: AppColors.surfaceAlt, ), ), ), ), Positioned( top: 12, right: 12, child: Container( height: 24, padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: AppColors.overlayDark, borderRadius: BorderRadius.circular(14), ), child: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( LucideIcons.sparkles, size: 12, color: AppColors.surface, ), const SizedBox(width: 4), Text( widget.credits, style: const TextStyle( color: AppColors.surface, fontSize: 11, fontWeight: FontWeight.w600, fontFamily: 'Inter', ), ), ], ), ), ), 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: 16, left: 12, right: 12, child: Center( child: IntrinsicWidth( child: GestureDetector( onTap: widget.onGenerateSimilar, child: Container( height: 24, padding: const EdgeInsets.symmetric(horizontal: 10), decoration: BoxDecoration( color: AppColors.primaryButtonFill, borderRadius: BorderRadius.circular(12), border: Border.all( color: AppColors.primary, width: 1, ), boxShadow: [ BoxShadow( color: AppColors.primaryButtonShadow, 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', ), ), ), ), ), ), ), ], ), ), ); }, ); } }