import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:gal/gal.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:http/http.dart' as http; import '../../design/pencil_theme.dart'; import '../../widgets/resilient_network_video.dart'; import '../report/report_screen.dart'; /// Generate result: full-bleed media (clipped), top nav, bottom save + report (design `2SyyL`). /// /// **Why match [HomeScreen]’s Scaffold:** with `extendBody: true`, Material /// wraps the body and adjusts [MediaQueryData.padding] for the subtree (see /// `scaffold.dart` `_BodyBuilder`), unlike the default used on the home page. /// That diverged from full-bleed behavior; this screen keeps `extendBody` false. class GenerateResultScreen extends StatefulWidget { const GenerateResultScreen({ super.key, required this.taskId, required this.resultUrl, }); final String taskId; final String resultUrl; @override State createState() => _GenerateResultScreenState(); } class _GenerateResultScreenState extends State { bool _saving = false; bool get _hasUrl { final u = widget.resultUrl.trim(); return u.startsWith('http://') || u.startsWith('https://'); } bool get _isVideo => _urlLooksLikeVideo(widget.resultUrl); Future _saveToGallery() async { if (!_hasUrl || _saving) return; setState(() => _saving = true); try { final ok = await Gal.hasAccess(); if (!ok) { await Gal.requestAccess(); } final uri = Uri.parse(widget.resultUrl.trim()); final res = await http.get(uri); if (res.statusCode < 200 || res.statusCode >= 300) { throw HttpException('HTTP ${res.statusCode}'); } final ext = _isVideo ? _guessVideoExt(widget.resultUrl) : '.jpg'; final file = File( '${Directory.systemTemp.path}/funymee_${widget.taskId}$ext', ); await file.writeAsBytes(res.bodyBytes); if (_isVideo) { await Gal.putVideo(file.path); } else { await Gal.putImage(file.path); } if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'Saved to Photos', style: GoogleFonts.inter(fontWeight: FontWeight.w600), ), ), ); } on GalException catch (e) { if (!mounted) return; ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(e.type.message))); } catch (e) { if (!mounted) return; ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('Save failed: $e'))); } finally { if (mounted) setState(() => _saving = false); } } void _openReport() { Navigator.of(context).push( MaterialPageRoute( builder: (_) => ReportScreen(taskId: widget.taskId), ), ); } @override Widget build(BuildContext context) { final bottomInset = MediaQuery.paddingOf(context).bottom; return AnnotatedRegion( value: const SystemUiOverlayStyle( statusBarColor: Colors.transparent, statusBarIconBrightness: Brightness.light, systemNavigationBarColor: Colors.black, systemNavigationBarIconBrightness: Brightness.light, ), child: Scaffold( backgroundColor: Colors.black, resizeToAvoidBottomInset: false, body: Stack( fit: StackFit.expand, clipBehavior: Clip.hardEdge, children: [ Positioned.fill( child: MediaQuery.removeViewPadding( context: context, removeTop: true, removeBottom: true, child: MediaQuery.removePadding( context: context, removeTop: true, removeBottom: true, child: ClipRect(child: _buildBackdrop()), ), ), ), Positioned( left: 0, right: 0, top: 0, height: 140, child: MediaQuery.removeViewPadding( context: context, removeTop: true, child: MediaQuery.removePadding( context: context, removeTop: true, child: DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.black.withValues(alpha: 0.55), Colors.black.withValues(alpha: 0), ], ), ), ), ), ), ), Positioned( left: 0, right: 0, top: 0, child: SafeArea( bottom: false, child: Padding( padding: const EdgeInsets.fromLTRB(2, 0, 14, 0), child: SizedBox( height: 56, child: Row( children: [ Material( color: Colors.transparent, child: InkWell( onTap: () => Navigator.of(context).pop(), borderRadius: BorderRadius.circular(14), child: const SizedBox( width: 44, height: 44, child: Icon( Icons.chevron_left_rounded, size: 26, color: Colors.white, shadows: [ Shadow( blurRadius: 8, color: Color(0x99000000), offset: Offset(0, 1), ), ], ), ), ), ), Expanded( child: Center( child: Text( 'Save', style: GoogleFonts.inter( fontSize: 19, fontWeight: FontWeight.w700, fontStyle: FontStyle.italic, color: Colors.white, shadows: const [ Shadow( blurRadius: 8, color: Color(0x66000000), offset: Offset(0, 1), ), ], ), ), ), ), const SizedBox(width: 44), ], ), ), ), ), ), Positioned( top: 0, right: 0, child: SafeArea( bottom: false, child: Padding( padding: const EdgeInsets.only(top: 6, right: 14), child: Text( 'FunyMeeAI', style: GoogleFonts.inter( fontSize: 11, fontWeight: FontWeight.w600, letterSpacing: 0.35, color: Colors.white.withValues(alpha: 0.72), shadows: const [ Shadow( blurRadius: 8, color: Color(0x99000000), offset: Offset(0, 1), ), ], ), ), ), ), ), Align( alignment: Alignment.bottomCenter, child: _ResultBottomBar( bottomInset: bottomInset, saving: _saving, onSave: _saveToGallery, onReport: _openReport, ), ), ], ), ), ); } Widget _buildBackdrop() { if (!_hasUrl) { return ColoredBox( color: PencilTheme.stone900, child: Center( child: Padding( padding: const EdgeInsets.all(24), child: Text( 'The result is not ready yet. Check History later.\nTask: ${widget.taskId}', textAlign: TextAlign.center, style: GoogleFonts.inter( color: Colors.white70, fontSize: 15, height: 1.4, ), ), ), ), ); } if (_isVideo) { final u = widget.resultUrl.trim(); return ResilientNetworkVideoCover( key: ValueKey(u), url: u, loadingWidget: const ColoredBox( color: Colors.black, child: Center( child: CircularProgressIndicator(color: Colors.white54), ), ), failedWidget: ColoredBox( color: PencilTheme.stone900, child: Center( child: Icon( Icons.videocam_off_outlined, size: 48, color: Colors.white54, ), ), ), ); } return SizedBox.expand( child: CachedNetworkImage( imageUrl: widget.resultUrl.trim(), fit: BoxFit.cover, width: double.infinity, height: double.infinity, progressIndicatorBuilder: (_, _, _) => const ColoredBox( color: Colors.black, child: Center( child: CircularProgressIndicator(color: Colors.white54), ), ), errorWidget: (_, _, _) => ColoredBox( color: PencilTheme.stone900, child: Center( child: Icon(Icons.broken_image, size: 48, color: Colors.white54), ), ), ), ); } } /// Bottom actions: gold CTA + hint + report on transparent background (no white bar). class _ResultBottomBar extends StatelessWidget { const _ResultBottomBar({ required this.bottomInset, required this.saving, required this.onSave, required this.onReport, }); final double bottomInset; final bool saving; final VoidCallback onSave; final VoidCallback onReport; static const _goldGradient = LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Color(0xFFEAB308), Color(0xFFCA8A04)], ); @override Widget build(BuildContext context) { return Padding( padding: EdgeInsets.fromLTRB(20, 16, 20, 34 + bottomInset), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Material( color: Colors.transparent, child: InkWell( onTap: saving ? null : onSave, borderRadius: BorderRadius.circular(999), child: Ink( height: 54, decoration: BoxDecoration( borderRadius: BorderRadius.circular(999), gradient: _goldGradient, boxShadow: const [ BoxShadow( color: Color(0x40B45309), blurRadius: 16, offset: Offset(0, 6), ), ], ), child: Center( child: saving ? const SizedBox( width: 22, height: 22, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : Text( 'Save to Photos', style: GoogleFonts.inter( fontSize: 17, fontWeight: FontWeight.w700, color: Colors.white, ), ), ), ), ), ), const SizedBox(height: 12), Material( color: Colors.transparent, child: InkWell( onTap: onReport, borderRadius: BorderRadius.circular(8), child: Padding( padding: const EdgeInsets.symmetric(vertical: 10), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.flag_outlined, size: 18, color: Colors.white.withValues(alpha: 0.88), ), const SizedBox(width: 6), Text( 'Report', style: GoogleFonts.inter( fontSize: 14, fontWeight: FontWeight.w600, color: Colors.white.withValues(alpha: 0.88), shadows: const [ Shadow( blurRadius: 6, color: Color(0x80000000), offset: Offset(0, 1), ), ], ), ), ], ), ), ), ), ], ), ); } } bool _urlLooksLikeVideo(String url) { if (url.isEmpty) return false; final lower = url.toLowerCase(); const hints = ['.mp4', '.m3u8', '.webm', '.mov', '.mkv', '.avi']; return hints.any((h) => lower.contains(h)); } String _guessVideoExt(String url) { final lower = url.toLowerCase(); if (lower.contains('.webm')) return '.webm'; if (lower.contains('.mov')) return '.mov'; if (lower.contains('.m3u8')) return '.mp4'; return '.mp4'; }