FunyMeeAI/lib/features/generate/generate_progress_screen.dart
2026-04-10 15:36:08 +08:00

194 lines
6.0 KiB
Dart

import 'dart:io';
import 'package:client_proxy_framework/client_proxy_framework.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../core/app_env.dart';
import '../../core/user/user_state.dart';
import '../../design/pencil_theme.dart';
import '../../widgets/pencil_chrome.dart';
import 'generate_result_screen.dart';
/// `YoZaK` 生成中 — 与 EYsUi 同款顶栏结构,标题「生成中」。
class GenerateProgressScreen extends StatefulWidget {
const GenerateProgressScreen({
super.key,
required this.taskId,
this.localPreviewPath,
});
final String taskId;
final String? localPreviewPath;
@override
State<GenerateProgressScreen> createState() => _GenerateProgressScreenState();
}
class _GenerateProgressScreenState extends State<GenerateProgressScreen> {
ImageProgressPollHandle? _pollHandle;
String _status = '';
int _progress = 0;
String? _resultUrl;
String? _error;
bool _navigated = false;
@override
void initState() {
super.initState();
_pollHandle = ImageProgressPoll.start(
app: currentBackendAppType(),
taskId: widget.taskId,
userId: UserState.userId.value,
interval: const Duration(seconds: 5),
onTick: _onProgressTick,
onTransientNetworkFailure: (n, max) {
if (!mounted || _navigated) return;
setState(() {
_status = 'Reconnecting… ($n/$max)';
});
},
onFatalError: (msg) {
if (!mounted || _navigated) return;
setState(() => _error = msg);
},
);
}
void _onProgressTick(ProgressPollTick tick) {
if (!mounted || _navigated) return;
final res = tick.response;
if (!res.isSuccess || res.data == null) {
setState(() => _error = res.msg.isNotEmpty ? res.msg : 'Progress error');
return;
}
final p = res.data!;
setState(() {
_status = p.status ?? '';
_progress = p.progress ?? 0;
_resultUrl = p.resultUrl;
});
final doneSuccess = ProgressPollSemantics.isSuccessTerminal(p.status) ||
ProgressPollSemantics.hasUsableResultUrl(p.resultUrl);
if (doneSuccess) {
_navigated = true;
_pollHandle?.cancel();
Navigator.of(context).pushReplacement(
MaterialPageRoute<void>(
builder: (_) => GenerateResultScreen(
taskId: widget.taskId,
resultUrl: _resultUrl ?? '',
),
),
);
return;
}
if (ProgressPollSemantics.isTerminalStatus(p.status) &&
ProgressPollSemantics.isFailureTerminal(p.status)) {
setState(() => _error ??= 'Task failed (${p.status})');
}
}
@override
void dispose() {
_pollHandle?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
gradient: PencilTheme.yellowWhitePageGradient,
),
child: Scaffold(
backgroundColor: Colors.transparent,
body: SafeArea(
bottom: false,
child: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(2, 0, 14, 10),
child: SizedBox(
height: 56,
child: Row(
children: [
PencilRoundBackButton(
onPressed: () => Navigator.of(context).pop(),
),
Expanded(
child: Center(
child: Text(
'Generating',
style: GoogleFonts.inter(
fontSize: 19,
fontWeight: FontWeight.w700,
fontStyle: FontStyle.italic,
color: PencilTheme.stone900,
),
),
),
),
const SizedBox(width: 44),
],
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
children: [
if (widget.localPreviewPath != null)
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: AspectRatio(
aspectRatio: 1,
child: Image.file(
File(widget.localPreviewPath!),
fit: BoxFit.cover,
),
),
)
else
const SizedBox(height: 48),
const SizedBox(height: 24),
if (_error != null)
Text(
_error!,
style: GoogleFonts.inter(color: Colors.red),
textAlign: TextAlign.center,
)
else ...[
LinearProgressIndicator(
value: _progress > 0 ? _progress / 100 : null,
color: PencilTheme.underlineGold,
),
const SizedBox(height: 16),
Text(
_status.isEmpty ? 'Processing…' : _status,
style: GoogleFonts.inter(color: PencilTheme.stone600),
),
Text(
'$_progress%',
style: GoogleFonts.inter(
fontSize: 28,
fontWeight: FontWeight.w700,
color: PencilTheme.stone900,
),
),
],
],
),
),
),
],
),
),
),
);
}
}