315 lines
9.2 KiB
Dart
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,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|