import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_lucide/flutter_lucide.dart'; import '../../core/api/api_config.dart'; import '../../core/auth/auth_service.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_spacing.dart'; import '../../core/theme/app_typography.dart'; import '../../core/user/user_state.dart'; import '../../shared/widgets/top_nav_bar.dart'; import '../../core/api/services/image_api.dart'; import '../../core/api/services/user_api.dart'; import '../../features/gallery/models/gallery_task_item.dart'; import '../../shared/tab_selector_scope.dart'; import '../../shared/widgets/bottom_nav_bar.dart'; /// Progress states: 1=Queued 2=Processing 3=Completed 4=Timeout 5=Error 6=Aborted /// Progress bar has 3 stages; states 3–6 are stage 3. const _stateLabels = { 1: 'Queued', 2: 'Processing', 3: 'Completed', 4: 'Timeout', 5: 'Error', 6: 'Aborted', }; /// Stage progress: 1 -> 1/3, 2 -> 2/3, 3..6 -> 1.0 double _progressForState(int? state) { if (state == null) return 0; if (state == 1) return 1 / 3; if (state == 2) return 2 / 3; return 1.0; // 3, 4, 5, 6 } int? _parseProgressTaskId(dynamic raw) { if (raw == null) return null; if (raw is int) return raw > 0 ? raw : null; if (raw is num) { final v = raw.toInt(); return v > 0 ? v : null; } final v = int.tryParse(raw.toString()); return (v != null && v > 0) ? v : null; } /// Build GalleryMediaItem from /v1/image/progress response (data = sidekick). /// curate[].reconfigure = imgUrl, reconnect(imgType): 2=视频,1或其他=图片 GalleryMediaItem? _mediaItemFromProgressData( Map data, { int? taskId, }) { final curate = data['curate'] as List?; if (curate == null || curate.isEmpty) return null; final first = curate.first; if (first is! Map) return null; final reconfigure = first['reconfigure'] as String?; if (reconfigure == null || reconfigure.isEmpty) return null; final reconnect = first['reconnect']; final imgType = reconnect is int ? reconnect : reconnect is num ? reconnect.toInt() : 1; if (imgType == 2) { return GalleryMediaItem(videoUrl: reconfigure, taskId: taskId); } return GalleryMediaItem(imageUrl: reconfigure, taskId: taskId); } /// Generate Video Progress screen - matches Pencil rSN3T (video), Eghqc (label) class GenerateProgressScreen extends StatefulWidget { const GenerateProgressScreen({super.key, this.taskId, this.imagePath}); final dynamic taskId; final String? imagePath; @override State createState() => _GenerateProgressScreenState(); } class _GenerateProgressScreenState extends State { int? _state; Timer? _pollTimer; bool _isFetching = false; double get _progress => _progressForState(_state); void _onBack(BuildContext context) { final completed = _state == 3; if (!completed) { TabSelectorScope.maybeOf(context)?.selectTab(NavTab.gallery); Navigator.of(context).popUntil(ModalRoute.withName('/')); } else { Navigator.of(context).pop(); } } @override void initState() { super.initState(); if (widget.taskId != null) { _startPolling(); } else { _state = 2; } } @override void dispose() { _pollTimer?.cancel(); super.dispose(); } Future _startPolling() async { await AuthService.loginComplete; _pollTimer = Timer.periodic( const Duration(seconds: 5), (_) => _fetchProgress(), ); _fetchProgress(); } Future _fetchProgress() async { if (widget.taskId == null) return; if (_isFetching) return; _isFetching = true; try { final res = await ImageApi.getProgress( sentinel: ApiConfig.appId, tree: widget.taskId.toString(), asset: UserState.userId.value, ); if (!res.isSuccess || res.data == null) return; final data = res.data as Map; final state = (data['listing'] as num?)?.toInt(); if (!mounted) return; setState(() => _state = state); switch (state) { case 3: // 完成 _pollTimer?.cancel(); // 生成成功后同步更新积分 final userId = UserState.userId.value; if (userId != null && userId.isNotEmpty) { try { final accountRes = await UserApi.getAccount( sentinel: ApiConfig.appId, asset: userId, ); if (accountRes.isSuccess && accountRes.data != null) { final accountData = accountRes.data as Map?; final credits = accountData?['reveal'] as int?; if (credits != null) { UserState.setCredits(credits); } } } catch (_) {} } if (!mounted) return; final mediaItem = _mediaItemFromProgressData( data, taskId: _parseProgressTaskId(widget.taskId), ); Navigator.of(context).pushReplacementNamed( '/result', arguments: mediaItem, ); break; case 4: case 5: case 6: // 超时 / 错误 / 中止 _pollTimer?.cancel(); break; } } catch (_) {} finally { _isFetching = false; } } @override Widget build(BuildContext context) { final labelText = _stateLabels[_state] ?? 'Queued'; return Scaffold( backgroundColor: AppColors.background, appBar: PreferredSize( preferredSize: const Size.fromHeight(56), child: TopNavBar( title: 'Generating', showBackButton: true, onBack: () => _onBack(context), ), ), body: SingleChildScrollView( padding: const EdgeInsets.all(AppSpacing.screenPaddingLarge), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _VideoPreview(imagePath: widget.imagePath), const SizedBox(height: AppSpacing.xxl), Text( 'Generating may take some time. Please wait patiently.', textAlign: TextAlign.center, style: AppTypography.bodyRegular.copyWith( color: AppColors.textSecondary, ), ), const SizedBox(height: AppSpacing.xxl), _ProgressSection(progress: _progress, label: labelText), ], ), ), ); } } /// Video area rSN3T: show uploaded image with crop (BoxFit.cover) class _VideoPreview extends StatelessWidget { const _VideoPreview({this.imagePath}); final String? imagePath; @override Widget build(BuildContext context) { final hasImage = imagePath != null && imagePath!.isNotEmpty && File(imagePath!).existsSync(); return Container( height: 360, decoration: BoxDecoration( color: AppColors.surfaceAlt, borderRadius: BorderRadius.circular(16), border: Border.all(color: AppColors.border, width: 1), ), clipBehavior: Clip.antiAlias, child: hasImage ? Image.file( File(imagePath!), fit: BoxFit.cover, width: double.infinity, height: double.infinity, ) : Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( LucideIcons.film, size: 64, color: AppColors.textSecondary, ), const SizedBox(height: AppSpacing.lg), Text( 'Previewing', style: AppTypography.bodyRegular.copyWith( color: AppColors.textSecondary, ), ), ], ), ); } } /// Progress bar: 3 stages. Label shows state (Queued|Processing|Completed|Timeout|Error|Aborted) class _ProgressSection extends StatelessWidget { const _ProgressSection({required this.progress, required this.label}); final double progress; final String label; @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( label, style: AppTypography.bodyMedium.copyWith( fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.textPrimary, ), ), const SizedBox(height: AppSpacing.lg), LayoutBuilder( builder: (context, constraints) { final fillWidth = constraints.maxWidth * progress.clamp(0.0, 1.0); return Stack( children: [ Container( height: 8, decoration: BoxDecoration( color: AppColors.border, borderRadius: BorderRadius.circular(4), ), ), Positioned( left: 0, top: 0, bottom: 0, child: Container( width: fillWidth, decoration: BoxDecoration( color: AppColors.primary, borderRadius: BorderRadius.circular(4), ), ), ), ], ); }, ), ], ); } }