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

315 lines
9.2 KiB
Dart

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/auth/auth_service.dart';
import '../../core/user/user_state.dart';
import '../../design/pencil_theme.dart';
import '../../widgets/pencil_chrome.dart';
import '../generate/generate_result_screen.dart';
import 'credit_record_tab.dart';
import 'widgets/history_grid_card.dart';
/// `WBRp4` My History — 黄→白渐变、顶栏、双 Tab、24h 提示、双列卡片。
class HistoryScreen extends StatefulWidget {
const HistoryScreen({
super.key,
this.isRootTab = false,
this.isTabSelected = true,
});
/// When true (e.g. bottom tab), hide back button; when pushed, show back.
final bool isRootTab;
/// Bottom shell: only `true` when the History tab is selected — defers `my-tasks` load.
/// Pushed routes should default `true` so load runs as before.
final bool isTabSelected;
@override
State<HistoryScreen> createState() => _HistoryScreenState();
}
class _HistoryScreenState extends State<HistoryScreen> {
int _tab = 0;
bool _loading = false;
String? _error;
List<MyTaskItem> _items = [];
Map<String, String> _localCovers = {};
VoidCallback? _cancelLoginWait;
@override
void initState() {
super.initState();
_cancelLoginWait = AuthService.whenLoginSucceeded(
onReady: _tryLoadTasks,
onFailed: () {
if (!mounted || !widget.isTabSelected) return;
setState(() {
_loading = false;
_error = 'Sign in failed';
});
},
);
}
@override
void didUpdateWidget(HistoryScreen oldWidget) {
super.didUpdateWidget(oldWidget);
if (!oldWidget.isTabSelected && widget.isTabSelected) {
_tryLoadTasks();
}
}
@override
void dispose() {
_cancelLoginWait?.call();
super.dispose();
}
/// `GET /v1/image/my-tasks` only when this tab is visible and login succeeded.
void _tryLoadTasks() {
if (!widget.isTabSelected) return;
if (!AuthService.isLoginComplete.value) return;
final uid = UserState.userId.value;
if (uid == null || uid.isEmpty) return;
_load();
}
Future<void> _load() async {
setState(() {
_loading = true;
_error = null;
});
final res = await ImageApi.getMyTasks(
app: currentBackendAppType(),
page: '1',
pageSize: '30',
);
if (!mounted) return;
if (!res.isSuccess || res.data == null) {
setState(() {
_loading = false;
_error = res.msg.isNotEmpty ? res.msg : 'Failed to load';
});
return;
}
final tasks = res.data!.tasks ?? [];
final locals = await ImageTaskHistory.localCoverPathsForMyTaskItems(tasks);
setState(() {
_loading = false;
_items = tasks;
_localCovers = locals;
});
}
@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(4, 0, 4, 8),
child: SizedBox(
height: 58,
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 12),
child: Row(
children: [
if (widget.isRootTab)
const SizedBox(width: 44)
else
PencilRoundBackButton(
onPressed: () => Navigator.of(context).pop(),
),
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_headerTab('My History', 0),
const SizedBox(width: 26),
_headerTab('Credit Record', 1),
],
),
),
const SizedBox(width: 44),
],
),
),
),
),
Expanded(
child: _tab == 0 ? _myHistoryBody() : const CreditRecordTab(),
),
],
),
),
),
);
}
Widget _headerTab(String label, int index) {
final selected = _tab == index;
return InkWell(
onTap: () => setState(() => _tab = index),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
label,
style: GoogleFonts.inter(
fontSize: 17,
fontWeight: selected ? FontWeight.w600 : FontWeight.w500,
fontStyle: FontStyle.italic,
color: selected ? PencilTheme.ink : PencilTheme.inkSoft,
),
),
const SizedBox(height: 8),
if (selected)
Container(
width: 50,
height: 4,
decoration: BoxDecoration(
color: PencilTheme.underlineGold,
borderRadius: BorderRadius.circular(2),
),
)
else
const SizedBox(height: 4),
],
),
);
}
Widget _myHistoryBody() {
if (!widget.isTabSelected) {
return const SizedBox.shrink();
}
if (!AuthService.isLoginComplete.value) {
return const Center(child: CircularProgressIndicator());
}
final uid = UserState.userId.value;
if (uid == null || uid.isEmpty) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
_error ?? 'Sign in failed',
textAlign: TextAlign.center,
),
],
),
);
}
if (_loading) {
return const Center(child: CircularProgressIndicator());
}
if (_error != null) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(_error!),
TextButton(onPressed: _load, child: const Text('Retry')),
],
),
);
}
return RefreshIndicator(
onRefresh: _load,
child: CustomScrollView(
slivers: [
SliverPadding(
padding: const EdgeInsets.fromLTRB(16, 4, 16, 12),
sliver: SliverToBoxAdapter(child: _expiryNotice()),
),
if (_items.isEmpty)
SliverFillRemaining(
child: Center(
child: Text('No tasks yet.',
style: GoogleFonts.inter(color: PencilTheme.inkSoft)),
),
)
else
SliverPadding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 28),
sliver: SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 14,
crossAxisSpacing: 14,
childAspectRatio: 171 / 182,
),
delegate: SliverChildBuilderDelegate(
(context, i) {
final t = _items[i];
final id = t.taskId ?? '';
return HistoryGridCard(
item: t,
localCoverPath:
id.isEmpty ? null : _localCovers[id],
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => GenerateResultScreen(
taskId: id,
resultUrl: t.resultUrl?.trim() ?? '',
),
),
);
},
onDownload: () {},
);
},
childCount: _items.length,
),
),
),
],
),
);
}
Widget _expiryNotice() {
return Container(
padding: const EdgeInsets.fromLTRB(12, 10, 12, 10),
decoration: BoxDecoration(
color: PencilTheme.expiryBg,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: PencilTheme.expiryBorder),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'24-hour expiry',
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w600,
color: PencilTheme.expiryHead,
),
),
const SizedBox(height: 4),
Text(
'Each item is kept for 24 hours after creation. Download before it expires.',
style: GoogleFonts.inter(
fontSize: 11,
height: 1.35,
color: PencilTheme.expiryBody,
),
),
],
),
);
}
}