222 lines
7.4 KiB
Dart
222 lines
7.4 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import 'package:arcx/app/app_theme.dart';
|
|
import 'package:arcx/data/local/models/models.dart';
|
|
import 'package:arcx/features/config/application/app_config_controller.dart';
|
|
import 'package:arcx/features/home/presentation/widgets/heatmap_card.dart';
|
|
import 'package:arcx/features/home/presentation/widgets/profile_card.dart';
|
|
import 'package:arcx/features/home/presentation/widgets/score_distribution_card.dart';
|
|
import 'package:arcx/features/home/presentation/widgets/stats_card.dart';
|
|
import 'package:arcx/features/iap/presentation/iap_unlock_dialog.dart';
|
|
import 'package:arcx/features/profile/presentation/profile_edit_dialog.dart';
|
|
import 'package:arcx/features/records/presentation/point_book_list_page.dart';
|
|
import 'package:arcx/features/scoring/application/scoring_service.dart';
|
|
import 'package:arcx/features/scoring/presentation/point_book_create_page.dart';
|
|
import 'package:arcx/features/scoring/presentation/point_book_edit_page.dart';
|
|
import 'package:arcx/features/stats/application/stats.dart';
|
|
|
|
class PointBookHomeScreen extends ConsumerWidget {
|
|
const PointBookHomeScreen({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final statsAsync = ref.watch(statsProvider);
|
|
final configAsync = ref.watch(appConfigProvider);
|
|
|
|
return Scaffold(
|
|
backgroundColor: AppTheme.surfaceMuted,
|
|
appBar: AppBar(title: const Text('记分本')),
|
|
body: statsAsync.when(
|
|
loading: () => const Center(child: CircularProgressIndicator()),
|
|
error: (e, _) => Center(child: Text('数据加载失败:$e')),
|
|
data: (stats) => RefreshIndicator(
|
|
onRefresh: () async => ref.invalidate(statsProvider),
|
|
child: ListView(
|
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
|
|
children: [
|
|
ProfileCard(onEdit: () => _editProfile(context)),
|
|
const SizedBox(height: 12),
|
|
_EntitlementBanner(configAsync: configAsync),
|
|
const SizedBox(height: 12),
|
|
StatsCard(stats: stats),
|
|
const SizedBox(height: 12),
|
|
HeatmapCard(stats: stats),
|
|
const SizedBox(height: 12),
|
|
ScoreDistributionCard(stats: stats),
|
|
const SizedBox(height: 20),
|
|
_EntryButtons(
|
|
onOpenRecords: () => _openRecords(context),
|
|
onStartScoring: () => _startScoring(context, ref),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _editProfile(BuildContext context) async {
|
|
await showDialog<void>(
|
|
context: context,
|
|
builder: (_) => const ProfileEditDialog(),
|
|
);
|
|
}
|
|
|
|
Future<void> _openRecords(BuildContext context) async {
|
|
await Navigator.of(
|
|
context,
|
|
).push(MaterialPageRoute<void>(builder: (_) => const PointBookListPage()));
|
|
}
|
|
|
|
Future<void> _startScoring(BuildContext context, WidgetRef ref) async {
|
|
final config = ref.read(appConfigProvider).value;
|
|
if (config == null) return;
|
|
|
|
// IAP / trial gate.
|
|
// if (!config.canStartScoring) {
|
|
// await showDialog<void>(
|
|
// context: context,
|
|
// builder: (_) => const IapUnlockDialog(),
|
|
// );
|
|
// return;
|
|
// }
|
|
|
|
// Draft check.
|
|
final notifier = ref.read(pointRecordsProvider.notifier);
|
|
final draft = await notifier.currentDraft();
|
|
if (draft != null && context.mounted) {
|
|
final choice = await _showDraftConfirm(context);
|
|
if (choice == null) return;
|
|
if (choice == true) {
|
|
await _goToEdit(context, draft);
|
|
return;
|
|
}
|
|
await notifier.clearDrafts();
|
|
}
|
|
|
|
if (!context.mounted) return;
|
|
await _goToCreate(context);
|
|
}
|
|
|
|
Future<void> _goToCreate(BuildContext context) async {
|
|
final created = await Navigator.of(context).push<PointRecord>(
|
|
MaterialPageRoute<PointRecord>(
|
|
builder: (_) => const PointBookCreatePage(),
|
|
),
|
|
);
|
|
// Create already popped itself before returning, so the stack is [Home]
|
|
// here. A plain push yields [Home, Edit] and back/popUntil lands on Home.
|
|
if (created != null && context.mounted) {
|
|
await _goToEdit(context, created);
|
|
}
|
|
}
|
|
|
|
Future<void> _goToEdit(BuildContext context, PointRecord record) async {
|
|
await Navigator.of(context).push(
|
|
MaterialPageRoute<void>(
|
|
builder: (_) => PointBookEditPage(recordId: record.id),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<bool?> _showDraftConfirm(BuildContext context) {
|
|
return showDialog<bool>(
|
|
context: context,
|
|
builder: (_) => AlertDialog(
|
|
title: const Text('存在未完成草稿'),
|
|
content: const Text('检测到上次有未完成的记分草稿,是否继续编辑?选择“重新计分”将删除该草稿。'),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context, false),
|
|
child: const Text('重新计分'),
|
|
),
|
|
FilledButton(
|
|
onPressed: () => Navigator.pop(context, true),
|
|
child: const Text('继续编辑'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _EntitlementBanner extends StatelessWidget {
|
|
const _EntitlementBanner({required this.configAsync});
|
|
final AsyncValue<AppConfig> configAsync;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return configAsync.when(
|
|
loading: () => const SizedBox.shrink(),
|
|
error: (_, __) => const SizedBox.shrink(),
|
|
data: (config) {
|
|
final unlocked = config.isVipUnlocked;
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: unlocked
|
|
? AppTheme.primary.withValues(alpha: 0.12)
|
|
: AppTheme.accent.withValues(alpha: 0.18),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
unlocked ? Icons.verified_outlined : Icons.timer_outlined,
|
|
color: unlocked ? AppTheme.primary : AppTheme.accent,
|
|
size: 20,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
unlocked
|
|
? '已解锁无限记分权益'
|
|
: '免费试用剩余 ${config.remainingTrialCount}/${config.freeTrialLimit} 次',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
color: unlocked ? AppTheme.primaryDark : AppTheme.accent,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class _EntryButtons extends StatelessWidget {
|
|
const _EntryButtons({
|
|
required this.onOpenRecords,
|
|
required this.onStartScoring,
|
|
});
|
|
final VoidCallback onOpenRecords;
|
|
final VoidCallback onStartScoring;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Row(
|
|
children: [
|
|
Expanded(
|
|
child: OutlinedButton.icon(
|
|
onPressed: onOpenRecords,
|
|
icon: const Icon(Icons.list_alt_outlined),
|
|
label: const Text('计分记录'),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: ElevatedButton.icon(
|
|
onPressed: onStartScoring,
|
|
icon: const Icon(Icons.play_arrow),
|
|
label: const Text('开始记分'),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|