7 Commits
Author SHA1 Message Date
linfeng 4385c6941e 修复白屏BUG、UI 边界溢出 BUG 2026-08-09 01:35:48 +08:00
linfeng 4a357024bc 修复保存记分无法返回上一页 2026-08-08 13:29:05 +08:00
linfeng 636d936474 增加数据结构源 2026-08-08 11:20:14 +08:00
linfeng 9f70ac99b4 完善 IOS 配置 2026-08-08 11:06:26 +08:00
linfeng 604af909b0 init 2026-08-08 10:57:54 +08:00
linfeng d0d48c1ffc update 2026-08-07 18:31:37 +08:00
linfeng ccda415cd7 更新代码工程 2026-08-07 09:36:05 +08:00
200 changed files with 13798 additions and 16393 deletions
+7
View File
@@ -0,0 +1,7 @@
---
name: grill-me
description: A relentless interview to sharpen a plan or design.
disable-model-invocation: true
---
Run a `/grilling` session.
@@ -0,0 +1,5 @@
interface:
display_name: "Grill Me"
short_description: "Sharpen a plan through interview"
policy:
allow_implicit_invocation: false
+7
View File
@@ -0,0 +1,7 @@
---
name: grill-with-docs
description: A relentless interview to sharpen a plan or design, which also creates docs (ADR's and glossary) as we go.
disable-model-invocation: true
---
Run a `/grilling` session, using the `/domain-modeling` skill.
@@ -0,0 +1,5 @@
interface:
display_name: "Grill with Docs"
short_description: "Grill a design and write its docs"
policy:
allow_implicit_invocation: false
+22
View File
@@ -0,0 +1,22 @@
---
name: grilling
description: Grill the user relentlessly about a plan, decision, or idea. Use when the user wants to stress-test their thinking, or uses any 'grill' trigger phrases.
---
Interview the user relentlessly until you reach a shared understanding. Map this as a **design tree**: every decision branches into the decisions that hang off it.
Work the tree in **rounds**. The **frontier** is every decision whose prerequisites are already settled — the questions you can ask _now_ without guessing at answers you haven't heard yet. Ask the whole frontier in one round: number each question and give your recommended answer. Then wait for the user's answers before the next round.
Each question should be formatted like so:
```
❓ **Q1** - **<question title>**: <question body, might be multiple paragraphs, including multiple choices>
➡️ <your recommended answer>
```
Each round the user answers reshapes the tree — settled decisions push the frontier outward and unblock questions that depended on them. Recompute the frontier and ask the next round. A question whose answer depends on another question still open in this round belongs to a _later_ round, not this one.
Finding _facts_ is your job, never the user's. When a frontier question needs a fact from the environment (filesystem, tools, etc.), dispatch a sub-agent to find it — don't ask the user for anything you could look up yourself. Don't block on it: a running exploration is an unsettled prerequisite, so only the questions downstream of it wait for the sub-agent to report — ask the rest of the frontier now. The _decisions_ are the user's — put each to them and wait.
The session is done when the frontier is empty: every branch of the design tree visited, nothing left silently assumed. Do not act on it until the user confirms you have reached a shared understanding.
@@ -0,0 +1,3 @@
interface:
display_name: "Grilling"
short_description: "Stress-test thinking a round of questions at a time"
+44 -3
View File
@@ -1,4 +1,45 @@
unpackage # Miscellaneous
dist *.class
node_modules *.log
*.pyc
*.swp
.DS_Store .DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
+33
View File
@@ -0,0 +1,33 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "924134a44c189315be2148659913dda1671cbe99"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 924134a44c189315be2148659913dda1671cbe99
base_revision: 924134a44c189315be2148659913dda1671cbe99
- platform: android
create_revision: 924134a44c189315be2148659913dda1671cbe99
base_revision: 924134a44c189315be2148659913dda1671cbe99
- platform: ios
create_revision: 924134a44c189315be2148659913dda1671cbe99
base_revision: 924134a44c189315be2148659913dda1671cbe99
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
@@ -0,0 +1,164 @@
# 修复 UI 边界溢出与记分页白屏问题
## 问题概述
1. **记分页白屏(严重)**:进入记分页后白屏,由 `OutlinedButton` 收到无限宽度约束导致整个 `Scaffold` 布局失败引起
2. **首页环值分布卡片溢出**`score_distribution_card.dart:86` Column 底部溢出 13px
3. **创建页数字选择器溢出**`point_book_create_page.dart:187` Row 右侧溢出 14px
## 根因分析
### 白屏根因:`SizedBox(height: size)` 与 `Expanded` 的约束冲突
`point_book_edit_page.dart` 第 82-141 行的结构:
```
Expanded( // 给子节点 tight height = 剩余高度 (如 200px)
child: Padding(
child: LayoutBuilder(
builder: (context, constraints) {
final size = constraints.maxWidth; // 如 350px (屏幕宽度)
return SizedBox(
width: size, // 350
height: size, // 350 ← 与 Expanded 的 200px 冲突!
child: GestureDetector(...),
);
},
),
),
)
```
**约束冲突原理**
- `Expanded` 给子节点 `BoxConstraints(minHeight: 200, maxHeight: 200)` (tight)
- `SizedBox(height: 350)` 给子节点 `BoxConstraints(minHeight: 350, maxHeight: 350)` (tight)
- 交集:`minHeight: max(200,350)=350, maxHeight: min(200,350)=200`**350 > 200,不可能约束!**
- 布局失败级联到整个 Column → Scaffold → 白屏
### 环值分布卡片溢出根因
`score_distribution_card.dart` 第 84 行:`maxH = constraints.maxHeight - 28`
柱状图 `_Bar` 的 Column 内容总高度 = count文字(~15) + spacing(4) + bar(最大 maxH=112) + spacing(4) + label文字(~16) = 151px,但可用高度仅 140px,溢出 11-13px。
### 创建页溢出根因
`point_book_create_page.dart` 第 187 行:`_NumberPicker` 内的 Row 包含两个默认尺寸 `IconButton`(各 48px)+ 文字 + 标签文字,在半屏宽度内放不下。
## 修改方案
### 1. `point_book_edit_page.dart` — 修复白屏(关键)
**文件**`lib/features/scoring/presentation/point_book_edit_page.dart`
**行号**82-141
`Expanded > Padding > LayoutBuilder > SizedBox` 结构改为 `Expanded > Padding > Center > AspectRatio > LayoutBuilder`
```dart
// 修改前(第 85-90 行):
child: LayoutBuilder(
builder: (context, constraints) {
final size = constraints.maxWidth;
return SizedBox(
width: size,
height: size,
child: GestureDetector(
// 修改后:
child: Center(
child: AspectRatio(
aspectRatio: 1.0,
child: LayoutBuilder(
builder: (context, constraints) {
final size = constraints.maxWidth;
return GestureDetector(
```
关键变更:
-`Padding``LayoutBuilder` 之间插入 `Center(child: AspectRatio(aspectRatio: 1.0))`
- 删除 `SizedBox(width: size, height: size)` 包装层
- `Center` 松解 `Expanded` 的 tight height 约束 → `AspectRatio` 计算 min(宽,高) 的正方形 → `LayoutBuilder` 获取的 `constraints.maxWidth == constraints.maxHeight` → 无冲突
- `Image.asset``CustomPaint``size: Size(size, size)` 保持不变
- 对应关闭括号:需要增加 `),` (AspectRatio) 和 `),` (Center) 的闭合
### 2. `point_book_create_page.dart` — 修复 Row 溢出 14px
**文件**`lib/features/scoring/presentation/point_book_create_page.dart`
**行号**187-216`_NumberPicker` 的 build 方法)
修改 `_NumberPicker` 中的两个 `IconButton`,使其更紧凑:
```dart
// 修改前(第 193-198 行):
IconButton(
icon: const Icon(Icons.remove_circle_outline),
onPressed: () {
// 修改后:
IconButton(
icon: const Icon(Icons.remove_circle_outline),
iconSize: 20,
visualDensity: VisualDensity.compact,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () {
```
同样修改 `add` 按钮(第 207-211 行)。
并将标签文字包裹 `Flexible`(第 190 行):
```dart
// 修改前:
Text(label, style: const TextStyle(fontSize: 13)),
// 修改后:
Flexible(
child: Text(label, style: const TextStyle(fontSize: 13)),
),
```
### 3. `score_distribution_card.dart` — 修复 Column 溢出 13px
**文件**`lib/features/home/presentation/widgets/score_distribution_card.dart`
**行号**:第 84 行
```dart
// 修改前:
final maxH = constraints.maxHeight - 28; // reserve space for label
// 修改后:
final maxH = constraints.maxHeight - 40; // reserve space for count + label
```
将预留空间从 28px 增加到 40px,确保柱子最大高度不会导致 Column 溢出。
### 4. `heatmap_card.dart` — 修复 CustomPaint 尺寸为零(附带修复)
**文件**`lib/features/home/presentation/widgets/heatmap_card.dart`
**行号**:第 29 行
当前 `Stack``CustomPaint` 没有指定 `size`,在 `Stack` 中默认尺寸为 0x0,导致命中点不可见。
```dart
// 修改前(第 29 行):
child: Stack(
alignment: Alignment.center,
children: [
// 修改后:
child: Stack(
fit: StackFit.expand,
alignment: Alignment.center,
children: [
```
添加 `fit: StackFit.expand` 使所有非定位子节点(`Image.asset``CustomPaint`)填满 Stack。
## 验证步骤
1. 运行 `flutter analyze` 确认无错误
2. 启动 APP,进入首页 → 确认无黄色溢出条纹
3. 点击"开始记分" → 进入创建页 → 确认数字选择器无溢出
4. 完成创建 → 进入记分页 → 确认 **不白屏**,靶面正常显示
5. 点击靶面 → 确认命中点准确渲染
6. 返回首页 → 查看热力图 → 确认命中点可见
+3
View File
@@ -0,0 +1,3 @@
{
"java.compile.nullAnalysis.mode": "disabled"
}
+17
View File
@@ -0,0 +1,17 @@
# arcx
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
+28
View File
@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
+14
View File
@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks
+45
View File
@@ -0,0 +1,45 @@
plugins {
id("com.android.application")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.shelingxingqiu.arcx"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.shelingxingqiu.arcx"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
}
}
flutter {
source = "../.."
}
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+45
View File
@@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="arcx"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
@@ -0,0 +1,5 @@
package com.shelingxingqiu.arcx
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+24
View File
@@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
File diff suppressed because one or more lines are too long
+6
View File
@@ -0,0 +1,6 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
# This newDsl flag was added by the Flutter template
android.newDsl=false
# This builtInKotlin flag was added by the Flutter template
android.builtInKotlin=false
+5
View File
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip
+26
View File
@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "9.0.1" apply false
id("org.jetbrains.kotlin.android") version "2.3.20" apply false
}
include(":app")
+1
View File
@@ -0,0 +1 @@
Place image assets for the local-first Flutter app here.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

