设置 App 图标与启动页,并修复记分详情回显与环值分布溢出。

同步恢复试用次数校验,免费额度调整为 2 次,便于验证 IAP 流程。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-09 03:50:03 +08:00
co-authored by Cursor
parent 4385c6941e
commit 6901b08a94
71 changed files with 429 additions and 342 deletions
@@ -74,13 +74,13 @@ class PointBookHomeScreen extends ConsumerWidget {
if (config == null) return;
// IAP / trial gate.
// if (!config.canStartScoring) {
// await showDialog<void>(
// context: context,
// builder: (_) => const IapUnlockDialog(),
// );
// return;
// }
if (!config.canStartScoring) {
await showDialog<void>(
context: context,
builder: (_) => const IapUnlockDialog(),
);
return;
}
// Draft check.
final notifier = ref.read(pointRecordsProvider.notifier);
@@ -79,40 +79,40 @@ class _Bar extends StatelessWidget {
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final maxH = constraints.maxHeight - 40; // reserve space for count + label
final h = (maxH * ratio).clamp(4.0, maxH);
return Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
'$count',
style: TextStyle(
fontSize: 11,
color: AppTheme.textSecondary,
return Column(
children: [
Text(
'$count',
style: TextStyle(
fontSize: 11,
color: AppTheme.textSecondary,
),
),
const SizedBox(height: 4),
Expanded(
child: Align(
alignment: Alignment.bottomCenter,
child: FractionallySizedBox(
heightFactor: ratio.clamp(0.04, 1.0),
widthFactor: 1,
child: DecoratedBox(
decoration: BoxDecoration(
color: AppTheme.primary,
borderRadius: BorderRadius.circular(4),
),
),
),
const SizedBox(height: 4),
Container(
width: double.infinity,
height: h,
decoration: BoxDecoration(
color: AppTheme.primary,
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(height: 4),
Text(
label,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
],
);
},
),
),
const SizedBox(height: 4),
Text(
label,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
],
);
}
}
@@ -11,8 +11,10 @@ final pointRecordsProvider =
);
/// Loads a single record by id (used by the edit/view page).
/// Watches [pointRecordsProvider] so upsert/complete invalidates the cache.
final recordByIdProvider =
FutureProvider.family<PointRecord?, int>((ref, id) async {
ref.watch(pointRecordsProvider);
final isar = await ref.watch(isarProvider.future);
return isar.pointRecords.get(id);
});
@@ -46,6 +46,7 @@ class _PointBookEditPageState extends ConsumerState<PointBookEditPage> {
],
),
body: recordAsync.when(
skipLoadingOnReload: true,
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('加载失败:$e')),
data: (record) {
@@ -57,6 +58,11 @@ class _PointBookEditPageState extends ConsumerState<PointBookEditPage> {
_scores = List<int>.from(record.scores);
_hitPoints = List<HitPoint>.from(record.hitPoints);
_initialized = true;
} else if (widget.readOnly) {
// Detail view: always mirror latest provider/Isar data.
_record = record;
_scores = List<int>.from(record.scores);
_hitPoints = List<HitPoint>.from(record.hitPoints);
}
return _buildBody(context, record);
},
@@ -81,61 +87,63 @@ class _PointBookEditPageState extends ConsumerState<PointBookEditPage> {
),
const SizedBox(height: 12),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Center(
child: AspectRatio(
aspectRatio: 1.0,
child: LayoutBuilder(
builder: (context, constraints) {
final size = constraints.maxWidth;
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTapUp: widget.readOnly || isComplete || _saving
? null
: (details) => _onTap(details, size, record),
child: Stack(
alignment: Alignment.center,
children: [
Image.asset(
'assets/images/target_face.png',
width: size,
height: size,
fit: BoxFit.fill,
),
CustomPaint(
size: Size(size, size),
painter: HitPointPainter(
hitPoints: _hitPoints,
highlightIndex: _hitPoints.isEmpty
? null
: _hitPoints.length - 1,
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Center(
child: AspectRatio(
aspectRatio: 1.0,
child: LayoutBuilder(
builder: (context, constraints) {
final size = constraints.maxWidth;
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTapUp: widget.readOnly || isComplete || _saving
? null
: (details) => _onTap(details, size, record),
child: Stack(
alignment: Alignment.center,
children: [
Image.asset(
'assets/images/target_face.png',
width: size,
height: size,
fit: BoxFit.fill,
),
),
if (widget.readOnly && _hitPoints.isEmpty)
Text(
'无落点数据',
style: TextStyle(color: AppTheme.textSecondary),
),
if (!widget.readOnly && isComplete)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'已射完,点击右上角保存',
style: TextStyle(color: Colors.white),
CustomPaint(
size: Size(size, size),
painter: HitPointPainter(
hitPoints: _hitPoints,
highlightIndex: _hitPoints.isEmpty
? null
: _hitPoints.length - 1,
),
),
],
),
);
},
if (widget.readOnly && _hitPoints.isEmpty)
Text(
'无落点数据',
style: TextStyle(color: AppTheme.textSecondary),
),
if (!widget.readOnly && isComplete)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'已射完,点击右上角保存',
style: TextStyle(color: Colors.white),
),
),
],
),
);
},
),
),
),
),
@@ -262,42 +270,44 @@ class _ProgressHeader extends StatelessWidget {
width: double.infinity,
color: AppTheme.primary.withValues(alpha: 0.06),
padding: const EdgeInsets.fromLTRB(16, 12, 16, 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
record.title,
style: const TextStyle(fontWeight: FontWeight.w600),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
record.title,
style: const TextStyle(fontWeight: FontWeight.w600),
),
),
),
Text(
'$placed / $total',
style: const TextStyle(
fontWeight: FontWeight.w700,
color: AppTheme.primaryDark,
),
),
],
),
const SizedBox(height: 6),
if (placed < total)
Text(
'$placed / $total',
style: const TextStyle(
fontWeight: FontWeight.w700,
color: AppTheme.primaryDark,
),
'${currentEnd + 1}/${record.endCount} 组 · 本组第 ${currentArrow + 1}/${record.arrowsPerEnd}',
style: TextStyle(fontSize: 12, color: AppTheme.textSecondary),
),
const SizedBox(height: 8),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: total == 0 ? 0 : placed / total,
minHeight: 6,
backgroundColor: AppTheme.primary.withValues(alpha: 0.15),
),
],
),
const SizedBox(height: 6),
if (placed < total)
Text(
'${currentEnd + 1}/${record.endCount} 组 · 本组第 ${currentArrow + 1}/${record.arrowsPerEnd}',
style: TextStyle(fontSize: 12, color: AppTheme.textSecondary),
),
const SizedBox(height: 8),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: total == 0 ? 0 : placed / total,
minHeight: 6,
backgroundColor: AppTheme.primary.withValues(alpha: 0.15),
),
),
],
],
),
),
);
}