import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_lucide/flutter_lucide.dart'; import 'package:gal/gal.dart'; import 'package:http/http.dart' as http; import 'package:image_picker/image_picker.dart'; import 'package:path_provider/path_provider.dart'; import 'package:video_player/video_player.dart'; import '../../core/api/services/feedback_api.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_spacing.dart'; import '../../core/theme/app_typography.dart'; import '../../features/gallery/models/gallery_task_item.dart'; import '../../shared/widgets/top_nav_bar.dart'; /// Video Generation Result screen - matches Pencil cFA4T class GenerationResultScreen extends StatefulWidget { const GenerationResultScreen({super.key, this.mediaItem}); final GalleryMediaItem? mediaItem; @override State createState() => _GenerationResultScreenState(); } class _GenerationResultScreenState extends State { VideoPlayerController? _videoController; bool _saving = false; bool _videoLoading = true; String? _videoLoadError; String? get _videoUrl => widget.mediaItem?.videoUrl; String? get _imageUrl => widget.mediaItem?.imageUrl; @override void initState() { super.initState(); if (_videoUrl != null) { _initVideoFromCache(); } } Future _initVideoFromCache() async { if (_videoUrl == null) return; try { final file = await DefaultCacheManager().getSingleFile(_videoUrl!); if (!mounted) return; final controller = VideoPlayerController.file(file); await controller.initialize(); if (!mounted) return; setState(() { _videoController = controller; _videoLoading = false; }); } catch (e) { if (mounted) { setState(() { _videoLoading = false; _videoLoadError = e.toString(); }); } } } @override void dispose() { _videoController?.dispose(); super.dispose(); } Future _saveToAlbum() async { if (widget.mediaItem == null) return; setState(() => _saving = true); try { final hasAccess = await Gal.hasAccess(toAlbum: true); if (!hasAccess) { final granted = await Gal.requestAccess(toAlbum: true); if (!granted) { throw Exception('Photo library access denied'); } } if (_videoUrl != null) { final tempDir = await getTemporaryDirectory(); final file = File( '${tempDir.path}/gallery_video_${DateTime.now().millisecondsSinceEpoch}.mp4'); final response = await http.get(Uri.parse(_videoUrl!)); if (response.statusCode != 200) { throw Exception('Failed to download video'); } await file.writeAsBytes(response.bodyBytes); await Gal.putVideo(file.path); await file.delete(); } else if (_imageUrl != null) { final tempDir = await getTemporaryDirectory(); final file = File( '${tempDir.path}/gallery_image_${DateTime.now().millisecondsSinceEpoch}.jpg'); final response = await http.get(Uri.parse(_imageUrl!)); if (response.statusCode != 200) { throw Exception('Failed to download image'); } await file.writeAsBytes(response.bodyBytes); await Gal.putImage(file.path); await file.delete(); } if (mounted) { setState(() => _saving = false); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Saved to photo library')), ); } } catch (e) { if (mounted) { setState(() => _saving = false); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Save failed: $e')), ); } } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColors.background, appBar: PreferredSize( preferredSize: const Size.fromHeight(56), child: TopNavBar( title: 'Ready', showBackButton: true, onBack: () => Navigator.of(context).pop(), ), ), body: widget.mediaItem == null ? const Center( child: Text( 'No media', style: TextStyle(color: AppColors.textSecondary), ), ) : SingleChildScrollView( padding: const EdgeInsets.all(AppSpacing.screenPaddingLarge), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _MediaDisplay( videoUrl: _videoUrl, imageUrl: _imageUrl, videoController: _videoController, videoLoading: _videoUrl != null ? _videoLoading : false, videoLoadError: _videoLoadError, ), const SizedBox(height: AppSpacing.xxl), _DownloadButton( onDownload: _saving ? null : _saveToAlbum, saving: _saving, ) ], ), ), ); } } class _MediaDisplay extends StatelessWidget { const _MediaDisplay({ this.videoUrl, this.imageUrl, this.videoController, this.videoLoading = false, this.videoLoadError, }); final String? videoUrl; final String? imageUrl; final VideoPlayerController? videoController; final bool videoLoading; final String? videoLoadError; @override Widget build(BuildContext context) { return Container( height: 360, decoration: BoxDecoration( color: AppColors.textPrimary, borderRadius: BorderRadius.circular(16), border: Border.all(color: AppColors.border, width: 1), ), child: ClipRRect( borderRadius: BorderRadius.circular(16), child: Stack( fit: StackFit.expand, children: [ videoUrl != null && videoController != null ? _VideoPlayer( controller: videoController!, ) : videoUrl != null && videoLoading ? Container( color: AppColors.textPrimary, alignment: Alignment.center, child: const CircularProgressIndicator( color: AppColors.surface, ), ) : videoUrl != null && videoLoadError != null ? Container( color: AppColors.textPrimary, alignment: Alignment.center, child: const Text( 'Load failed', style: TextStyle(color: AppColors.surface), ), ) : imageUrl != null ? CachedNetworkImage( imageUrl: imageUrl!, fit: BoxFit.cover, placeholder: (_, __) => _Placeholder(), errorWidget: (_, __, ___) => _Placeholder(), ) : _Placeholder(), Positioned( top: 16, right: 20, child: GestureDetector( onTap: () => _ReportDialog.show(context), child: Container( width: 44, height: 44, decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.6), borderRadius: BorderRadius.circular(8), ), alignment: Alignment.center, child: Icon( LucideIcons.triangle_alert, size: 24, color: AppColors.surface, ), ), ), ), Positioned( bottom: 20, right: 20, child: Text( 'PetsHero AI', style: AppTypography.bodyMedium.copyWith( color: AppColors.surface.withValues(alpha: 0.6), fontSize: 12, ), ), ), ], ), ), ); } } class _VideoPlayer extends StatefulWidget { const _VideoPlayer({required this.controller}); final VideoPlayerController controller; @override State<_VideoPlayer> createState() => _VideoPlayerState(); } class _VideoPlayerState extends State<_VideoPlayer> { @override void initState() { super.initState(); widget.controller.addListener(_listener); } @override void dispose() { widget.controller.removeListener(_listener); super.dispose(); } void _listener() { if (mounted) setState(() {}); } @override Widget build(BuildContext context) { if (!widget.controller.value.isInitialized) { return Container( color: AppColors.textPrimary, child: const Center( child: CircularProgressIndicator(color: AppColors.surface), ), ); } return GestureDetector( onTap: () { if (widget.controller.value.isPlaying) { widget.controller.pause(); } else { widget.controller.play(); } }, child: FittedBox( fit: BoxFit.contain, child: SizedBox( width: widget.controller.value.size.width > 0 ? widget.controller.value.size.width : 16, height: widget.controller.value.size.height > 0 ? widget.controller.value.size.height : 9, child: VideoPlayer(widget.controller), ), ), ); } } class _Placeholder extends StatelessWidget { @override Widget build(BuildContext context) { return Container( color: AppColors.textPrimary, alignment: Alignment.center, child: Text( 'Your work is ready', style: AppTypography.bodyRegular.copyWith( color: AppColors.surface.withValues(alpha: 0.6), ), ), ); } } class _DownloadButton extends StatelessWidget { const _DownloadButton({ required this.onDownload, this.saving = false, }); final VoidCallback? onDownload; final bool saving; @override Widget build(BuildContext context) { return GestureDetector( onTap: onDownload, child: Container( height: 52, decoration: BoxDecoration( color: saving ? AppColors.surfaceAlt : AppColors.primary, borderRadius: BorderRadius.circular(14), boxShadow: saving ? null : [ BoxShadow( color: AppColors.primaryShadow.withValues(alpha: 0.25), blurRadius: 6, offset: const Offset(0, 2), ), ], ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ if (saving) const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: AppColors.textSecondary, ), ) else const Icon(LucideIcons.download, size: 20, color: AppColors.surface), const SizedBox(width: AppSpacing.md), Text( saving ? 'Saving...' : 'Save to Album', style: AppTypography.bodyMedium.copyWith( color: AppColors.surface, ), ), ], ), ), ); } } /// Report dialog - matches Pencil 5qKUB class _ReportDialog extends StatefulWidget { const _ReportDialog({required this.parentContext}); final BuildContext parentContext; static void show(BuildContext context) { showDialog( context: context, barrierColor: Colors.black54, builder: (_) => _ReportDialog(parentContext: context), ); } @override State<_ReportDialog> createState() => _ReportDialogState(); } class _ReportDialogState extends State<_ReportDialog> { final _controller = TextEditingController(); File? _pickedImage; final _picker = ImagePicker(); bool _submitting = false; String? _errorText; @override void dispose() { _controller.dispose(); super.dispose(); } Future _pickImage() async { final x = await _picker.pickImage(source: ImageSource.gallery); if (x != null && mounted) { setState(() => _pickedImage = File(x.path)); } } Future _submit() async { final content = _controller.text.trim(); if (content.isEmpty) { setState(() => _errorText = 'Please describe the issue'); return; } if (_pickedImage == null) { setState(() => _errorText = 'Please upload an image'); return; } setState(() { _errorText = null; _submitting = true; }); try { final file = _pickedImage!; final ext = file.path.split('.').last.toLowerCase(); final contentType = ext == 'png' ? 'image/png' : ext == 'gif' ? 'image/gif' : 'image/jpeg'; final fileName = 'feedback_${DateTime.now().millisecondsSinceEpoch}.$ext'; final presignedRes = await FeedbackApi.getUploadPresignedUrl(fileName: fileName); if (!presignedRes.isSuccess || presignedRes.data == null) { throw Exception( presignedRes.msg.isNotEmpty ? presignedRes.msg : 'Failed to get upload URL'); } final data = presignedRes.data as Map; final uploadUrl = data['shed'] as String?; final filePath = data['hunt'] as String?; if (uploadUrl == null || uploadUrl.isEmpty || filePath == null || filePath.isEmpty) { throw Exception('Invalid presigned URL response'); } final bytes = await file.readAsBytes(); final uploadResponse = await http.put( Uri.parse(uploadUrl), headers: {'Content-Type': contentType}, body: bytes, ); if (uploadResponse.statusCode < 200 || uploadResponse.statusCode >= 300) { throw Exception('Upload failed: ${uploadResponse.statusCode}'); } final submitRes = await FeedbackApi.submit( fileUrls: [filePath], content: content, contentType: 'text/plain', ); if (!submitRes.isSuccess) { throw Exception( submitRes.msg.isNotEmpty ? submitRes.msg : 'Failed to submit report'); } if (!mounted) return; Navigator.of(context).pop(); if (widget.parentContext.mounted) { ScaffoldMessenger.of(widget.parentContext).showSnackBar( const SnackBar( content: Text('Report submitted'), behavior: SnackBarBehavior.floating, ), ); } } catch (e) { if (mounted) { setState(() => _errorText = e.toString().replaceAll('Exception: ', '')); } } finally { if (mounted) { setState(() => _submitting = false); } } } @override Widget build(BuildContext context) { return Dialog( backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.symmetric(horizontal: 24), child: Container( width: 342, padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.15), blurRadius: 24, offset: const Offset(0, 8), ), ], ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Report', style: AppTypography.bodyMedium.copyWith( fontSize: 20, fontWeight: FontWeight.w700, color: AppColors.textPrimary, ), ), GestureDetector( onTap: () => Navigator.of(context).pop(), child: const SizedBox( width: 40, height: 40, child: Icon( LucideIcons.x, size: 24, color: AppColors.textSecondary, ), ), ), ], ), const SizedBox(height: 20), Container( height: 120, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.background, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.border), ), child: TextField( controller: _controller, maxLines: null, decoration: const InputDecoration( hintText: 'Describe the issue...', hintStyle: TextStyle(color: AppColors.textMuted), border: InputBorder.none, contentPadding: EdgeInsets.zero, isDense: true, ), ), ), const SizedBox(height: 20), GestureDetector( onTap: _pickImage, child: Container( height: 120, decoration: BoxDecoration( color: AppColors.surfaceAlt, borderRadius: BorderRadius.circular(12), border: Border.all( color: const Color(0xFFD4D4D8), width: 2, ), ), child: _pickedImage != null ? ClipRRect( borderRadius: BorderRadius.circular(10), child: Image.file( _pickedImage!, fit: BoxFit.cover, width: double.infinity, height: double.infinity, ), ) : Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( LucideIcons.image_plus, size: 32, color: AppColors.textMuted, ), const SizedBox(height: 8), Text( 'Tap to upload image', style: TextStyle( color: AppColors.textSecondary, fontSize: 14, ), ), ], ), ), ), if (_errorText != null) ...[ const SizedBox(height: 12), Text( _errorText!, style: TextStyle( color: AppColors.accentOrange, fontSize: 14, ), ), ], const SizedBox(height: 20), GestureDetector( onTap: _submitting ? null : _submit, child: Container( height: 52, decoration: BoxDecoration( color: _submitting ? AppColors.textMuted : const Color(0xFF7C3AED), borderRadius: BorderRadius.circular(14), ), alignment: Alignment.center, child: _submitting ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: AppColors.surface, ), ) : Text( 'Submit', style: AppTypography.bodyMedium.copyWith( color: AppColors.surface, fontSize: 16, fontWeight: FontWeight.w600, ), ), ), ), ], ), ), ); } } class _ShareButton extends StatelessWidget { const _ShareButton({required this.onShare}); final VoidCallback onShare; @override Widget build(BuildContext context) { return GestureDetector( onTap: onShare, child: Container( height: 52, decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(14), border: Border.all(color: AppColors.border), boxShadow: const [ BoxShadow( color: AppColors.shadowLight, blurRadius: 6, offset: Offset(0, 2), ), ], ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(LucideIcons.share_2, size: 20, color: AppColors.primary), const SizedBox(width: AppSpacing.md), Text( 'Share', style: AppTypography.bodyMedium.copyWith( color: AppColors.textPrimary, ), ), ], ), ), ); } }