+21
View File
@@ -0,0 +1,21 @@
## 接口与数据文件清单
进入 `point-book.vue` 页面调用的接口(已排除个人信息 `getHomeData`),均来自**测试环境** `apitest.shelingxingqiu.com`
| # | 接口函数 | URL | 调用时机 | 数据文件 |
| --- | ---------------------------- | ------------------------------------------------------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------- |
| 1 | `getPointBookConfigAPI` | `GET /user/score/sheet/option` | onMounted | [getPointBookConfigAPI.json](file:///Users/ZhuanZ/Documents/gdfw/shoot-miniprograms/datas/getPointBookConfigAPI.json) |
| 2 | `getPointBookStatisticsAPI` | `GET /v2/user/score/sheet/statistics` | onShow / user 变化 | [getPointBookStatisticsAPI.json](file:///Users/ZhuanZ/Documents/gdfw/shoot-miniprograms/datas/getPointBookStatisticsAPI.json) |
| 3 | `getPointBookRankListAPI(1)` | `GET /user/score/sheet/week/shoot/rank/list?pageNum=1&pageSize=100` | onShow | [getPointBookRankListAPI.json](file:///Users/ZhuanZ/Documents/gdfw/shoot-miniprograms/datas/getPointBookRankListAPI.json) |
## 关于环境的说明
之前失败是因为我默认请求了生产环境 `api.shelingxingqiu.com`,而你的 token 是测试环境签发的(小程序 envVersion 为 develop/trial 时走 `apitest.shelingxingqiu.com`,见 [apis.js:9-20](file:///Users/ZhuanZ/Documents/gdfw/shoot-miniprograms/src/apis.js#L9-L20))。切换到测试环境后三个接口均返回 `code:0` 成功。
## 数据结构说明
每个 JSON 文件保存的是接口响应中 `data` 字段的内容(与前端 `request` 函数 resolve 的结果一致,见 [apis.js:50](file:///Users/ZhuanZ/Documents/gdfw/shoot-miniprograms/src/apis.js#L50))。
- **Config**`bowOption`(5种弓) + `targetOption`(10种靶),两环境数据一致
- **Statistics**:签到/今日与累计箭数/训练天数/黄心率/平均环数/各环数分布/本周箭着点坐标(当前 `weekArrows` 为空)
- **Rank**`my`(当前用户) + `list`(榜单,本周共2人上榜) + `total`
+91
View File
@@ -0,0 +1,91 @@
{
"bowOption": [
{
"id": 1,
"name": "反曲弓",
"icon": "https://static.shelingxingqiu.com/attachment/2025-08-04/dbt8n02c0mwlgpcihn.png"
},
{
"id": 2,
"name": "复合弓",
"icon": "https://static.shelingxingqiu.com/attachment/2025-08-04/dbt8n02d17qt8548j4.png"
},
{
"id": 3,
"name": "美洲猎弓",
"icon": "https://static.shelingxingqiu.com/attachment/2025-08-04/dbt8n02cv1b8ucjp4y.png"
},
{
"id": 4,
"name": "传统弓",
"icon": "https://static.shelingxingqiu.com/attachment/2025-08-04/dbt8n02cohc2omofsb.png"
},
{
"id": 5,
"name": "光弓",
"icon": "https://static.shelingxingqiu.com/attachment/2025-08-04/dbt8n01ru48k0bwwz8.png"
}
],
"targetOption": [
{
"id": 1,
"name": "40 全环靶",
"icon": "https://static.shelingxingqiu.com/target20251126/%E5%85%A8%E7%8E%AF%E9%9D%B6@3x.svg",
"iconPng": "https://static.shelingxingqiu.com/shootTarget/shoot@2x.png"
},
{
"id": 2,
"name": "80 全环靶",
"icon": "https://static.shelingxingqiu.com/target20251126/%E5%85%A8%E7%8E%AF%E9%9D%B6@3x.svg",
"iconPng": "https://static.shelingxingqiu.com/shootTarget/shoot@2x.png"
},
{
"id": 3,
"name": "122 全环靶",
"icon": "https://static.shelingxingqiu.com/target20251126/%E5%85%A8%E7%8E%AF%E9%9D%B6@3x.svg",
"iconPng": "https://static.shelingxingqiu.com/shootTarget/shoot@2x.png"
},
{
"id": 4,
"name": "40 半环靶",
"icon": "https://static.shelingxingqiu.com/target20251126/%E5%8D%8A%E7%8E%AF%E9%9D%B6.svg",
"iconPng": "https://static.shelingxingqiu.com/shootTarget/shoot@2x(1).png"
},
{
"id": 5,
"name": "60 半环靶",
"icon": "https://static.shelingxingqiu.com/target20251126/%E5%8D%8A%E7%8E%AF%E9%9D%B6.svg",
"iconPng": "https://static.shelingxingqiu.com/shootTarget/shoot@2x(1).png"
},
{
"id": 6,
"name": "80 半环靶",
"icon": "https://static.shelingxingqiu.com/target20251126/%E5%8D%8A%E7%8E%AF%E9%9D%B6.svg",
"iconPng": "https://static.shelingxingqiu.com/shootTarget/shoot@2x(1).png"
},
{
"id": 7,
"name": "三连靶",
"icon": "https://static.shelingxingqiu.com/20260310target/%E4%B8%89%E8%BF%9E.svg",
"iconPng": "https://static.shelingxingqiu.com/shootTarget/shoot@2x(2).png"
},
{
"id": 8,
"name": "品字靶",
"icon": "https://static.shelingxingqiu.com/target20251126/%E5%93%81%E5%AD%97%E9%9D%B6.svg",
"iconPng": "https://static.shelingxingqiu.com/shootTarget/shoot@2x(3).png"
},
{
"id": 9,
"name": "复合 三连靶",
"icon": "https://static.shelingxingqiu.com/20260310target/%E5%A4%8D%E5%90%88%E4%B8%89%E8%BF%9E.svg",
"iconPng": "https://static.shelingxingqiu.com/shootTarget/shoot@2x(5).png"
},
{
"id": 10,
"name": "复合 品字靶",
"icon": "https://static.shelingxingqiu.com/target20251126/%E5%A4%8D%E5%90%88%E5%93%81%E5%AD%97%E9%9D%B6.svg",
"iconPng": "https://static.shelingxingqiu.com/shootTarget/shoot@2x(4).png"
}
]
}
+44
View File
@@ -0,0 +1,44 @@
{
"my": {
"id": 339,
"name": "高桥凉介(发量惊人)",
"avatar": "https://static.shelingxingqiu.com/attachment/2026-08-08/dkj77wp1cmk3ovqjimjpeg",
"totalDay": 1,
"averageRing": 7,
"weekArrow": 0,
"ifLike": false,
"rank": 0,
"likeTotal": 0,
"vip": true,
"sVip": true
},
"list": [
{
"id": 212,
"name": "Amer就是总部的喵美酱啦!𓆡",
"avatar": "https://static.shelingxingqiu.com/attachment/2026-08-07/dkijshwr9k3i0aiyqnjpeg",
"totalDay": 29,
"averageRing": 5.681518151815181,
"weekArrow": 60,
"ifLike": false,
"rank": 1,
"likeTotal": 0,
"vip": false,
"sVip": false
},
{
"id": 338,
"name": "Sylar",
"avatar": "https://static.shelingxingqiu.com/attachment/2026-08-07/dkibj7cfdx34jh04gtjpeg",
"totalDay": 1,
"averageRing": 4.583333333333333,
"weekArrow": 24,
"ifLike": false,
"rank": 2,
"likeTotal": 0,
"vip": true,
"sVip": false
}
],
"total": 2
}
+24
View File
@@ -0,0 +1,24 @@
{
"weeksCheckIn": [false, false, false, false, false, false, false],
"todayTotalArrow": 0,
"totalArrow": 3,
"totalDay": 1,
"averageRing": 7,
"yellowRate": 0.3333,
"checkInCount": -44,
"ringRate": {
"-1": 0,
"0": 0,
"1": 0,
"10": 0,
"2": 0,
"3": 0,
"4": 0,
"5": 0,
"6": 0,
"7": 0,
"8": 0,
"9": 0
},
"weekArrows": []
}
+165
View File
@@ -0,0 +1,165 @@
# 📝 记分本(Point BookFlutter 纯本地版 PRD
## 文档信息
- **文档状态**:已完成(重构版)
- **适用端**iOS / Android 客户端(Flutter 纯本地架构)
- **变现模式**:免费试用 + iOS 苹果内购(IAP 买断/解锁)
- **关联模块**:本地训练统计 / 个人资料管理 / 记分管理 / 苹果内购
---
## 1. 页面概述
### 1.1 页面定位与目标
- **定位**:记分本业务线的 **总入口与本地数据总览页**
- **目标**
1. **零门槛使用**:无任何登录流程,首次打开即可直接查看本地统计或发起记分。
2. **正向数据反馈**:直观展示本地累计射箭数据、日均消耗、环值命中分布与落点热力图。
3. **内购合规转化**:提供免费体验额度,在用户第 N 次点击【开始记分】时,平滑唤起苹果内购付费解锁弹框。
### 1.2 权限与架构变更
- **数据架构****纯本地架构(Local-First)**,无后端服务器、无网络 API,所有训练数据与配置均存储于手机本地数据库(Isar/Hive)。
- **用户体系**:**完全免登录**,移除所有手机号获取、微信登录及协议勾选框。
- **排行榜**:**彻底移除**周榜、点赞及任何他人对比功能。
---
## 2. 页面流转与初始化逻辑
```mermaid
graph TD
A[用户打开 App / 进入首页] --> B[加载本地数据库 Isar]
B --> C[读取/初始化本地 UserProfile]
B --> D[读取/初始化本地 AppConfig]
B --> E[读取本地历史 PointRecord 列表]
C --> F[渲染:顶部个人资料卡片]
D --> G[渲染:用户权益/免费剩余次数]
E --> H[渲染:本地统计指标、落点热力图、环值分布图]
```
---
## 3. 核心功能与交互说明
### 3.1 本地个人资料管理(头像与昵称修改)
- **默认生成**:App 首次启动时,本地自动创建默认 Profile:
- **默认昵称**`弓箭手_XXXX`(四位随机数字)。
- **默认头像**:预设本地 Avatar Asset。
- **编辑交互**
- **入口**:点击首页顶部的个人资料卡片或【编辑】图标。
- **修改昵称**:弹出本地输入框,限制 1~12 个字符。
- **修改头像**:支持从手机本地相册选择图片或调起相机拍摄;选中后将图片异步拷贝至 App 本地沙盒目录 `ApplicationDocumentsDirectory/avatar.png`,数据库中仅保存文件路径。
---
### 3.2 个人训练统计卡片(纯本地计算)
- **关键指标**:今日射箭数、今日消耗、运动强度、训练天数、累计射箭数、平均环数。
- **计算规则**
- **无数据时**:指标统一展示为 `-`
- **今日消耗**$\text{今日箭数} \times 1.6$。
- **运动强度**$\frac{\text{今日箭数} \times 5}{60}$(上限封顶为 10)。
- $> 6$:标记为 **重度**
- $4 \sim 6$:标记为 **中度**
- $< 4$:标记为 **轻度**
---
### 3.3 核心业务入口与苹果内购(IAP)拦截
#### 🔹 入口一:【计分记录】
- **交互**:点击直接进入 `本地计分记录列表页`(支持按弓型/距离/靶纸筛选及本地记录滑动删除)。
#### 🔹 入口二:【开始记分】(含免费试用与内购拦截)
- **参数配置**
- `free_trial_limit`: 可配置免费试用次数(默认 `2` 次)。
- `used_trial_count`: 已使用免费试用次数。
- `is_vip_unlocked`: 是否已购买内购解锁(`true` / `false`)。
- **拦截判定流程**
```mermaid
graph TD
Start[点击“开始记分”] --> VIPCheck{is_vip_unlocked == true ?}
VIPCheck -- 是 --> DraftCheck
VIPCheck -- 否 --> TrialCheck{used_trial_count < free_trial_limit ?}
TrialCheck -- 是 (试用额度内) --> DraftCheck
TrialCheck -- 否 (试用额度已满) --> Paywall[阻断进入,弹出收费解锁弹框]
DraftCheck{本地是否存在未完成草稿?} -- 无草稿 --> CreatePage[进入参数选择/新建记分页]
DraftCheck -- 有草稿 --> DraftConfirmDialog[弹出草稿二选一确认框]
DraftConfirmDialog -- 继续编辑 --> EditPage[进入记分编辑页加载草稿]
DraftConfirmDialog -- 重新计分 --> ClearDraft[清空本地草稿] --> CreatePage
```
> **计数更新时机**:当用户在试用期内完成一次完整记分并点击**【保存记分】**成功写入本地数据库后,系统自动执行 `used_trial_count + 1`。
#### 💳 内购解锁弹窗与结果流转
- **弹窗展示**:包含内购商品名称、价格、买断权益说明、`[立即解锁]` 按钮、`[恢复购买]` 按钮及 `[取消/暂不解锁]` 按钮。
- **支付流转规则**
| 用户操作 / 支付状态 | 系统响应 | 页面流转 |
| ------------------------ | ------------------------------------------ | --------------------------- |
| **点击 [立即解锁]** | 调起 Apple App Store 原生支付组件。 | 界面显示原生 Loading 遮罩。 |
| **支付成功 (Purchased)** | 1. 本地数据库更新 `is_vip_unlocked = true` | |
2. 弹出 Toast 提示:“解锁成功!已获得无限记分权益” | 1. 关闭内购弹窗;
3. **返回【记分本首页】**
4. 再次点击【开始记分】自动解锁放行。 |
| **取消支付 (Canceled)** | 1. 弹出 Toast 提示:“支付已取消” | 1. 关闭内购弹窗;
5. **停留在【记分本首页】**。 |
| **支付失败 (Error)** | 1. 弹出 Toast 提示:“支付失败:[错误信息]” | 1. 关闭 Loading
6. **停留在【记分本首页】**。 |
| **点击 [恢复购买]** | 调起 `InAppPurchase.instance.restorePurchases()` | 查到历史购买凭证后更新 `is_vip_unlocked = true` 并提示“权益已恢复”,关闭弹窗并**返回【记分本首页】**。 |
---
### 3.4 本地图像分析模块(热力图与环值分布)
#### 🎯 落点热力图
- **展示条件**:本地存在历史训练数据时展示。
- **生成方式**:读取本地 `PointRecord` 中的 `hitPoints` 坐标数据,使用 Flutter Canvas 在本地预设靶纸底图上直接绘制热力点覆盖层。
#### 📊 个人环值分布图
- **展示条件**:本地存在历史记录时展示。
- **展示形式**:按 `M / X / 1~10 环` 统计本地所有箭数的占比,以柱状图形式呈现。
---
## 4. 边界异常处理与业务细节(QA 关注)
| 场景 | 预期处理机制 | 备注 / 风险点 |
| --------------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- |
| **首次安装 / 无数据** | 统计数据指标展示 `-`,热力图展示纯靶纸背景图,免费次数显示 `0/2`。 | 避免因为数据为空导致本地 Canvas 渲染报错。 |
| **本地草稿覆盖** | 点击“重新计分”会强制删除本地 SQLite/Isar 中的草稿记录,并覆盖写入。 | 需有二次弹窗明确确认,防止误删未保存数据。 |
| **重装 App / 换手机** | 纯本地 App 不支持跨设备数据同步;用户可通过内购弹窗的 **[恢复购买]** 恢复付费权益。 | 需在 App 内“关于我们”或设置页明确告知“数据存储于本地,卸载 App 会清空训练记录”。 |
| **网络离线内购** | 发起购买需要网络连接,若离线调起内购,提示:“无法连接到 App Store,请检查网络设置”。 | - |
---
---
## 5. 关联页面流转汇总
- 📄 **记分本首页** (`PointBookHomeScreen`):本地数据总览、免费次数提示、个人资料编辑入口、内购弹窗触发。
- 📄 **计分记录列表** (`PointBookListPage`):读取本地 Isar 历史列表、草稿展示与记录删除。
- 📄 **新建参数配置** (`PointBookCreatePage`):选择弓型、距离、靶纸类型及组数。
- 📄 **记分编辑/打靶页** (`PointBookEditPage`):本地实时记录落点与环数,保存时写入本地数据库并扣减试用次数。
+34
View File
@@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3
+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
</dict>
</plist>
+2
View File
@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"
+2
View File
@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"
+43
View File
@@ -0,0 +1,43 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '13.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end
+35
View File
@@ -0,0 +1,35 @@
PODS:
- Flutter (1.0.0)
- image_picker_ios (0.0.1):
- Flutter
- in_app_purchase_storekit (0.0.1):
- Flutter
- FlutterMacOS
- isar_flutter_libs (1.0.0):
- Flutter
DEPENDENCIES:
- Flutter (from `Flutter`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`)
- isar_flutter_libs (from `.symlinks/plugins/isar_flutter_libs/ios`)
EXTERNAL SOURCES:
Flutter:
:path: Flutter
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
in_app_purchase_storekit:
:path: ".symlinks/plugins/in_app_purchase_storekit/darwin"
isar_flutter_libs:
:path: ".symlinks/plugins/isar_flutter_libs/ios"
SPEC CHECKSUMS:
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
in_app_purchase_storekit: 22cca7d08eebca9babdf4d07d0baccb73325d3c8
isar_flutter_libs: 9fc2cfb928c539e1b76c481ba5d143d556d94920
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
COCOAPODS: 1.16.2
+746
View File
@@ -0,0 +1,746 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
27A77D74E4505B5D2AA63D0F /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 561C8CEE01920EEC7A945F80 /* Pods_RunnerTests.framework */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
8F0B832FFA652893A2418D6F /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E868FF8080DD0482F6C33647 /* Pods_Runner.framework */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
048D9C63DCC6330C4375E801 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
41517F8DE1244167EA1C3EF0 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
54790F6136DFBB3350A35FAA /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
561C8CEE01920EEC7A945F80 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
8173FD6563D966BAFA3AA699 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
9FE17248895AEA422B438BFD /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
DCD6C7F1914EC7BEB639DB0B /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
E868FF8080DD0482F6C33647 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
927EAE761096EB2B5FA9B76C /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
27A77D74E4505B5D2AA63D0F /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
8F0B832FFA652893A2418D6F /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
156C087B45530EC102A0D4A8 /* Pods */ = {
isa = PBXGroup;
children = (
41517F8DE1244167EA1C3EF0 /* Pods-Runner.debug.xcconfig */,
54790F6136DFBB3350A35FAA /* Pods-Runner.release.xcconfig */,
8173FD6563D966BAFA3AA699 /* Pods-Runner.profile.xcconfig */,
DCD6C7F1914EC7BEB639DB0B /* Pods-RunnerTests.debug.xcconfig */,
048D9C63DCC6330C4375E801 /* Pods-RunnerTests.release.xcconfig */,
9FE17248895AEA422B438BFD /* Pods-RunnerTests.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
};
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
156C087B45530EC102A0D4A8 /* Pods */,
B36C47CC576BE63C4192FAAB /* Frameworks */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
B36C47CC576BE63C4192FAAB /* Frameworks */ = {
isa = PBXGroup;
children = (
E868FF8080DD0482F6C33647 /* Pods_Runner.framework */,
561C8CEE01920EEC7A945F80 /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
5ED369F7938D6E55445F98FE /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
927EAE761096EB2B5FA9B76C /* Frameworks */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
7AE3623C7230451709ED948F /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
6E44F253809A70D824E46E32 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
5ED369F7938D6E55445F98FE /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
6E44F253809A70D824E46E32 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
7AE3623C7230451709ED948F /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = MT26BPCKF6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.shelingxingqiu.arcx;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "DevProfile-arc";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = DCD6C7F1914EC7BEB639DB0B /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.shelingxingqiu.arcx.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 048D9C63DCC6330C4375E801 /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.shelingxingqiu.arcx.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9FE17248895AEA422B438BFD /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.shelingxingqiu.arcx.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = MT26BPCKF6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.shelingxingqiu.arcx;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "DevProfile-arc";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = MT26BPCKF6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.shelingxingqiu.arcx;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "DevProfile-arc";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -2,7 +2,7 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>NSUserTrackingUsageDescription</key> <key>IDEDidComputeMac32BitWarning</key>
<string>We use tracking to analyze in-app clicks and usage to continuously improve the product experience.</string> <true/>
</dict> </dict>
</plist> </plist>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
+16
View File
@@ -0,0 +1,16 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
}
@@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

@@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>
+26
View File
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>
+74
View File
@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Arcx</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>arcx</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>用于拍摄并设置个人头像</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>用于从相册选择并设置个人头像</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
+1
View File
@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"
+6
View File
@@ -0,0 +1,6 @@
import Flutter
import UIKit
class SceneDelegate: FlutterSceneDelegate {
}
+12
View File
@@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}
+18
View File
@@ -0,0 +1,18 @@
import 'package:flutter/material.dart';
import '../features/home/presentation/point_book_home_screen.dart';
import 'app_theme.dart';
class ArcxApp extends StatelessWidget {
const ArcxApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '记分本',
debugShowCheckedModeBanner: false,
theme: AppTheme.light(),
home: const PointBookHomeScreen(),
);
}
}
+77
View File
@@ -0,0 +1,77 @@
import 'package:flutter/material.dart';
class AppTheme {
const AppTheme._();
static const Color primary = Color(0xFF1F7A4D);
static const Color primaryDark = Color(0xFF13533A);
static const Color accent = Color(0xFFE8A33D);
static const Color scoreRing = Color(0xFFE0B350);
static const Color hitPoint = Color(0xFFE5484D);
static const Color surfaceMuted = Color(0xFFF4F6F5);
static const Color textSecondary = Color(0xFF6B7280);
static ThemeData light() {
final base = ThemeData.light(useMaterial3: true);
return base.copyWith(
colorScheme: const ColorScheme.light(
primary: primary,
secondary: accent,
surface: Colors.white,
onPrimary: Colors.white,
onSecondary: Colors.black,
),
scaffoldBackgroundColor: surfaceMuted,
appBarTheme: const AppBarTheme(
backgroundColor: primary,
foregroundColor: Colors.white,
elevation: 0,
centerTitle: true,
),
cardTheme: CardThemeData(
color: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
margin: EdgeInsets.zero,
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: primary,
foregroundColor: Colors.white,
minimumSize: const Size(64, 48),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
backgroundColor: primary,
foregroundColor: Colors.white,
minimumSize: const Size(64, 48),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: primary,
minimumSize: const Size(64, 48),
side: const BorderSide(color: primary),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
),
);
}
}
+11
View File
@@ -0,0 +1,11 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'app/app.dart';
import 'data/local/app_database.dart';
Future<void> bootstrap() async {
WidgetsFlutterBinding.ensureInitialized();
await AppDatabase.instance.open();
runApp(const ProviderScope(child: ArcxApp()));
}
+15
View File
@@ -0,0 +1,15 @@
class AppDefaults {
const AppDefaults._();
static const int singletonId = 1;
static const int defaultFreeTrialLimit = 10;
static const int nicknameRandomMin = 1000;
static const int nicknameRandomMax = 9999;
static const String defaultAvatarAssetPath =
'assets/images/avatar_default.png';
static const String localDatabaseName = 'arcx_local_first_db';
static const String avatarFileName = 'avatar.png';
}
+15
View File
@@ -0,0 +1,15 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:isar/isar.dart';
import '../../data/local/app_database.dart';
/// Exposes the opened Isar instance. Throws if [AppDatabase.open] has not run.
final isarProvider = FutureProvider<Isar>((ref) => AppDatabase.instance.open());
/// Apple IAP product identifier for the one-time unlock.
/// Configure the matching product in App Store Connect when publishing.
const String kUnlockProductId = 'com.shelingxingqiu.arcx.unlock';
/// Whether the build is a debug build (used to surface a dev-only unlock).
final bool kIsDebugBuild = kDebugMode;
+74
View File
@@ -0,0 +1,74 @@
import 'package:isar/isar.dart';
import 'package:path_provider/path_provider.dart';
import '../../core/constants/app_defaults.dart';
import 'models/models.dart';
class AppDatabase {
AppDatabase._();
static final AppDatabase instance = AppDatabase._();
Isar? _isar;
Future<Isar> open() async {
final existing = _isar;
if (existing != null && existing.isOpen) {
return existing;
}
final directory = await getApplicationDocumentsDirectory();
final isar = await Isar.open(
<CollectionSchema>[
UserProfileSchema,
AppConfigSchema,
PointRecordSchema,
],
name: AppDefaults.localDatabaseName,
directory: directory.path,
inspector: false,
);
await _ensureBootstrapData(isar);
_isar = isar;
return isar;
}
Future<void> close() async {
final database = _isar;
if (database == null || !database.isOpen) {
return;
}
await database.close();
_isar = null;
}
Future<void> resetSingletonData() async {
final isar = await open();
await isar.writeTxn(() async {
await isar.userProfiles.put(UserProfile.createDefault());
await isar.appConfigs.put(AppConfig.createDefault());
});
}
Future<void> _ensureBootstrapData(Isar isar) async {
final hasProfile =
await isar.userProfiles.get(AppDefaults.singletonId) != null;
final hasConfig = await isar.appConfigs.get(AppDefaults.singletonId) != null;
if (hasProfile && hasConfig) {
return;
}
await isar.writeTxn(() async {
if (!hasProfile) {
await isar.userProfiles.put(UserProfile.createDefault());
}
if (!hasConfig) {
await isar.appConfigs.put(AppConfig.createDefault());
}
});
}
}
+76
View File
@@ -0,0 +1,76 @@
import 'package:isar/isar.dart';
import '../../../core/constants/app_defaults.dart';
part 'app_config.g.dart';
enum EntitlementSource {
none,
purchase,
restore,
}
@collection
class AppConfig {
AppConfig();
Id id = AppDefaults.singletonId;
@Index(unique: true, replace: true)
late String configKey;
@Index(type: IndexType.value)
late DateTime updatedAt;
late DateTime createdAt;
int freeTrialLimit = AppDefaults.defaultFreeTrialLimit;
int usedTrialCount = 0;
bool isVipUnlocked = false;
DateTime? vipUnlockedAt;
String? lastIapErrorMessage;
@enumerated
EntitlementSource entitlementSource = EntitlementSource.none;
factory AppConfig.createDefault({DateTime? now}) {
final current = now ?? DateTime.now();
return AppConfig()
..configKey = 'local_app_config'
..createdAt = current
..updatedAt = current
..freeTrialLimit = AppDefaults.defaultFreeTrialLimit
..usedTrialCount = 0
..isVipUnlocked = false
..entitlementSource = EntitlementSource.none;
}
int get remainingTrialCount {
final remaining = freeTrialLimit - usedTrialCount;
return remaining > 0 ? remaining : 0;
}
bool get canStartScoring => isVipUnlocked || usedTrialCount < freeTrialLimit;
AppConfig copyWith({
int? freeTrialLimit,
int? usedTrialCount,
bool? isVipUnlocked,
DateTime? vipUnlockedAt,
String? lastIapErrorMessage,
EntitlementSource? entitlementSource,
DateTime? updatedAt,
}) {
return AppConfig()
..id = id
..configKey = configKey
..createdAt = createdAt
..updatedAt = updatedAt ?? DateTime.now()
..freeTrialLimit = freeTrialLimit ?? this.freeTrialLimit
..usedTrialCount = usedTrialCount ?? this.usedTrialCount
..isVipUnlocked = isVipUnlocked ?? this.isVipUnlocked
..vipUnlockedAt = vipUnlockedAt ?? this.vipUnlockedAt
..lastIapErrorMessage = lastIapErrorMessage ?? this.lastIapErrorMessage
..entitlementSource = entitlementSource ?? this.entitlementSource;
}
}
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
export 'app_config.dart';
export 'point_record.dart';
export 'user_profile.dart';
+151
View File
@@ -0,0 +1,151 @@
import 'package:isar/isar.dart';
part 'point_record.g.dart';
enum PointRecordStatus {
draft,
completed,
}
enum BowType {
recurve,
compound,
barebow,
traditional,
longbow,
other,
}
enum TargetFaceType {
cm40,
cm60,
cm80,
cm122,
vegas3Spot,
vertical3Spot,
custom,
}
@embedded
class HitPoint {
HitPoint({
this.arrowIndex = 0,
this.endIndex = 0,
this.normalizedX = 0,
this.normalizedY = 0,
this.score,
this.isMiss = false,
});
int arrowIndex;
int endIndex;
/// Range: -1.0 ~ 1.0. The painter can map the normalized coordinate
/// directly onto different target canvases.
double normalizedX;
double normalizedY;
int? score;
bool isMiss;
}
@collection
class PointRecord {
PointRecord();
Id id = Isar.autoIncrement;
@Index(type: IndexType.value)
late DateTime updatedAt;
@Index(type: IndexType.value)
late DateTime createdAt;
@Index(type: IndexType.value)
DateTime? completedAt;
@enumerated
PointRecordStatus status = PointRecordStatus.draft;
@enumerated
BowType bowType = BowType.recurve;
@enumerated
TargetFaceType targetFaceType = TargetFaceType.cm40;
late String title;
int distanceMeters = 18;
int endCount = 6;
int arrowsPerEnd = 6;
String? note;
List<int> scores = <int>[];
List<HitPoint> hitPoints = <HitPoint>[];
bool get isDraft => status == PointRecordStatus.draft;
int get totalArrows => scores.length;
int get totalScore => scores.fold<int>(0, (sum, item) => sum + item);
double? get averageScore =>
scores.isEmpty ? null : totalScore / scores.length;
factory PointRecord.createDraft({
required BowType bowType,
required TargetFaceType targetFaceType,
required int distanceMeters,
required int endCount,
required int arrowsPerEnd,
String? title,
DateTime? now,
}) {
final current = now ?? DateTime.now();
return PointRecord()
..title = title ?? '未完成记分'
..status = PointRecordStatus.draft
..bowType = bowType
..targetFaceType = targetFaceType
..distanceMeters = distanceMeters
..endCount = endCount
..arrowsPerEnd = arrowsPerEnd
..createdAt = current
..updatedAt = current;
}
PointRecord markCompleted({DateTime? completedAt}) {
final current = completedAt ?? DateTime.now();
return PointRecord()
..id = id
..title = title
..status = PointRecordStatus.completed
..bowType = bowType
..targetFaceType = targetFaceType
..distanceMeters = distanceMeters
..endCount = endCount
..arrowsPerEnd = arrowsPerEnd
..note = note
..scores = List<int>.from(scores)
..hitPoints = List<HitPoint>.from(hitPoints)
..createdAt = createdAt
..updatedAt = current
..completedAt = current;
}
/// Returns a draft copy with updated scores/hit points (used while editing).
PointRecord copyWithScores(List<int> scores, List<HitPoint> hitPoints) {
return PointRecord()
..id = id
..title = title
..status = status
..bowType = bowType
..targetFaceType = targetFaceType
..distanceMeters = distanceMeters
..endCount = endCount
..arrowsPerEnd = arrowsPerEnd
..note = note
..scores = List<int>.from(scores)
..hitPoints = List<HitPoint>.from(hitPoints)
..createdAt = createdAt
..updatedAt = DateTime.now()
..completedAt = completedAt;
}
}
File diff suppressed because it is too large Load Diff
+79
View File
@@ -0,0 +1,79 @@
import 'dart:math';
import 'package:isar/isar.dart';
import '../../../core/constants/app_defaults.dart';
part 'user_profile.g.dart';
enum AvatarSource {
asset,
localFile,
}
@collection
class UserProfile {
UserProfile();
Id id = AppDefaults.singletonId;
@Index(unique: true, replace: true)
late String profileKey;
@Index(type: IndexType.value)
late DateTime updatedAt;
late DateTime createdAt;
late String nickname;
String? avatarLocalPath;
late String avatarAssetPath;
@enumerated
AvatarSource avatarSource = AvatarSource.asset;
factory UserProfile.createDefault({DateTime? now, Random? random}) {
final current = now ?? DateTime.now();
final generator = random ?? Random();
final suffix = AppDefaults.nicknameRandomMin +
generator.nextInt(
AppDefaults.nicknameRandomMax - AppDefaults.nicknameRandomMin + 1,
);
return UserProfile()
..profileKey = 'local_user_profile'
..createdAt = current
..updatedAt = current
..nickname = '弓箭手_$suffix'
..avatarAssetPath = AppDefaults.defaultAvatarAssetPath
..avatarSource = AvatarSource.asset;
}
String get displayAvatarPath =>
avatarSource == AvatarSource.localFile &&
avatarLocalPath != null &&
avatarLocalPath!.trim().isNotEmpty
? avatarLocalPath!
: avatarAssetPath;
UserProfile copyWith({
String? nickname,
Object? avatarLocalPath = _unset,
String? avatarAssetPath,
AvatarSource? avatarSource,
DateTime? updatedAt,
}) {
return UserProfile()
..id = id
..profileKey = profileKey
..createdAt = createdAt
..updatedAt = updatedAt ?? DateTime.now()
..nickname = nickname ?? this.nickname
..avatarLocalPath = identical(avatarLocalPath, _unset)
? this.avatarLocalPath
: avatarLocalPath as String?
..avatarAssetPath = avatarAssetPath ?? this.avatarAssetPath
..avatarSource = avatarSource ?? this.avatarSource;
}
}
const Object _unset = Object();
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,61 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/constants/app_defaults.dart';
import '../../../core/providers/app_providers.dart';
import '../../../data/local/models/models.dart';
final appConfigProvider =
AsyncNotifierProvider<AppConfigNotifier, AppConfig>(AppConfigNotifier.new);
class AppConfigNotifier extends AsyncNotifier<AppConfig> {
@override
Future<AppConfig> build() async {
final isar = await ref.watch(isarProvider.future);
final config = await isar.appConfigs.get(AppDefaults.singletonId);
if (config != null) return config;
final fresh = AppConfig.createDefault();
await isar.writeTxn(() => isar.appConfigs.put(fresh));
return fresh;
}
Future<AppConfig> _persist(AppConfig config) async {
final isar = await ref.read(isarProvider.future);
await isar.writeTxn(() => isar.appConfigs.put(config));
state = AsyncData(config);
return config;
}
/// Called after a scoring session is saved successfully.
Future<void> incrementTrialCount() async {
final current = state.value;
if (current == null || current.isVipUnlocked) return;
final next = current.copyWith(
usedTrialCount: current.usedTrialCount + 1,
);
await _persist(next);
}
/// Marks the app as unlocked via IAP purchase or restore.
Future<void> markUnlocked({required bool fromRestore}) async {
final current = state.value;
if (current == null) return;
final next = current.copyWith(
isVipUnlocked: true,
vipUnlockedAt: DateTime.now(),
entitlementSource:
fromRestore ? EntitlementSource.restore : EntitlementSource.purchase,
);
await _persist(next);
}
Future<void> setLastError(String? message) async {
final current = state.value;
if (current == null) return;
await _persist(current.copyWith(lastIapErrorMessage: message));
}
/// Debug-only: instantly unlock without going through App Store.
Future<void> debugUnlock() async {
await markUnlocked(fromRestore: false);
}
}
@@ -0,0 +1,221 @@
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('开始记分'),
),
),
],
);
}
}
@@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import 'package:arcx/app/app_theme.dart';
import 'package:arcx/features/scoring/presentation/widgets/hit_point_painter.dart';
import 'package:arcx/features/stats/application/stats.dart';
class HeatmapCard extends StatelessWidget {
const HeatmapCard({super.key, required this.stats});
final StatsSummary stats;
@override
Widget build(BuildContext context) {
final hasHits = stats.hitPoints.isNotEmpty;
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'落点热力图',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
const SizedBox(height: 12),
Center(
child: AspectRatio(
aspectRatio: 1,
child: Stack(
fit: StackFit.expand,
alignment: Alignment.center,
children: [
Image.asset(
'assets/images/target_face.png',
fit: BoxFit.fill,
),
CustomPaint(
painter: HitPointPainter(
hitPoints: stats.hitPoints,
hitColor: AppTheme.hitPoint,
),
),
],
),
),
),
if (!hasHits)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
'暂无落点数据,完成一次记分后展示',
style: TextStyle(fontSize: 13, color: AppTheme.textSecondary),
),
),
],
),
),
);
}
}
@@ -0,0 +1,84 @@
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/profile/application/profile_controller.dart';
import 'package:arcx/features/profile/presentation/avatar_image.dart';
class ProfileCard extends ConsumerWidget {
const ProfileCard({super.key, required this.onEdit});
final VoidCallback onEdit;
@override
Widget build(BuildContext context, WidgetRef ref) {
final profileAsync = ref.watch(profileProvider);
return profileAsync.when(
loading: () => const _CardShell(child: SizedBox(height: 64)),
error: (e, _) => _CardShell(child: Text('加载失败:$e')),
data: (profile) => _CardShell(
child: Row(
children: [
_Avatar(profile: profile),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
profile.nickname,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
'本地用户',
style: TextStyle(
fontSize: 13,
color: AppTheme.textSecondary,
),
),
],
),
),
IconButton(
icon: const Icon(Icons.edit_outlined),
color: AppTheme.primary,
onPressed: onEdit,
),
],
),
),
);
}
}
class _Avatar extends StatelessWidget {
const _Avatar({required this.profile});
final UserProfile profile;
@override
Widget build(BuildContext context) {
return CircleAvatar(
radius: 30,
backgroundColor: AppTheme.surfaceMuted,
backgroundImage: resolveAvatarImage(profile),
);
}
}
class _CardShell extends StatelessWidget {
const _CardShell({required this.child});
final Widget child;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(padding: const EdgeInsets.all(16), child: child),
);
}
}
@@ -0,0 +1,118 @@
import 'package:flutter/material.dart';
import 'package:arcx/app/app_theme.dart';
import 'package:arcx/features/stats/application/stats.dart';
class ScoreDistributionCard extends StatelessWidget {
const ScoreDistributionCard({super.key, required this.stats});
final StatsSummary stats;
@override
Widget build(BuildContext context) {
final buckets = stats.scoreDistribution;
final hasData = buckets.isNotEmpty;
final maxCount = hasData
? buckets.map((b) => b.count).reduce((a, b) => a > b ? a : b)
: 1;
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'环值分布',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
const SizedBox(height: 12),
if (!hasData)
Padding(
padding: const EdgeInsets.symmetric(vertical: 24),
child: Center(
child: Text(
'暂无数据',
style: TextStyle(
fontSize: 13,
color: AppTheme.textSecondary,
),
),
),
)
else
SizedBox(
height: 140,
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
for (var i = 0; i < buckets.length; i++) ...[
Expanded(
child: _Bar(
label: buckets[i].label,
count: buckets[i].count,
ratio: buckets[i].count / maxCount,
),
),
if (i < buckets.length - 1) const SizedBox(width: 6),
],
],
),
),
],
),
),
);
}
}
class _Bar extends StatelessWidget {
const _Bar({
required this.label,
required this.count,
required this.ratio,
});
final String label;
final int count;
final double ratio;
@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,
),
),
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,
),
),
],
);
},
);
}
}

Some files were not shown because too many files have changed in this diff Show More