chore: 初始化脚手架 — sunny_mochi 企业级 Flutter 模板

Core 基础设施:Flavor 三包体系、Drift+SQLCipher 加密数据库
7 拦截器 Dio 网络栈、CrashReporter 崩溃日志、Sealed Failure 错误体系
5 色板主题系统、Auth 认证骨架(Clean Architecture)、Dev Panel、Mock Adapter

通过验证:flutter analyze(0 errors)、flutter test(全绿)、staging APK 74.8MB
This commit is contained in:
SkyJourney
2026-05-14 12:51:05 +08:00
commit 61017f1c39
204 changed files with 15386 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
# 统一行尾为 LF(跨平台 CI 安全)
* text=auto eol=lf
# 二进制文件不做行尾处理
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ttf binary
*.otf binary
*.ico binary
*.jar binary
*.zip binary
*.apk binary
*.aab binary
# Android keystore(万一误入,保持二进制)
*.jks binary
*.keystore binary
+105
View File
@@ -0,0 +1,105 @@
# ═══════════════════════════════════════════════════════════════════
# Flutter / Dart
# ═══════════════════════════════════════════════════════════════════
# Dart tooling
.dart_tool/
.pub-cache/
.pub/
**/doc/api/
# Flutter build cache
/build/
/coverage/
**/ios/Flutter/.last_build_id
# Flutter plugin metadata (auto-generated on `flutter pub get`)
.flutter-plugins
.flutter-plugins-dependencies
# Symbolication / obfuscation maps
app.*.symbols
app.*.map.json
# ═══════════════════════════════════════════════════════════════════
# Android
# ═══════════════════════════════════════════════════════════════════
# Gradle
/android/.gradle/
/android/local.properties # 本机 SDK 路径,不应提交
/android/captures/
# Build outputs (Flavor 构建产物)
/android/app/debug/
/android/app/profile/
/android/app/release/
# Android 签名密钥(严禁提交)
*.jks
*.keystore
key.properties
/android/app/google-services.json # 按需:若使用 Firebase 则配置 CI secret
# ═══════════════════════════════════════════════════════════════════
# iOS
# ═══════════════════════════════════════════════════════════════════
# CocoaPodspod install 后自动生成)
/ios/Pods/
/ios/.symlinks/
# Xcode 自动生成
/ios/Flutter/Generated.xcconfig
/ios/Flutter/flutter_export_environment.sh
/ios/Runner/GeneratedPluginRegistrant.swift
/ios/Runner/GeneratedPluginRegistrant.h
/ios/Runner/GeneratedPluginRegistrant.m
# ═══════════════════════════════════════════════════════════════════
# dart-defines(敏感配置)
# ═══════════════════════════════════════════════════════════════════
# prod.json 包含真实 Sentry DSN / RSA 公钥,由 CI secret 注入,禁止提交
dart-defines/prod.json
# dev.json / staging.json 含占位符,可提交(无真实密钥)
# dart-defines/dev.json ← 不忽略
# dart-defines/staging.json ← 不忽略
# ═══════════════════════════════════════════════════════════════════
# IDE
# ═══════════════════════════════════════════════════════════════════
# IntelliJ / Android Studio
*.iml
*.ipr
*.iws
.idea/
# VS Code(可按团队习惯解注释)
# .vscode/
# ═══════════════════════════════════════════════════════════════════
# OS
# ═══════════════════════════════════════════════════════════════════
.DS_Store
Thumbs.db
Desktop.ini
*.swp
*.orig
.history/
# ═══════════════════════════════════════════════════════════════════
# Misc
# ═══════════════════════════════════════════════════════════════════
*.log
*.class
*.pyc
.atom/
.buildlog/
.svn/
.swiftpm/
migrate_working_dir/
+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: "00b0c91f06209d9e4a41f71b7a512d6eb3b9c694"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694
base_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694
- platform: android
create_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694
base_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694
- platform: ios
create_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694
base_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694
# 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'
+89
View File
@@ -0,0 +1,89 @@
# CLAUDE.md — sunny_mochi 脚手架项目规范
## 项目简介
`sunny_mochi` 是企业级 Flutter APP 脚手架模板,包含:
- Flavor 三包体系(dev/staging/prod
- 完整 Core 基础设施层(网络/存储/崩溃/主题/观测性)
- 认证骨架(Auth Feature
- 开发者工具面板(Dev Panel)
- 本地错误日志 + 上报
## 目录结构
```
lib/
├── main.dart # 6 步启动序列
├── app.dart # AppRoot → MaterialApp.router
├── i18n/ # Slang 翻译文件
├── core/
│ ├── config/ # Env / ApiConfig / ApiPaths
│ ├── router/ # GoRouter + TypedRoutes
│ ├── network/ # Dio 7 拦截器 + MockAdapter
│ ├── storage/ # Drift + SQLCipher + SecureStorage
│ ├── crash/ # CrashReporter 3 步生命周期
│ ├── error/ # sealed Failure 层级
│ ├── theme/ # 5 色板 + 深色模式
│ ├── observability/ # Talker + Sentry
│ ├── sync/ # Offline-First 同步框架
│ ├── crypto/ # RSA 加密工具
│ ├── extensions/ # BuildContext / String / num / DateTime
│ ├── utils/ # 验证器 / 密码强度 / SMS 倒计时
│ ├── data/ # FreshnessPolicy 缓存策略
│ └── widgets/ # 通用 UI 组件库
└── features/
├── auth/ # 认证 Feature(完整 Clean Architecture
├── dev_panel/ # Talker 调试面板(Internal 包)
└── error_report/ # 本地错误日志查看 + 上报
```
## 新项目初始化清单
1. **替换包名**`sunny_mochi` → 你的包名(android/app/build.gradle.kts + pubspec.yaml + import
2. **填入 API 路径**`lib/core/config/api_paths.dart` 中所有空字符串
3. **配置 Flavor**`android/app/build.gradle.kts` applicationId + 签名配置
4. **填入 Sentry DSN**`dart-defines/prod.json`
5. **填入 RSA 公钥**`dart-defines/dev.json``rsaPublicKey` 字段
6. **替换 i18n 字符串**`lib/i18n/strings_zh_CN.i18n.json` 按需扩展
7. **添加业务路由**`lib/core/router/routes.dart` 新增 TypedGoRoute
8. **实现 Tab 页面**:替换 HomeRoute / MineRoute 的 placeholder build
## 零容忍规则
| 规则 | 正确 | 禁止 |
|------|------|------|
| API 路径 | `ApiPaths.xxx` | 字面量字符串 |
| 错误处理 | `ExceptionMapper.fromUnknown()` | 散落的 `catch(e)` 直接显示 |
| Token 存储 | `SecureStorage`SoT | 普通 SharedPreferences |
| DB 加密 | SQLCipher(默认) | 无加密 sqflite |
| 日志 | `appTalker.xxx()` | `print()` / `debugPrint()` |
| 路由跳转 | `LoginRoute().go(context)` | `Navigator.push()` |
## 常用命令
```bash
# 代码生成
make gen
# i18n 生成
make gen-i18n
# 运行(dev 包)
make run-dev
# 构建 staging APK
make build-staging
# 静态分析
flutter analyze
# 测试
flutter test --coverage
```
## 关键技术决策
- **SQLCipher**:企业合规要求加密存储,部署后无法无损迁移到无加密
- **7 拦截器栈**Token 刷新互斥(Completer)+ 证书绑定是安全基础,不可简化
- **sealed Failure**:统一错误分类,UI / 日志 / Sentry 三层复用同一模型
- **schemaVersion=1**:脚手架全新 DB,不携带任何历史迁移负担
+50
View File
@@ -0,0 +1,50 @@
FLUTTER := flutter
.PHONY: help run-dev run-staging build-staging build-prod build-apk-dev clean gen gen-i18n
help:
@echo "Flutter Flavor 构建命令:"
@echo ""
@echo " make run-dev 开发版调试运行(热重载,连 dev 服务器)"
@echo " make run-staging 测试版 Release 运行(连 test 服务器)"
@echo " make build-staging 打测试版 APK(给测试团队安装)"
@echo " make build-prod 打正式版 AAB(上 Play Store / 内部分发)"
@echo " make build-apk-dev 打开发版 APK(快速真机验证)"
@echo " make clean 清除构建缓存"
@echo " make gen 代码生成(build_runner"
@echo " make gen-i18n 生成 i18n ARB → Dart"
@echo ""
@echo "注意:build-prod 需先复制 dart-defines/prod.json.template 为 dart-defines/prod.json 并填入真实密钥"
run-dev:
$(FLUTTER) run --flavor dev \
--dart-define-from-file=dart-defines/dev.json
run-staging:
$(FLUTTER) run --flavor staging --release \
--dart-define-from-file=dart-defines/staging.json
build-apk-dev:
$(FLUTTER) build apk --flavor dev --debug \
--dart-define-from-file=dart-defines/dev.json
build-staging:
$(FLUTTER) build apk --flavor staging --release \
--dart-define-from-file=dart-defines/staging.json
@echo "APK 路径:build/app/outputs/flutter-apk/app-staging-release.apk"
build-prod:
@test -f dart-defines/prod.json || \
(echo "ERROR: dart-defines/prod.json 不存在!请从 prod.json.template 复制并填入真实密钥。" && exit 1)
$(FLUTTER) build appbundle --flavor prod --release \
--dart-define-from-file=dart-defines/prod.json
@echo "AAB 路径:build/app/outputs/bundle/prodRelease/app-prod-release.aab"
clean:
$(FLUTTER) clean
gen:
dart run build_runner build --delete-conflicting-outputs
gen-i18n:
dart run slang
+17
View File
@@ -0,0 +1,17 @@
# sunny_mochi
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.
+14
View File
@@ -0,0 +1,14 @@
include: package:very_good_analysis/analysis_options.yaml
analyzer:
exclude:
- '**/*.g.dart'
- '**/*.freezed.dart'
- '**/*.gen.dart'
- 'lib/i18n/**'
errors:
invalid_annotation_target: ignore
linter:
rules:
public_member_api_docs: false
+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
+116
View File
@@ -0,0 +1,116 @@
import java.util.Properties
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
// 签名配置(android/key.properties 存在时启用;本地开发可不创建,CI/CD 必须提供)
val keystorePropertiesFile = rootProject.file("key.properties")
val keystoreProperties = Properties().also { props ->
if (keystorePropertiesFile.exists()) {
props.load(keystorePropertiesFile.inputStream())
}
}
android {
namespace = "com.example.sunny_mochi"
// v1.2 锁定(详见 docs/flutter-architecture-design.md §十一.6):
// compileSdk = 36(编译期 API;插件生态已要求 36+)
// minSdk = 24Android 7OMA 安全推荐 + 主流国内 App 基线)
// targetSdk = 35Google Play 2026 强制要求;运行时声明目标)
compileSdk = 36
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
applicationId = "com.example.sunny_mochi"
minSdk = 24
targetSdk = 35
versionCode = flutter.versionCode
versionName = flutter.versionName
}
// ── Flavor 三包共存(dev / staging / prod)────────────────────────────
// 注意:Android Kotlin DSL 中 "test" 是保留字,测试环境 flavor 命名为 staging
flavorDimensions += "environment"
productFlavors {
create("dev") {
dimension = "environment"
applicationIdSuffix = ".dev"
versionNameSuffix = "-dev"
resValue("string", "app_name", "MyApp(开发版)")
}
create("staging") {
dimension = "environment"
applicationIdSuffix = ".staging"
versionNameSuffix = "-staging"
resValue("string", "app_name", "MyApp(测试版)")
}
create("prod") {
dimension = "environment"
// 正式包无后缀,applicationId 与 namespace 保持一致
resValue("string", "app_name", "MyApp")
}
}
// ──────────────────────────────────────────────────────────────────────
signingConfigs {
// release signingConfigkey.properties 存在时填充;否则 buildTypes.release 回退 debug
create("release") {
if (keystorePropertiesFile.exists()) {
keyAlias = keystoreProperties.getProperty("keyAlias")
keyPassword = keystoreProperties.getProperty("keyPassword")
storeFile = file(keystoreProperties.getProperty("storeFile"))
storePassword = keystoreProperties.getProperty("storePassword")
}
}
}
buildTypes {
debug {
isMinifyEnabled = false
isDebuggable = true
}
release {
// dev / staging release 包:shrink 资源但不混淆(方便调试)
// prod release 包:全开 R8 混淆(配合 proguard-rules.pro
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
// key.properties 存在时用 release 签名,否则回退 debug 签名(本地开发)
signingConfig = if (keystorePropertiesFile.exists()) {
signingConfigs.getByName("release")
} else {
signingConfigs.getByName("debug")
}
}
}
// sqlcipher_flutter_libs 提供加密版 libsqlite3.so
// tencent_cloud_chat_sdk 也打包了原版同名 .so,AGP 会报冲突并随机丢弃其中一份。
// pickFirsts 保证遇到重名时优先保留(sqlcipher 插件在 classpath 中排前面)。
packaging {
jniLibs {
pickFirsts += setOf("**/libsqlite3.so")
}
}
}
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="sunny_mochi"
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.example.sunny_mochi
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)
}
+2
View File
@@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
+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-8.14-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 "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")
View File
+22
View File
@@ -0,0 +1,22 @@
{
"code": "00000",
"msg": "success",
"data": {
"saTokenInfo": {
"tokenName": "satoken",
"tokenValue": "mock-access-token-dev-1234567890abcdef",
"loginId": "1001",
"loginType": "login"
},
"userInfo": {
"realName": "Mock User",
"avatar": "",
"phone": "13800138000",
"email": "mock@example.com",
"sex": 1,
"workNo": "EMP001",
"orgName": "Example Corp",
"deptName": "Engineering"
}
}
}
@@ -0,0 +1,8 @@
{
"code": "00000",
"msg": "success",
"data": {
"access_token": "mock-refreshed-access-token-abcdef",
"refresh_token": "mock-refreshed-refresh-token-fedcba"
}
}
@@ -0,0 +1,5 @@
{
"code": "00000",
"msg": "success",
"data": "123456"
}
@@ -0,0 +1,5 @@
{
"code": "00000",
"msg": "success",
"data": null
}
+7
View File
@@ -0,0 +1,7 @@
{
"code": "00000",
"msg": "success",
"data": {
"url": "https://example.com/mock-avatar.jpg"
}
}
+15
View File
@@ -0,0 +1,15 @@
{
"code": "00000",
"msg": "success",
"data": {
"id": "1001",
"realName": "Mock User",
"avatar": "",
"phone": "13800138000",
"email": "mock@example.com",
"sex": 1,
"workNo": "EMP001",
"orgName": "Example Corp",
"deptName": "Engineering"
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
View File
View File
View File
View File
+8
View File
@@ -0,0 +1,8 @@
{
"ENV": "dev",
"USE_MOCK": "false",
"INTERNAL_BUILD": "true",
"SENTRY_DSN": "",
"PINNED_FINGERPRINTS": "",
"RSA_PUBLIC_KEY": "REPLACE_WITH_RSA_PUBLIC_KEY"
}
+8
View File
@@ -0,0 +1,8 @@
{
"ENV": "release",
"USE_MOCK": "false",
"INTERNAL_BUILD": "false",
"SENTRY_DSN": "REPLACE_WITH_ACTUAL_SENTRY_DSN",
"PINNED_FINGERPRINTS": "REPLACE_WITH_ACTUAL_FINGERPRINTS_AA:BB:CC",
"RSA_PUBLIC_KEY": "REPLACE_WITH_RSA_PUBLIC_KEY"
}
+8
View File
@@ -0,0 +1,8 @@
{
"ENV": "test",
"USE_MOCK": "false",
"INTERNAL_BUILD": "true",
"SENTRY_DSN": "",
"PINNED_FINGERPRINTS": "",
"RSA_PUBLIC_KEY": "REPLACE_WITH_RSA_PUBLIC_KEY"
}
+5
View File
@@ -0,0 +1,5 @@
environment:
sdk: ">=3.7.0 <4.0.0"
flutter_test:
timeout: 60s
+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>
+1
View File
@@ -0,0 +1 @@
#include "Generated.xcconfig"
+1
View File
@@ -0,0 +1 @@
#include "Generated.xcconfig"
+4
View File
@@ -0,0 +1,4 @@
#include "../Generated.xcconfig"
APP_DISPLAY_NAME = MyApp(开发版)
PRODUCT_BUNDLE_IDENTIFIER = com.example.sunnyMochi.dev
FLUTTER_FLAVOR = dev
+4
View File
@@ -0,0 +1,4 @@
#include "../Generated.xcconfig"
APP_DISPLAY_NAME = MyApp
PRODUCT_BUNDLE_IDENTIFIER = com.example.sunnyMochi
FLUTTER_FLAVOR = prod
+4
View File
@@ -0,0 +1,4 @@
#include "../Generated.xcconfig"
APP_DISPLAY_NAME = MyApp(测试版)
PRODUCT_BUNDLE_IDENTIFIER = com.example.sunnyMochi.staging
FLUTTER_FLAVOR = staging
+50
View File
@@ -0,0 +1,50 @@
# Flutter Template — iOS Podfile
# 目标平台 iOS 15.0v1.2 锁定,详见 docs/flutter-architecture-design.md §十一.6
# Q1 答复 = 接受 iOS 15 → 保持本配置;如需保 iOS 13/14,调整 platform 行后重跑 pod install。
platform :ios, '15.0'
# CocoaPods 静态链接(与腾讯 IM 三件套兼容;P4 接入 IM 时再启用)
# 注意:use_frameworks! :linkage => :static 要求所有 Pod 支持静态库
# 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)
target.build_configurations.each do |config|
# 强制所有 Pod 的最低部署目标 ≥ iOS 15.0(与 Podfile platform 行对齐)
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
end
end
end
+620
View File
@@ -0,0 +1,620 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
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 */; };
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 */
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>"; };
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>"; };
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>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
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 */,
);
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>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
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 = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
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";
};
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;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.sunnyMochi;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
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.example.sunnyMochi.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;
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.example.sunnyMochi.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;
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.example.sunnyMochi.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;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.sunnyMochi;
PRODUCT_NAME = "$(TARGET_NAME)";
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;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.sunnyMochi;
PRODUCT_NAME = "$(TARGET_NAME)";
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>
@@ -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>
@@ -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>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.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>
+70
View File
@@ -0,0 +1,70 @@
<?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>Sunny Mochi</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>sunny_mochi</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>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.
}
}
+28
View File
@@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sunny_mochi/core/crash/crash_gate.dart';
import 'package:sunny_mochi/core/router/app_router.dart';
import 'package:sunny_mochi/core/theme/app_theme.dart';
import 'package:sunny_mochi/core/theme/theme_notifier.dart';
class App extends ConsumerWidget {
const App({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final router = ref.watch(appRouterProvider);
final themeMode = ref.watch(themeModeProvider);
final scheme = ref.watch(themeProvider);
return MaterialApp.router(
title: 'Sunny Mochi', // TODO: 替换为项目应用名
debugShowCheckedModeBanner: false,
theme: AppTheme.light(scheme: scheme),
darkTheme: AppTheme.dark(scheme: scheme),
themeMode: themeMode,
routerConfig: router,
// CrashGate 在 MaterialApp 内部:showDialog 需要 MaterialLocalizations
builder: (context, child) => CrashGate(child: child ?? const SizedBox()),
);
}
}
+43
View File
@@ -0,0 +1,43 @@
import 'package:sunny_mochi/core/config/env.dart';
/// API 网络配置 — **baseUrl 决策的单一权威**
///
/// **对应 iOS BasicModule/Configuration/NetworkConfig.swift2026-05 同步)**
///
/// | 环境 | iOS apiBaseURL | Flutter ApiConfig.baseUrl |
/// |------|---------------|--------------------------|
/// | dev | http://192.168.1.201:24801 | 同 |
/// | test | https://dev.yixiong-tech.com:8081 | 同 |
/// | release | https://bac.new.hamkke.top | 同 |
///
/// **优先级**
/// 1. CI/CD / 临时调试通过 `--dart-define=API_BASE_URL=...` 注入 → 优先
/// 2. 否则按 `Env.name`dev/test/release)返回 iOS 同源 URL
///
/// **不再引入 h5BaseURL** — H5 评估报告已全部 Flutter 原生化(详见
/// docs/h5-to-native-decision-2026-05-10.md),不再需要 in-app WebView。
///
/// **响应码请用 [ResponseCode]**lib/core/network/response_code.dart)。
/// **API 路径请用 [ApiPaths]**lib/core/config/api_paths.dart)—
/// 不允许 datasource 散落字面量。
abstract class ApiConfig {
/// API 网关 baseUrl — 与 iOS NetworkConfig.swift 同源
static String get baseUrl {
// 1. dart-define 覆盖优先(CI/CD / 临时切换私有环境)
if (Env.apiBaseUrlOverride.isNotEmpty) {
return Env.apiBaseUrlOverride;
}
// 2. 按 Env.name 返回 iOS 同源 URL
return switch (Env.name) {
'dev' => 'http://192.168.1.201:24801',
'test' => 'https://dev.yixiong-tech.com:8081',
'release' => 'https://bac.new.hamkke.top',
_ => 'http://192.168.1.201:24801', // 默认 dev(与 iOS Debug 包一致)
};
}
// 网络超时 — 对应共性需求说明 §移动端 7.网络异常处理(10s 超时建议)
static const Duration connectTimeout = Duration(seconds: 15);
static const Duration receiveTimeout = Duration(seconds: 30);
static const Duration sendTimeout = Duration(seconds: 30);
}
+72
View File
@@ -0,0 +1,72 @@
/// API 路径常量(按微服务子系统分组)。
///
/// **零容忍规则**:所有 datasource 必须引用本文件常量;不允许字面量散落。
///
/// TODO: 填入项目 API 路径
abstract class ApiPaths {
// ============== Auth ==============
// TODO: 填入项目 API 路径
static const String authLoginSms = '';
static const String authLoginPwd = '';
// TODO: 填入项目 API 路径
static const String authSmsSend = '';
// TODO: 填入项目 API 路径
static const String authLogout = '';
// TODO: 填入项目 API 路径
static const String authRefresh = '';
// ============== User / Mine ==============
// TODO: 填入项目 API 路径
static const String userProfile = '';
// TODO: 填入项目 API 路径
static const String userChangePassword = '';
// TODO: 填入项目 API 路径
static const String userIdCardVerify = '';
// TODO: 填入项目 API 路径
static const String userPhoneList = '';
// TODO: 填入项目 API 路径
static const String userPhoneAdd = '';
// TODO: 填入项目 API 路径
static const String userPhoneDelete = '';
// TODO: 填入项目 API 路径
static const String userPhoneVerifyCode = '';
// TODO: 填入项目 API 路径
static const String userPhoneSetDefault = '';
// TODO: 填入项目 API 路径
static const String userAvatarUpload = '';
// TODO: 填入项目 API 路径
static const String userAvatarSetUrl = '';
// TODO: 填入项目 API 路径
static const String userPasswordVerify = '';
// TODO: 填入项目 API 路径
static const String userPasswordReset = '';
// ============== Home ==============
// TODO: 填入项目 API 路径
static const String homeBanner = '';
// ============== File ==============
// TODO: 填入项目 API 路径
static const String fileUpload = '';
// ============== Error Report ==============
// TODO: 填入项目 API 路径
static const String errorReport = '';
// ============== Crash Report ==============
// TODO: 填入项目 API 路径
static const String crashReport = '';
}
+109
View File
@@ -0,0 +1,109 @@
import 'package:flutter/foundation.dart';
import 'package:sentry_flutter/sentry_flutter.dart' show SentryFlutter;
/// 全局编译期环境配置。所有值通过 --dart-define 注入,避免明文 secrets。
///
/// **环境名与 iOS NetworkConfig.swift 对齐**dev / test / release
///
/// 用法:
/// ```bash
/// # 开发环境 + mock(默认)
/// flutter run --dart-define=ENV=dev --dart-define=USE_MOCK=true
///
/// # 测试环境(连真服 dev.yixiong-tech.com:8081
/// flutter run --dart-define=ENV=test
///
/// # 正式环境(连 bac.new.hamkke.top
/// flutter build apk --release --dart-define=ENV=release \
/// --dart-define=SENTRY_DSN=https://...@sentry/1
///
/// # CI/CD 临时覆盖 API URL(不修改源码)
/// flutter run --dart-define=API_BASE_URL=https://my-private.example.com
/// ```
///
/// 详见:
/// - `docs/flutter-architecture-design.md` §九.1 / §十一.5
/// - `docs/real-environment-verification.md`(环境就绪后填值)
/// - `lib/core/config/api_config.dart`baseUrl 决策权威)
abstract class Env {
/// 当前环境名:**dev / test / release**(与 iOS NetworkConfig 三态对齐)。默认 dev。
///
/// - dev:开发环境(局域网 192.168.1.201:24801 / mock 模式可用)
/// - test:测试环境(dev.yixiong-tech.com:8081 — 与 iOS test 同源)
/// - release:正式环境(bac.new.hamkke.top — Release 包应锁定此值)
static const String name = String.fromEnvironment('ENV', defaultValue: 'dev');
/// API baseUrl 临时覆盖(CI/CD / 私有环境调试用)。
///
/// **正常情况下不应使用此变量** — 让 [ApiConfig.baseUrl] 按 [name] 自动选择
/// iOS 同源 URL。仅当需要连私有/临时环境时通过 dart-define 注入。
///
/// ```bash
/// flutter run --dart-define=API_BASE_URL=https://my-private.example.com
/// ```
static const String apiBaseUrlOverride = String.fromEnvironment(
'API_BASE_URL',
);
/// Sentry DSN。**Q9 待答前**为空,[SentryFlutter.init] 自动跳过实际上报。
static const String sentryDsn = String.fromEnvironment(
'SENTRY_DSN',
);
/// 是否为内部测试包(决定 TalkerScreen 调试面板是否挂载)。
/// 详见 docs/real-environment-verification.md §M4 / §Talker 准入。
static const bool isInternalBuild = bool.fromEnvironment(
'INTERNAL_BUILD',
);
/// 是否启用 mock fixtures 路径(绕过真实网络请求)。
/// 详见 plan §Mock 与真实环境验证分层策略。
static const bool useMock = bool.fromEnvironment(
'USE_MOCK',
);
/// 腾讯云 IM SDKAppID(数字 ID)。
/// **Q3 待答前**为 0(无效值,init 会返回失败但不崩溃)。
/// 沙箱测试用控制台测试 SDKAppID;生产用真实业务 SDKAppID。
/// 注入:--dart-define=IM_SDK_APP_ID=1400xxxxxx
static const int imSdkAppId = int.fromEnvironment(
'IM_SDK_APP_ID',
);
/// RSA 公钥(DER-SPKI Base64)— 用于登录密码加密。
/// 默认值 = iOS dev 公钥(对应 platform-ios/.../RSAEncryption.swift 中的 publicKeyString)。
/// 生产 / Q6 答复后通过 --dart-define=RSA_PUBLIC_KEY=... 注入真实公钥。
static const String rsaPublicKey = String.fromEnvironment(
'RSA_PUBLIC_KEY',
defaultValue:
'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCmZfR/bA9X3vp86y1aEpvwzXJYKRRF1fLau2+05/ZtaITLpV8bhkmSf3neSy/Q9gAdvG75Fr73E+GWE+K5b0BpvIS1jDGo319+PpZR39SaZTKZ27XFXrosmJTZutN79t819HS1VseleunHAFgMVufE9U5jP6LGzl/wbkSy01GhzwIDAQAB',
);
/// TLS 证书绑定指纹列表(SHA-256,多指纹支持轮换)。
/// **Q2 待答前**为空,CertificatePinningInterceptor 在空列表时跳过校验。
/// 真实指纹通过 --dart-define=PINNED_FINGERPRINTS=AA:BB,CC:DD 注入。
static List<String> get pinnedFingerprints {
const raw = String.fromEnvironment(
'PINNED_FINGERPRINTS',
);
if (raw.isEmpty) return const [];
return raw
.split(',')
.map((s) => s.trim())
.where((s) => s.isNotEmpty)
.toList();
}
// ---- 便捷判断(对齐 iOS NetworkConfig.Environment 三态)----
static bool get isDev => name == 'dev';
static bool get isTest => name == 'test';
static bool get isRelease => name == 'release';
/// 兼容旧调用 — 历史代码可能用 isProd 判断
/// @Deprecated 新代码请用 [isRelease]
static bool get isProd => isRelease;
/// Release 包除非 INTERNAL_BUILD=true,否则视为生产模式。
/// 用于 TalkerScreen / Riverpod observer 等开发面板的门控。
static bool get enableDevPanel => kDebugMode || isInternalBuild;
}
+35
View File
@@ -0,0 +1,35 @@
import 'package:dio/dio.dart';
import 'package:sunny_mochi/core/config/api_paths.dart';
import 'package:sunny_mochi/core/crash/pending_crash_report.dart';
import 'package:sunny_mochi/core/network/api_response.dart';
import 'package:sunny_mochi/core/network/dio_client.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'crash_datasource.g.dart';
@riverpod
CrashDatasource crashDatasource(Ref ref) =>
CrashDatasource(ref.watch(dioClientProvider));
/// 崩溃报告上报接口。
///
/// **后端接口规范(待后端实现)**:
/// ```
/// POST /sys/crash-report
/// Headers: satoken: <token>(可选,未登录也应接受)
/// Body: { "kind", "error", "stackTrace", "occurredAt", "deviceModel", ... }
/// Response: { "retCode": "00000", "retMsg": "ok", "retData": null }
/// ```
// TODO(backend): 实现 POST /sys/crash-report 接口(接收客户端崩溃日志)
class CrashDatasource {
const CrashDatasource(this._dio);
final Dio _dio;
Future<void> sendCrashReport(PendingCrashReport report) async {
final resp = await _dio.post<Map<String, dynamic>>(
ApiPaths.crashReport,
data: report.toJson(),
);
parseEnvelope(resp.data, (j) => j).unwrapVoid();
}
}
+52
View File
@@ -0,0 +1,52 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'crash_datasource.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(crashDatasource)
final crashDatasourceProvider = CrashDatasourceProvider._();
final class CrashDatasourceProvider
extends
$FunctionalProvider<CrashDatasource, CrashDatasource, CrashDatasource>
with $Provider<CrashDatasource> {
CrashDatasourceProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'crashDatasourceProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$crashDatasourceHash();
@$internal
@override
$ProviderElement<CrashDatasource> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
CrashDatasource create(Ref ref) {
return crashDatasource(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(CrashDatasource value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<CrashDatasource>(value),
);
}
}
String _$crashDatasourceHash() => r'4506810eb85819b314f44127ee944feb65b56b20';
+206
View File
@@ -0,0 +1,206 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:sunny_mochi/core/crash/crash_datasource.dart';
import 'package:sunny_mochi/core/crash/pending_crash_report.dart';
import 'package:sunny_mochi/core/extensions/context_x.dart';
import 'package:sunny_mochi/core/widgets/app_toast.dart';
/// 崩溃报告对话框 — 下次启动时强制展示。
///
/// 用户体验:
/// - "发送报告"POST 上报 → 关闭弹窗,后台继续正常启动
/// - "跳过":直接关闭弹窗,继续正常启动
class CrashDialog extends ConsumerStatefulWidget {
const CrashDialog({super.key, required this.report});
final PendingCrashReport report;
@override
ConsumerState<CrashDialog> createState() => _CrashDialogState();
}
class _CrashDialogState extends ConsumerState<CrashDialog> {
bool _sending = false;
bool _showDetail = false;
Future<void> _send() async {
setState(() => _sending = true);
try {
await ref
.read(crashDatasourceProvider)
.sendCrashReport(widget.report);
if (mounted) {
Navigator.of(context).pop();
context.showToast('崩溃报告已发送,感谢您的反馈', type: ToastType.success);
}
} catch (e, st) {
if (mounted) {
setState(() => _sending = false);
context.showError(ref, e, st);
}
}
}
@override
Widget build(BuildContext context) {
final report = widget.report;
final time = report.occurredAtLocal;
final timeStr = time == null
? '未知时间'
: '${time.year}-${time.month.toString().padLeft(2, '0')}-'
'${time.day.toString().padLeft(2, '0')} '
'${time.hour.toString().padLeft(2, '0')}:'
'${time.minute.toString().padLeft(2, '0')}';
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r),
),
title: Row(
children: [
Icon(
Icons.warning_amber_rounded,
color: const Color(0xFFFF9F0A),
size: 22.sp,
),
SizedBox(width: 8.w),
Text(
'上次发生了意外退出',
style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600),
),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'应用在 $timeStr 崩溃退出。发送崩溃报告有助于我们修复问题,报告不包含您的个人数据。',
style: TextStyle(
fontSize: 14.sp,
color: const Color(0xFF636366),
height: 1.5,
),
),
SizedBox(height: 12.h),
_InfoRow('设备', '${report.deviceModel} / ${report.osVersion}'),
_InfoRow('版本', '${report.appVersion}+${report.appBuild}'),
SizedBox(height: 8.h),
GestureDetector(
onTap: () => setState(() => _showDetail = !_showDetail),
child: Row(
children: [
Text(
'错误详情',
style: TextStyle(
fontSize: 13.sp,
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w500,
),
),
Icon(
_showDetail
? Icons.expand_less_rounded
: Icons.expand_more_rounded,
size: 18.sp,
color: Theme.of(context).colorScheme.primary,
),
],
),
),
if (_showDetail) ...[
SizedBox(height: 8.h),
Container(
constraints: BoxConstraints(maxHeight: 160.h),
decoration: BoxDecoration(
color: const Color(0xFFF2F2F7),
borderRadius: BorderRadius.circular(8.r),
),
child: SingleChildScrollView(
padding:
EdgeInsets.symmetric(horizontal: 10.w, vertical: 8.h),
child: Text(
'${report.error}\n\n${_shortStack(report.stackTrace)}',
style: TextStyle(
fontSize: 11.sp,
color: const Color(0xFF3A3A3C),
fontFamily: 'monospace',
height: 1.4,
),
),
),
),
],
],
),
actions: [
TextButton(
onPressed: _sending ? null : () => Navigator.of(context).pop(),
child: Text(
'跳过',
style: TextStyle(
color: const Color(0xFF8E8E93),
fontSize: 15.sp,
),
),
),
FilledButton(
onPressed: _sending ? null : _send,
child: _sending
? SizedBox(
width: 16.w,
height: 16.w,
child: const CircularProgressIndicator.adaptive(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation(Colors.white),
),
)
: Text('发送报告', style: TextStyle(fontSize: 15.sp)),
),
],
);
}
String _shortStack(String stack) {
final lines = stack.split('\n');
if (lines.length <= 20) return stack;
return '${lines.take(20).join('\n')}\n...(${lines.length - 20} more lines)';
}
}
class _InfoRow extends StatelessWidget {
const _InfoRow(this.label, this.value);
final String label;
final String value;
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(bottom: 4.h),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 36.w,
child: Text(
label,
style: TextStyle(
fontSize: 12.sp,
color: const Color(0xFFAEAEB2),
),
),
),
Expanded(
child: Text(
value,
style: TextStyle(
fontSize: 12.sp,
color: const Color(0xFF636366),
),
),
),
],
),
);
}
}
+42
View File
@@ -0,0 +1,42 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sunny_mochi/core/crash/crash_dialog.dart';
import 'package:sunny_mochi/core/crash/crash_reporter.dart';
/// 应用根部崩溃检测门(wraps AppRoot)。
///
/// 在第一帧渲染完成后检查 [pendingCrashProvider],若非 null 则强制弹出
/// [CrashDialog]barrierDismissible: false)。
class CrashGate extends ConsumerStatefulWidget {
const CrashGate({super.key, required this.child});
final Widget child;
@override
ConsumerState<CrashGate> createState() => _CrashGateState();
}
class _CrashGateState extends ConsumerState<CrashGate> {
@override
void initState() {
super.initState();
final report = ref.read(pendingCrashProvider);
if (report != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
unawaited(
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (_) => CrashDialog(report: report),
),
);
}
});
}
}
@override
Widget build(BuildContext context) => widget.child;
}
+129
View File
@@ -0,0 +1,129 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:sunny_mochi/core/crash/pending_crash_report.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'crash_reporter.g.dart';
/// Riverpod provider:崩溃报告在 main.dart 通过 overrideWithValue 注入。
/// 默认值为 null(正常启动时无崩溃)。
@Riverpod(keepAlive: true)
PendingCrashReport? pendingCrash(Ref ref) => null;
/// 全局崩溃日志服务(纯静态,不依赖 Riverpod / Flutter framework)。
///
/// **生命周期**
/// 1. `preInit()` — main() 最早期调用,异步缓存路径 + 设备信息
/// 2. `consumePending()` — 读取并删除上次崩溃文件(一次性消费)
/// 3. `installHooks()` — 在 SentrySetup.init appRunner 内调用,链式挂载钩子
///
/// **崩溃写入**:仅用 `File.writeAsStringSync(flush: true)`,绝对不能 await。
abstract class CrashReporter {
static String? _crashFilePath;
static String _deviceModel = 'unknown';
static String _osVersion = 'unknown';
static String _appVersion = 'unknown';
static String _appBuild = '0';
/// 步骤 1:异步预初始化(收集路径 + 设备信息)。
static Future<void> preInit() async {
try {
final dir = await getApplicationDocumentsDirectory();
_crashFilePath = p.join(dir.path, 'crash_report.json');
final pkg = await PackageInfo.fromPlatform();
_appVersion = pkg.version;
_appBuild = pkg.buildNumber;
if (kIsWeb) return;
if (Platform.isAndroid) {
final info = await DeviceInfoPlugin().androidInfo;
_deviceModel = '${info.manufacturer} ${info.model}'.trim();
_osVersion = 'Android ${info.version.release}';
} else if (Platform.isIOS) {
final info = await DeviceInfoPlugin().iosInfo;
_deviceModel = info.utsname.machine;
_osVersion = '${info.systemName} ${info.systemVersion}';
} else {
_deviceModel = Platform.operatingSystem;
_osVersion = Platform.operatingSystemVersion;
}
} catch (_) {
// preInit 失败不能阻断启动流程
}
}
/// 步骤 2:一次性消费崩溃文件(读取 + 立即删除)。
/// 返回 null 表示本次是正常启动。
static PendingCrashReport? consumePending() {
final path = _crashFilePath;
if (path == null) return null;
final file = File(path);
if (!file.existsSync()) return null;
try {
final raw = file.readAsStringSync();
file.deleteSync();
return PendingCrashReport.fromRawJson(raw);
} catch (_) {
try {
file.deleteSync();
} catch (_) {}
return null;
}
}
/// 步骤 3:安装全局崩溃钩子(必须在 SentrySetup appRunner 内调用)。
///
/// 采用链式挂载:先读出 Sentry 已设置的 handler,我们包一层写回,
/// 确保 Sentry 依然能接到所有错误。
static void installHooks() {
final previousOnError = PlatformDispatcher.instance.onError;
PlatformDispatcher.instance.onError = (error, stack) {
_writeCrashSync(kind: 'dart', error: error, stackTrace: stack);
return previousOnError?.call(error, stack) ?? false;
};
}
/// 同步写入崩溃文件(crash hook 专用,不能有任何 await)。
static void _writeCrashSync({
required String kind,
required Object error,
required StackTrace stackTrace,
}) {
final path = _crashFilePath;
if (path == null) return;
try {
final stackStr = stackTrace.toString();
final truncated = stackStr.length > 4096
? '${stackStr.substring(0, 4096)}\n...(truncated)'
: stackStr;
final report = PendingCrashReport(
kind: kind,
error: error.toString(),
stackTrace: truncated,
occurredAt: DateTime.now().toUtc().toIso8601String(),
deviceModel: _deviceModel,
osVersion: _osVersion,
appVersion: _appVersion,
appBuild: _appBuild,
);
File(path).writeAsStringSync(
report.toRawJson(),
flush: true,
);
} catch (_) {
// crash hook 内绝对不能抛出异常
}
}
}
+64
View File
@@ -0,0 +1,64 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'crash_reporter.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Riverpod provider:崩溃报告在 main.dart 通过 overrideWithValue 注入。
/// 默认值为 null(正常启动时无崩溃)。
@ProviderFor(pendingCrash)
final pendingCrashProvider = PendingCrashProvider._();
/// Riverpod provider:崩溃报告在 main.dart 通过 overrideWithValue 注入。
/// 默认值为 null(正常启动时无崩溃)。
final class PendingCrashProvider
extends
$FunctionalProvider<
PendingCrashReport?,
PendingCrashReport?,
PendingCrashReport?
>
with $Provider<PendingCrashReport?> {
/// Riverpod provider:崩溃报告在 main.dart 通过 overrideWithValue 注入。
/// 默认值为 null(正常启动时无崩溃)。
PendingCrashProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'pendingCrashProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$pendingCrashHash();
@$internal
@override
$ProviderElement<PendingCrashReport?> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
PendingCrashReport? create(Ref ref) {
return pendingCrash(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(PendingCrashReport? value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<PendingCrashReport?>(value),
);
}
}
String _$pendingCrashHash() => r'4ccef6df51aa7ed093a0ac08518772bbd70ecc4e';
+77
View File
@@ -0,0 +1,77 @@
import 'dart:convert';
/// 上次启动前崩溃的快照(从 crash_report.json 反序列化)。
///
/// 手写 fromJson/toJson — 文件写入发生在崩溃现场,不能依赖 code generator。
class PendingCrashReport {
const PendingCrashReport({
required this.kind,
required this.error,
required this.stackTrace,
required this.occurredAt,
required this.deviceModel,
required this.osVersion,
required this.appVersion,
required this.appBuild,
this.context,
});
/// 崩溃来源:dart(未捕获异常)/ zone(Zone 逃逸)
final String kind;
/// 原始异常 toString(技术细节,不展示给用户)
final String error;
/// 堆栈(已截断至 4096 字符)
final String stackTrace;
/// UTC ISO 8601
final String occurredAt;
final String deviceModel;
final String osVersion;
final String appVersion;
final String appBuild;
/// FlutterErrorDetails.context(仅 flutter 类型有值)
final String? context;
factory PendingCrashReport.fromJson(Map<String, dynamic> json) =>
PendingCrashReport(
kind: json['kind'] as String? ?? 'unknown',
error: json['error'] as String? ?? '',
stackTrace: json['stackTrace'] as String? ?? '',
occurredAt: json['occurredAt'] as String? ?? '',
deviceModel: json['deviceModel'] as String? ?? 'unknown',
osVersion: json['osVersion'] as String? ?? 'unknown',
appVersion: json['appVersion'] as String? ?? 'unknown',
appBuild: json['appBuild'] as String? ?? '0',
context: json['context'] as String?,
);
factory PendingCrashReport.fromRawJson(String raw) =>
PendingCrashReport.fromJson(jsonDecode(raw) as Map<String, dynamic>);
Map<String, dynamic> toJson() => {
'kind': kind,
'error': error,
'stackTrace': stackTrace,
'occurredAt': occurredAt,
'deviceModel': deviceModel,
'osVersion': osVersion,
'appVersion': appVersion,
'appBuild': appBuild,
if (context != null) 'context': context,
};
String toRawJson() => jsonEncode(toJson());
/// 崩溃时间(本地时区,用于 UI 展示)。
DateTime? get occurredAtLocal {
try {
return DateTime.parse(occurredAt).toLocal();
} catch (_) {
return null;
}
}
}

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