petsHero-AI/lib/features/generate_video/generation_result_screen.dart

442 lines
13 KiB
Dart

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:path_provider/path_provider.dart';
import 'package:video_player/video_player.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<GenerationResultScreen> createState() => _GenerationResultScreenState();
}
class _GenerationResultScreenState extends State<GenerationResultScreen> {
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<void> _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<void> _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: () {
// TODO: Report action
},
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,
),
),
],
),
),
);
}
}
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,
),
),
],
),
),
);
}
}