迁移 Flutter 端与训练脚本,模型/训练产物移出 git(遵循纯代码约定)
@@ -0,0 +1,45 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.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
|
||||
@@ -0,0 +1,30 @@
|
||||
# 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: "058e0af2c2b57e369d905a03ac9748b0ebf543c6"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6
|
||||
base_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6
|
||||
- platform: ios
|
||||
create_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6
|
||||
base_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6
|
||||
|
||||
# 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,33 @@
|
||||
# observer
|
||||
|
||||
野生动物实时识别 App(Flutter 版)。Android / iOS 一套代码,后端接口与支付见
|
||||
[`docs/PaymentApi.md`](docs/PaymentApi.md)。
|
||||
|
||||
## iOS 真机部署(iPhone)
|
||||
|
||||
### 构建与安装
|
||||
|
||||
```bash
|
||||
# 真机必须传 Mac 局域网 IP:默认 API_BASE_URL 是 10.0.2.2(仅 Android 模拟器可用),
|
||||
# 不传则 iPhone 上所有网络请求(登录/授权/套餐)都会失败
|
||||
flutter build ios --release --dart-define=API_BASE_URL=http://<Mac局域网IP>:8080
|
||||
|
||||
# 安装到真机(UDID 可用 `xcrun devicectl list devices` 查询)
|
||||
xcrun devicectl device install app --device <UDID> build/ios/iphoneos/Runner.app
|
||||
|
||||
# 启动并抓控制台日志(--terminate-existing 先杀掉旧实例)
|
||||
xcrun devicectl device process launch --console --terminate-existing \
|
||||
--device <UDID> com.observer.app
|
||||
```
|
||||
|
||||
### 注意事项(踩过的坑)
|
||||
|
||||
- **debug 构建不能在真机上从桌面图标启动**:iOS 14+ 会提示
|
||||
"In iOS 14+, debug mode Flutter apps can only be launched from Flutter tooling"。
|
||||
debug 调试必须用 `flutter run -d <设备ID>` 或 Xcode IDE 启动(`flutter devices` 查设备ID);
|
||||
从图标启动只对 release 构建有效。
|
||||
- **模型输入是 NHWC**:`assets/model.tflite` 做过字节级手术(开头 TRANSPOSE→RESHAPE,
|
||||
输入 [1,320,320,3]),改动记录见 git 历史,重导模型需同步处理,否则 iOS 报
|
||||
"Node number 0 (TRANSPOSE) failed to prepare"。
|
||||
- **模拟器黑屏**:本机 iOS 模拟器 Impeller 渲染黑屏,验证 UI 用 VM service
|
||||
(`flutter run` 输出里的 DevTools 地址),或直接真机验证。
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,53 @@
|
||||
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.example.observer"
|
||||
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.example.observer"
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// tflite_flutter 依赖的 tensorflow-lite / tensorflow-lite-gpu / tensorflow-lite-api 三个 AAR
|
||||
// 声明了相同 namespace(org.tensorflow.lite),新 AGP 视作冲突直接报错;
|
||||
// 本项目仅用 CPU 推理,GPU delegate 未使用,排除 gpu 及其传递依赖的 api 即可。
|
||||
configurations.all {
|
||||
exclude(group = "org.tensorflow", module = "tensorflow-lite-gpu")
|
||||
exclude(group = "org.tensorflow", module = "tensorflow-lite-api")
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -0,0 +1,48 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<application
|
||||
android:label="视野"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:usesCleartextTraffic="true">
|
||||
<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.observer
|
||||
|
||||
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>
|
||||
|
After Width: | Height: | Size: 544 B |
|
After Width: | Height: | Size: 442 B |
|
After Width: | Height: | Size: 721 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
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>
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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")
|
||||
@@ -0,0 +1,2 @@
|
||||
pheasant
|
||||
suspect
|
||||
@@ -0,0 +1,103 @@
|
||||
# 后端 API 契约(账号 + 支付/授权)
|
||||
|
||||
客户端(Flutter)与后端之间的 REST 契约。账号体系:手机号 + 密码注册登录,登录返回自签名 token,后续接口携带 `Authorization: Bearer <token>`;**识别入口(搜索按钮)必须强制服务端校验授权**,不走本地缓存。
|
||||
|
||||
- Base URL: `AppConfig.apiBaseUrl`(占位 `https://YOUR_BACKEND.example.com`)
|
||||
- 响应统一格式: `{"code": 0, "message": "ok", "data": {...}}`,`code != 0` 视为失败;登录失效返回 `code 61`,客户端应回登录页
|
||||
- 授权语义: 自然日(当天 24:00 失效 / 7 天 / 30 天),以服务端为准
|
||||
- 账号标识: 手机号(服务端 license 表即账号表,手机号为主键;无独立用户表)
|
||||
|
||||
## 1. 注册
|
||||
|
||||
`POST /api/v1/auth/register`(公开,无需登录)
|
||||
|
||||
```json
|
||||
{"phone": "13800138000", "password": "pass123456"}
|
||||
```
|
||||
|
||||
响应 `data` 为空对象。重复注册报错;已被运营手动授权(发卡占位行)的手机号可注册补密码,授权保留。
|
||||
|
||||
## 2. 登录
|
||||
|
||||
`POST /api/v1/auth/login`(公开,无需登录)
|
||||
|
||||
```json
|
||||
{"phone": "13800138000", "password": "pass123456"}
|
||||
```
|
||||
|
||||
响应 `data`:
|
||||
|
||||
```json
|
||||
{"token": "<HMAC-SHA256 自签名 token>"}
|
||||
```
|
||||
|
||||
- token 无状态,有效期 `auth.tokenTtl`(默认 30 天),客户端存 secure storage
|
||||
- 后续所有接口携带 `Authorization: Bearer <token>`
|
||||
|
||||
## 3. 创建订单
|
||||
|
||||
`POST /api/v1/orders`(需登录)
|
||||
|
||||
```json
|
||||
{"planId": "day|week|month", "channel": "wechat|alipay"}
|
||||
```
|
||||
|
||||
响应 `data`:
|
||||
|
||||
```json
|
||||
{
|
||||
"orderId": "O20260822001",
|
||||
"payParams": {
|
||||
"partnerId": "1900xxxxx", "prepayId": "wx...", "nonceStr": "...",
|
||||
"timeStamp": "1728000000", "sign": "...", "packageValue": "Sign=WXPay"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 手机号由 token 识别,请求体不含 deviceId
|
||||
- `channel == wechat` 时 `payParams` 为微信 APP 支付下单参数
|
||||
- `channel == alipay` 时 `payParams` 为 `{"orderStr": "alipay_sdk=..."}`
|
||||
|
||||
## 4. 支付结果确认
|
||||
|
||||
`POST /api/v1/orders/{orderId}/confirm`(需登录)
|
||||
|
||||
```json
|
||||
{}
|
||||
```
|
||||
|
||||
客户端拉起 SDK 支付成功后调用(幂等)。服务端以微信/支付宝异步回调为准落授权;confirm 仅用于加速刷新。响应 `data: {"status": "paid|created|closed"}`。
|
||||
|
||||
## 5. 查询授权
|
||||
|
||||
`GET /api/v1/license`(需登录)
|
||||
|
||||
响应 `data`:
|
||||
|
||||
```json
|
||||
{"active": true, "expiresAt": "2026-08-29T23:59:59+08:00"}
|
||||
```
|
||||
|
||||
- `active: false` 或 `expiresAt` 已过期 → 客户端展示付费墙/充值入口
|
||||
- **识别入口(搜索按钮)必须调本接口做强制校验**:网络失败视为不可用并提示,禁止用本地缓存放行
|
||||
- 主界面到期时间展示可用本地缓存(服务端为准,刷新时覆盖)
|
||||
|
||||
## 6. 套餐
|
||||
|
||||
`GET /api/v1/plans`(需登录)拉取价格方案,**客户端不硬编码价格**(进充值页时拉取,价格以服务端为准)。
|
||||
|
||||
响应 `data`:
|
||||
|
||||
```json
|
||||
{
|
||||
"list": [
|
||||
{"planId": "day", "days": 1, "priceCents": 1000},
|
||||
{"planId": "week", "days": 7, "priceCents": 5600},
|
||||
{"planId": "month", "days": 30, "priceCents": 18000}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- `priceCents` 为整数分,客户端展示 ÷100 转元
|
||||
- 展示名由客户端按 `days` 派生「N天」,接口无 label 字段
|
||||
- 后端套餐来自 `config.yml` `plans` 节点(静态定价,改价改配置重启生效)
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -0,0 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
@@ -0,0 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
@@ -0,0 +1,66 @@
|
||||
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
|
||||
|
||||
# 微信 SDK podspec 对模拟器保守排除 arm64(其 xcframework 实际含 arm64 slice),
|
||||
# 不清除会导致 M 系 Mac 上模拟器构建被压成 x86_64 而无法安装运行。
|
||||
wechat = installer.pods_project.targets.find { |t| t.name == 'WechatOpenSDK-XCFramework' }
|
||||
wechat&.build_configurations&.each do |config|
|
||||
config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = ''
|
||||
end
|
||||
|
||||
# WechatOpenSDK-XCFramework 的 Headers 不会自动进入依赖方搜索路径
|
||||
# (CocoaPods 对 vendored XCFramework 的已知行为),fluwx 以引号引入
|
||||
# WXApi.h 需要显式补充头文件搜索路径。
|
||||
wechat_headers = Dir.glob(
|
||||
File.join(Pod::Config.instance.project_root, 'Pods', 'WechatOpenSDK-XCFramework',
|
||||
'WechatOpenSDK.xcframework', '*', 'WechatOpenSDK.framework', 'Headers')
|
||||
)
|
||||
unless wechat_headers.empty?
|
||||
fluwx = installer.pods_project.targets.find { |t| t.name == 'fluwx' }
|
||||
fluwx&.build_configurations&.each do |config|
|
||||
config.build_settings['HEADER_SEARCH_PATHS'] = [
|
||||
'$(inherited)',
|
||||
*wechat_headers.map { |p| "\"#{p}\"" },
|
||||
].join(' ')
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,83 @@
|
||||
PODS:
|
||||
- Flutter (1.0.0)
|
||||
- flutter_secure_storage (6.0.0):
|
||||
- Flutter
|
||||
- fluwx (0.0.1):
|
||||
- Flutter
|
||||
- fluwx/pay (= 0.0.1)
|
||||
- fluwx/pay (0.0.1):
|
||||
- Flutter
|
||||
- WechatOpenSDK-XCFramework (~> 2.0.4)
|
||||
- TensorFlowLiteC (2.12.0):
|
||||
- TensorFlowLiteC/Core (= 2.12.0)
|
||||
- TensorFlowLiteC/Core (2.12.0)
|
||||
- TensorFlowLiteC/CoreML (2.12.0):
|
||||
- TensorFlowLiteC/Core
|
||||
- TensorFlowLiteC/Metal (2.12.0):
|
||||
- TensorFlowLiteC/Core
|
||||
- TensorFlowLiteSwift (2.12.0):
|
||||
- TensorFlowLiteSwift/Core (= 2.12.0)
|
||||
- TensorFlowLiteSwift/Core (2.12.0):
|
||||
- TensorFlowLiteC (= 2.12.0)
|
||||
- TensorFlowLiteSwift/CoreML (2.12.0):
|
||||
- TensorFlowLiteC/CoreML (= 2.12.0)
|
||||
- TensorFlowLiteSwift/Core (= 2.12.0)
|
||||
- TensorFlowLiteSwift/Metal (2.12.0):
|
||||
- TensorFlowLiteC/Metal (= 2.12.0)
|
||||
- TensorFlowLiteSwift/Core (= 2.12.0)
|
||||
- tflite_flutter (0.0.1):
|
||||
- Flutter
|
||||
- TensorFlowLiteSwift (= 2.12.0)
|
||||
- TensorFlowLiteSwift/CoreML (= 2.12.0)
|
||||
- TensorFlowLiteSwift/Metal (= 2.12.0)
|
||||
- tobias (0.0.1):
|
||||
- Flutter
|
||||
- tobias/normal (= 0.0.1)
|
||||
- tobias/normal (0.0.1):
|
||||
- Flutter
|
||||
- vibration (1.7.5):
|
||||
- Flutter
|
||||
- WechatOpenSDK-XCFramework (2.0.7)
|
||||
|
||||
DEPENDENCIES:
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||
- fluwx (from `.symlinks/plugins/fluwx/ios`)
|
||||
- tflite_flutter (from `.symlinks/plugins/tflite_flutter/ios`)
|
||||
- tobias (from `.symlinks/plugins/tobias/ios`)
|
||||
- vibration (from `.symlinks/plugins/vibration/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- TensorFlowLiteC
|
||||
- TensorFlowLiteSwift
|
||||
- WechatOpenSDK-XCFramework
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
flutter_secure_storage:
|
||||
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||
fluwx:
|
||||
:path: ".symlinks/plugins/fluwx/ios"
|
||||
tflite_flutter:
|
||||
:path: ".symlinks/plugins/tflite_flutter/ios"
|
||||
tobias:
|
||||
:path: ".symlinks/plugins/tobias/ios"
|
||||
vibration:
|
||||
:path: ".symlinks/plugins/vibration/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
||||
fluwx: 6bf9c5a3a99ad31b0de137dd92370a0d10a60f4b
|
||||
TensorFlowLiteC: 20785a69299185a379ba9852b6625f00afd7984a
|
||||
TensorFlowLiteSwift: 3a4928286e9e35bdd3e17970f48e53c80d25e793
|
||||
tflite_flutter: 64b192e11352fe36943ab6656e1d49207f1a5595
|
||||
tobias: 7bc370eaccba2e7c7c345902a4a47dc5916cf8bb
|
||||
vibration: 8e2f50fc35bb736f9eecb7dd9f7047fbb6a6e888
|
||||
WechatOpenSDK-XCFramework: 5df9b250e9839dcc306ad8b00f46822eead1ed47
|
||||
|
||||
PODFILE CHECKSUM: bee538157bfc80e3e10d48a376da3677058aaad6
|
||||
|
||||
COCOAPODS: 1.17.0
|
||||
@@ -0,0 +1,782 @@
|
||||
// !$*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 */; };
|
||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
||||
847056A2B331EBEF7FE4658D /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 44D6E292382D4D067EB56539 /* 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 */; };
|
||||
9926B0B1CD9F66E289877411 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 324F3F864FEBF7907AB5D311 /* Pods_RunnerTests.framework */; };
|
||||
/* 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 */
|
||||
11C083119EC1D6C49118DBAB /* 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>"; };
|
||||
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>"; };
|
||||
324F3F864FEBF7907AB5D311 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
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; };
|
||||
352A4BA150122CC2D593B1FF /* 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>"; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
40021A3125FEC9A6F074E182 /* 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>"; };
|
||||
44D6E292382D4D067EB56539 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
542B41CC0975A03C978D0E7D /* 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>"; };
|
||||
6CCFCD2B5C2A6DB29ED6FEE9 /* Runner.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; 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>"; };
|
||||
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
95A9E03BE9A266B55717C203 /* 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>"; };
|
||||
B89928717BDFD7B081FE98E3 /* 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>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
8A8301CDECC0F90E622D6F74 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9926B0B1CD9F66E289877411 /* Pods_RunnerTests.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
|
||||
847056A2B331EBEF7FE4658D /* Pods_Runner.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
331C8082294A63A400263BE5 /* RunnerTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */,
|
||||
);
|
||||
path = RunnerTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
684998EFA363788852AAAD42 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
44D6E292382D4D067EB56539 /* Pods_Runner.framework */,
|
||||
324F3F864FEBF7907AB5D311 /* Pods_RunnerTests.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
|
||||
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 */,
|
||||
F8827B4F9A04615E3B2278F0 /* Pods */,
|
||||
684998EFA363788852AAAD42 /* 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 */,
|
||||
6CCFCD2B5C2A6DB29ED6FEE9 /* Runner.entitlements */,
|
||||
);
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F8827B4F9A04615E3B2278F0 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
352A4BA150122CC2D593B1FF /* Pods-Runner.debug.xcconfig */,
|
||||
11C083119EC1D6C49118DBAB /* Pods-Runner.release.xcconfig */,
|
||||
95A9E03BE9A266B55717C203 /* Pods-Runner.profile.xcconfig */,
|
||||
B89928717BDFD7B081FE98E3 /* Pods-RunnerTests.debug.xcconfig */,
|
||||
542B41CC0975A03C978D0E7D /* Pods-RunnerTests.release.xcconfig */,
|
||||
40021A3125FEC9A6F074E182 /* Pods-RunnerTests.profile.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
331C8080294A63A400263BE5 /* RunnerTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||
buildPhases = (
|
||||
435F2587C2B98BB6FE35CA42 /* [CP] Check Pods Manifest.lock */,
|
||||
331C807D294A63A400263BE5 /* Sources */,
|
||||
331C807F294A63A400263BE5 /* Resources */,
|
||||
8A8301CDECC0F90E622D6F74 /* 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 = (
|
||||
1A05B1452EF4ACCDC4D1537B /* [CP] Check Pods Manifest.lock */,
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
8C9F4CABE92F7C7C5DE4A971 /* [CP] Embed Pods Frameworks */,
|
||||
8E27EE045B33D54A69365374 /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = Runner;
|
||||
packageProductDependencies = (
|
||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
|
||||
);
|
||||
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;
|
||||
packageReferences = (
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
|
||||
);
|
||||
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 */
|
||||
1A05B1452EF4ACCDC4D1537B /* [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;
|
||||
};
|
||||
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";
|
||||
};
|
||||
435F2587C2B98BB6FE35CA42 /* [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;
|
||||
};
|
||||
8C9F4CABE92F7C7C5DE4A971 /* [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;
|
||||
};
|
||||
8E27EE045B33D54A69365374 /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\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_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = QRN5J857S2;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.observer.app;
|
||||
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;
|
||||
baseConfigurationReference = B89928717BDFD7B081FE98E3 /* 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.example.observer.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 = 542B41CC0975A03C978D0E7D /* 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.example.observer.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 = 40021A3125FEC9A6F074E182 /* 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.example.observer.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_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = QRN5J857S2;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.observer.app;
|
||||
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;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = QRN5J857S2;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.observer.app;
|
||||
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 */
|
||||
|
||||
/* Begin XCLocalSwiftPackageReference section */
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = {
|
||||
isa = XCLocalSwiftPackageReference;
|
||||
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
|
||||
};
|
||||
/* End XCLocalSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = FlutterGeneratedPluginSwiftPackage;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency 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,119 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<PreActions>
|
||||
<ExecutionAction
|
||||
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||
<ActionContent
|
||||
title = "Run Prepare Flutter Framework Script"
|
||||
scriptText = "/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" prepare ">
|
||||
<EnvironmentBuildable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</EnvironmentBuildable>
|
||||
</ActionContent>
|
||||
</ExecutionAction>
|
||||
</PreActions>
|
||||
<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>
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 295 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 450 B |
|
After Width: | Height: | Size: 282 B |
|
After Width: | Height: | Size: 462 B |
|
After Width: | Height: | Size: 704 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 586 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 762 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |
|
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>
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="24765" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24743"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</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="393" height="852"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-248" y="7"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
@@ -0,0 +1,108 @@
|
||||
<?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>视野</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>observer</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>wx0000000000000000</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>alipay0000000000</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>weixin</string>
|
||||
<string>weixinULAPI</string>
|
||||
<string>weixinURLParamsAPI</string>
|
||||
<string>alipay</string>
|
||||
<string>alipays</string>
|
||||
</array>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
<key>NSAllowsArbitraryLoadsInWebContent</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>需要使用相机进行野生动物实时识别</string>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>需要通过本地网络连接服务器进行账号验证和支付</string>
|
||||
<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>
|
||||
@@ -0,0 +1 @@
|
||||
#import "GeneratedPluginRegistrant.h"
|
||||
@@ -0,0 +1,6 @@
|
||||
<?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>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,6 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
|
||||
class SceneDelegate: FlutterSceneDelegate {
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'auth/auth_screen.dart';
|
||||
import 'auth/session_store.dart';
|
||||
import 'camera/camera_screen.dart';
|
||||
import 'container.dart';
|
||||
import 'home/home_screen.dart';
|
||||
import 'payment/paywall_screen.dart';
|
||||
|
||||
class ObserverApp extends StatelessWidget {
|
||||
final AppContainer container;
|
||||
|
||||
const ObserverApp({super.key, required this.container});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
Provider.value(value: container),
|
||||
Provider.value(value: container.sessionStore),
|
||||
ChangeNotifierProvider.value(value: container.authViewModel),
|
||||
ChangeNotifierProvider.value(value: container.homeViewModel),
|
||||
ChangeNotifierProvider.value(value: container.paywallViewModel),
|
||||
],
|
||||
child: MaterialApp(
|
||||
title: '视野',
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
|
||||
),
|
||||
initialRoute: '/gate',
|
||||
routes: {
|
||||
'/gate': (_) => const StartupGate(),
|
||||
'/login': (_) => const AuthScreen(),
|
||||
'/home': (_) => const HomeScreen(),
|
||||
'/paywall': (_) => const PaywallScreen(),
|
||||
'/camera': (_) => const CameraScreen(),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 启动门卫:无登录 token → 登录页;已登录 → 主界面(到期状态由主界面展示)
|
||||
class StartupGate extends StatefulWidget {
|
||||
const StartupGate({super.key});
|
||||
|
||||
@override
|
||||
State<StartupGate> createState() => _StartupGateState();
|
||||
}
|
||||
|
||||
class _StartupGateState extends State<StartupGate> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_check();
|
||||
}
|
||||
|
||||
Future<void> _check() async {
|
||||
final session = context.read<SessionStore>();
|
||||
final token = await session.readToken();
|
||||
if (!mounted) return;
|
||||
Navigator.of(context)
|
||||
.pushReplacementNamed(token == null ? '/login' : '/home');
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Scaffold(
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'auth_view_model.dart';
|
||||
|
||||
/// 登录/注册页:手机号 + 密码;注册成功后自动登录。
|
||||
class AuthScreen extends StatefulWidget {
|
||||
const AuthScreen({super.key});
|
||||
|
||||
@override
|
||||
State<AuthScreen> createState() => _AuthScreenState();
|
||||
}
|
||||
|
||||
class _AuthScreenState extends State<AuthScreen> {
|
||||
final _phoneCtrl = TextEditingController();
|
||||
final _passwordCtrl = TextEditingController();
|
||||
bool _obscure = true;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_phoneCtrl.dispose();
|
||||
_passwordCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
static final _phoneRe = RegExp(r'^1[3-9]\d{9}$');
|
||||
|
||||
Future<void> _submit() async {
|
||||
final vm = context.read<AuthViewModel>();
|
||||
final phone = _phoneCtrl.text.trim();
|
||||
final password = _passwordCtrl.text;
|
||||
if (!_phoneRe.hasMatch(phone)) {
|
||||
vm.showError('请输入正确的 11 位手机号');
|
||||
return;
|
||||
}
|
||||
if (password.length < 6) {
|
||||
vm.showError('密码至少 6 位');
|
||||
return;
|
||||
}
|
||||
final ok = await vm.submit(phone: phone, password: password);
|
||||
if (ok && mounted) {
|
||||
Navigator.of(context).pushReplacementNamed('/home');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final vm = context.watch<AuthViewModel>();
|
||||
final isLogin = vm.mode == AuthMode.login;
|
||||
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Icon(Icons.pets, size: 64, color: Colors.green),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'视野',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'野生动物实时识别',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.grey.shade600),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
TextField(
|
||||
controller: _phoneCtrl,
|
||||
keyboardType: TextInputType.phone,
|
||||
maxLength: 11,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
decoration: const InputDecoration(
|
||||
labelText: '手机号',
|
||||
prefixIcon: Icon(Icons.phone_android),
|
||||
border: OutlineInputBorder(),
|
||||
counterText: '',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: _passwordCtrl,
|
||||
obscureText: _obscure,
|
||||
maxLength: 64,
|
||||
decoration: InputDecoration(
|
||||
labelText: '密码',
|
||||
prefixIcon: const Icon(Icons.lock_outline),
|
||||
border: const OutlineInputBorder(),
|
||||
counterText: '',
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(_obscure ? Icons.visibility_off : Icons.visibility),
|
||||
onPressed: () => setState(() => _obscure = !_obscure),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FilledButton(
|
||||
onPressed: vm.loading ? null : _submit,
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size.fromHeight(48),
|
||||
),
|
||||
child: vm.loading
|
||||
? const SizedBox(
|
||||
width: 22,
|
||||
height: 22,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: Text(isLogin ? '登录' : '注册并登录'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextButton(
|
||||
onPressed: vm.loading ? null : vm.switchMode,
|
||||
child: Text(isLogin ? '没有账号?去注册' : '已有账号?去登录'),
|
||||
),
|
||||
if (vm.error != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
vm.error!,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../payment/order_api.dart';
|
||||
import 'session_store.dart';
|
||||
|
||||
enum AuthMode { login, register }
|
||||
|
||||
class AuthViewModel extends ChangeNotifier {
|
||||
final OrderApi orderApi;
|
||||
final SessionStore sessionStore;
|
||||
|
||||
AuthMode _mode = AuthMode.login;
|
||||
bool _loading = false;
|
||||
String? _error;
|
||||
|
||||
AuthMode get mode => _mode;
|
||||
bool get loading => _loading;
|
||||
String? get error => _error;
|
||||
|
||||
AuthViewModel({required this.orderApi, required this.sessionStore});
|
||||
|
||||
void switchMode() {
|
||||
_mode = _mode == AuthMode.login ? AuthMode.register : AuthMode.login;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 本地输入校验失败提示(不进网络请求)
|
||||
void showError(String message) {
|
||||
_error = message;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 登录或注册;成功返回 true(调用方负责跳转主界面)
|
||||
Future<bool> submit({required String phone, required String password}) async {
|
||||
_loading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
try {
|
||||
if (_mode == AuthMode.login) {
|
||||
final token = await orderApi.login(phone: phone, password: password);
|
||||
await sessionStore.save(phone, token);
|
||||
} else {
|
||||
await orderApi.register(phone: phone, password: password);
|
||||
final token = await orderApi.login(phone: phone, password: password);
|
||||
await sessionStore.save(phone, token);
|
||||
}
|
||||
return true;
|
||||
} on OrderApiException catch (e) {
|
||||
_error = e.message;
|
||||
return false;
|
||||
} finally {
|
||||
_loading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
/// 登录会话持久化:token + 手机号存 secure storage。
|
||||
/// 启动时读取判断是否已登录;登出/401 时清除回登录页。
|
||||
class SessionStore {
|
||||
static const _storage = FlutterSecureStorage();
|
||||
static const _tokenKey = 'auth_token';
|
||||
static const _phoneKey = 'auth_phone';
|
||||
|
||||
static String? _cachedToken;
|
||||
static String? _cachedPhone;
|
||||
|
||||
Future<String?> readToken() async {
|
||||
if (_cachedToken != null) return _cachedToken;
|
||||
return _cachedToken = await _storage.read(key: _tokenKey);
|
||||
}
|
||||
|
||||
Future<String?> readPhone() async {
|
||||
if (_cachedPhone != null) return _cachedPhone;
|
||||
return _cachedPhone = await _storage.read(key: _phoneKey);
|
||||
}
|
||||
|
||||
Future<void> save(String phone, String token) async {
|
||||
_cachedPhone = phone;
|
||||
_cachedToken = token;
|
||||
await _storage.write(key: _phoneKey, value: phone);
|
||||
await _storage.write(key: _tokenKey, value: token);
|
||||
}
|
||||
|
||||
Future<void> clear() async {
|
||||
_cachedPhone = null;
|
||||
_cachedToken = null;
|
||||
await _storage.delete(key: _phoneKey);
|
||||
await _storage.delete(key: _tokenKey);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'frame_analyzer.dart';
|
||||
|
||||
/// camera 插件封装:后摄图像流(对应 Kotlin CameraController)。
|
||||
class AppCameraController {
|
||||
final List<CameraDescription> cameras;
|
||||
CameraController? controller;
|
||||
|
||||
/// 图像流回调实际触发次数(诊断用,与 analyzer 帧计数区分)
|
||||
int streamCallbacks = 0;
|
||||
|
||||
AppCameraController._(this.cameras);
|
||||
|
||||
static Future<AppCameraController?> create() async {
|
||||
final cameras = await availableCameras();
|
||||
if (cameras.isEmpty) return null;
|
||||
return AppCameraController._(cameras);
|
||||
}
|
||||
|
||||
bool get isInitialized => controller?.value.isInitialized ?? false;
|
||||
|
||||
CameraController get currentController =>
|
||||
controller ?? (throw StateError('camera not initialized'));
|
||||
|
||||
/// 图像流送达时的旋转角(传感器 → 竖屏显示所需的顺时针旋转)。
|
||||
/// 与 CameraX rotationDegrees 同公式;预览本身由平台旋转,检测框 overlay
|
||||
/// 用同一角度映射即可对齐。
|
||||
int get rotationDegrees {
|
||||
final c = controller;
|
||||
if (c == null) return 0;
|
||||
final deviceDegrees = switch (c.value.deviceOrientation) {
|
||||
DeviceOrientation.portraitUp => 0,
|
||||
DeviceOrientation.landscapeLeft => 90,
|
||||
DeviceOrientation.portraitDown => 180,
|
||||
DeviceOrientation.landscapeRight => 270,
|
||||
};
|
||||
final sensor = c.description.sensorOrientation;
|
||||
final isFront =
|
||||
c.description.lensDirection == CameraLensDirection.front;
|
||||
final degrees = (isFront ? sensor + deviceDegrees : sensor - deviceDegrees) % 360;
|
||||
return degrees < 0 ? degrees + 360 : degrees;
|
||||
}
|
||||
|
||||
Future<void> start(FrameAnalyzer analyzer) async {
|
||||
await stop();
|
||||
final desc = cameras.firstWhere(
|
||||
(c) => c.lensDirection == CameraLensDirection.back,
|
||||
orElse: () => cameras.first);
|
||||
// iOS 用默认 bgra8888(420v 在部分 iOS 版本上视频输出静默不送帧),
|
||||
// Android 用 yuv420 多平面。
|
||||
final fmt = defaultTargetPlatform == TargetPlatform.iOS
|
||||
? ImageFormatGroup.bgra8888
|
||||
: ImageFormatGroup.yuv420;
|
||||
final c = CameraController(desc, ResolutionPreset.high,
|
||||
enableAudio: false, imageFormatGroup: fmt);
|
||||
controller = c;
|
||||
await c.initialize();
|
||||
// 相机(重新)启动后重置运动/背景参考与抽帧节流,避免旧场景残留
|
||||
analyzer.reset();
|
||||
analyzer.worker?.reset();
|
||||
debugPrint('[camera] initialized, starting image stream');
|
||||
try {
|
||||
await c.startImageStream((image) {
|
||||
streamCallbacks++;
|
||||
try {
|
||||
analyzer.analyze(image, rotationDegrees);
|
||||
} catch (e, st) {
|
||||
debugPrint('[camera] analyze error: $e\n$st');
|
||||
analyzer.recordStreamError('analyze: $e');
|
||||
}
|
||||
});
|
||||
debugPrint('[camera] startImageStream ok');
|
||||
} catch (e, st) {
|
||||
debugPrint('[camera] startImageStream FAILED: $e\n$st');
|
||||
analyzer.recordStreamError('startImageStream: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> stop() async {
|
||||
final c = controller;
|
||||
if (c == null) return;
|
||||
controller = null;
|
||||
try {
|
||||
await c.stopImageStream();
|
||||
} catch (_) {}
|
||||
await c.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
import 'dart:ui' show PlatformDispatcher;
|
||||
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/foundation.dart' show defaultTargetPlatform;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
||||
import '../detection/detector_worker.dart';
|
||||
import '../reminder/reminder.dart';
|
||||
import 'app_camera_controller.dart';
|
||||
import 'camera_view_model.dart';
|
||||
import 'detection_overlay.dart';
|
||||
import 'frame_analyzer.dart';
|
||||
|
||||
/// 主界面:相机预览 + 检测框 overlay + 顶栏(返回/切换摄像头)
|
||||
class CameraScreen extends StatefulWidget {
|
||||
const CameraScreen({super.key});
|
||||
|
||||
@override
|
||||
State<CameraScreen> createState() => _CameraScreenState();
|
||||
}
|
||||
|
||||
class _CameraScreenState extends State<CameraScreen> {
|
||||
CameraViewModel? _viewModel;
|
||||
FrameAnalyzer? _analyzer;
|
||||
AppCameraController? _cameraController;
|
||||
bool _initFailed = false;
|
||||
bool _permissionGranted = false;
|
||||
String? _globalError;
|
||||
String? _initError;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final oldPlatform = PlatformDispatcher.instance.onError;
|
||||
PlatformDispatcher.instance.onError = (error, stack) {
|
||||
setState(() => _globalError = 'Platform: $error');
|
||||
return oldPlatform?.call(error, stack) ?? false;
|
||||
};
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _init());
|
||||
// 相机页常亮:野外观察时保持屏幕不熄(离开页面时关闭)
|
||||
WakelockPlus.enable();
|
||||
}
|
||||
|
||||
Future<void> _init() async {
|
||||
final granted = await Permission.camera.request().isGranted;
|
||||
if (!mounted) return;
|
||||
setState(() => _permissionGranted = granted);
|
||||
if (!granted) return;
|
||||
|
||||
// 模型加载/推理在后台 isolate,不阻塞 UI;worker 为 null 时仅预览并提示
|
||||
final worker = await DetectorWorker.create();
|
||||
final viewModel = CameraViewModel(reminder: Reminder());
|
||||
viewModel.setModelReady(worker != null);
|
||||
final analyzer = FrameAnalyzer(worker: worker, viewModel: viewModel);
|
||||
if (!mounted) {
|
||||
analyzer.dispose();
|
||||
viewModel.dispose();
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_viewModel = viewModel;
|
||||
_analyzer = analyzer;
|
||||
});
|
||||
|
||||
await _startCamera();
|
||||
}
|
||||
|
||||
Future<void> _startCamera() async {
|
||||
final analyzer = _analyzer;
|
||||
if (analyzer == null) return;
|
||||
try {
|
||||
final controller = await AppCameraController.create();
|
||||
if (controller == null) {
|
||||
setState(() {
|
||||
_initFailed = true;
|
||||
_initError = '未找到可用摄像头';
|
||||
});
|
||||
return;
|
||||
}
|
||||
await controller.start(analyzer);
|
||||
if (!mounted) {
|
||||
controller.stop();
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_cameraController = controller;
|
||||
_initFailed = false;
|
||||
_initError = null;
|
||||
});
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_initFailed = true;
|
||||
_initError = '$e';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WakelockPlus.disable();
|
||||
_cameraController?.stop();
|
||||
_analyzer?.dispose();
|
||||
_viewModel?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final vm = _viewModel;
|
||||
final camera = _cameraController;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
if (!_permissionGranted)
|
||||
_PermissionGuide(onRequest: () => _init())
|
||||
else if (vm != null && (camera?.isInitialized ?? false))
|
||||
// 预览 + 检测框同几何:overlay 作为 CameraPreview 的 child,
|
||||
// 与纹理共享同一 Stack/尺寸,避免比例或裁剪导致的位置偏移
|
||||
ListenableBuilder(
|
||||
listenable: vm,
|
||||
builder: (context, _) => _ZoomablePreview(
|
||||
controller: camera!.currentController,
|
||||
imageWidthPx: vm.state.imageWidthPx,
|
||||
imageHeightPx: vm.state.imageHeightPx,
|
||||
overlay: DetectionOverlay(
|
||||
results: vm.state.results,
|
||||
// iOS 纹理不旋转显示(_wrapInRotatedBox 仅 Android),
|
||||
// 显示方向 = buffer 原样 = 检测方向,旋转必须为 0;
|
||||
// Android 纹理被 RotatedBox 旋转,需用插件报告的 rotation。
|
||||
rotation: defaultTargetPlatform == TargetPlatform.iOS
|
||||
? 0
|
||||
: vm.state.rotation,
|
||||
imageWidthPx: vm.state.imageWidthPx,
|
||||
imageHeightPx: vm.state.imageHeightPx,
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (camera?.isInitialized ?? false)
|
||||
_ZoomablePreview(controller: camera!.currentController)
|
||||
else
|
||||
const Center(
|
||||
child: Text('相机启动中…', style: TextStyle(color: Colors.white70)),
|
||||
),
|
||||
|
||||
// 帧级动态层(横幅/诊断行)单独订阅 viewModel,避免整屏重建
|
||||
if (vm != null)
|
||||
ListenableBuilder(
|
||||
listenable: vm,
|
||||
builder: (context, _) => _buildDiagnosticsLayer(camera),
|
||||
),
|
||||
|
||||
if (_initFailed)
|
||||
Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('相机初始化失败', style: TextStyle(color: Colors.white)),
|
||||
if (_initError != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text(
|
||||
_initError!,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
color: Colors.redAccent, fontSize: 11),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setState(() => _initFailed = false);
|
||||
_startCamera();
|
||||
},
|
||||
child: const Text('重试'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Positioned(
|
||||
top: MediaQuery.of(context).padding.top + 8,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: _CameraTopBar(
|
||||
onClose: () => Navigator.of(context).pop(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDiagnosticsLayer(AppCameraController? camera) {
|
||||
final vm = _viewModel!;
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// 模型未加载时仅显示相机预览,不做检测标注(横幅置于顶栏下方,避免与底部诊断行重叠)
|
||||
if (!vm.state.modelReady)
|
||||
Positioned(
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: MediaQuery.of(context).padding.top + 56,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'识别模型加载失败:${DetectorWorker.lastLoadError ?? '未知原因'}\n最后步骤:${DetectorWorker.lastLog ?? '-'}',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Colors.orange, fontSize: 14),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Positioned(
|
||||
left: 8,
|
||||
right: 8,
|
||||
bottom: MediaQuery.of(context).padding.bottom + 8,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'模型:${vm.state.modelReady ? '已加载' : '未加载'} 帧:${vm.state.framesReceived} 流:${camera?.streamCallbacks ?? 0} 推理:${vm.state.debugDetectCalls}次 异常:${vm.state.debugDetectErrors}次 处理:${vm.state.debugLastMs}ms 最高分:${(vm.state.debugHighestScore * 100).toStringAsFixed(1)}% 图:${vm.state.imageWidthPx}x${vm.state.imageHeightPx} 旋:${vm.state.rotation}',
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 12),
|
||||
),
|
||||
if (camera != null)
|
||||
Text(
|
||||
'streaming:${camera.currentController.value.isStreamingImages} '
|
||||
'camErr:${camera.currentController.value.errorDescription ?? '无'}',
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Colors.cyanAccent, fontSize: 11),
|
||||
),
|
||||
if (vm.state.debugLastError != null)
|
||||
Text(
|
||||
vm.state.debugLastError!,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
color: Colors.redAccent, fontSize: 11),
|
||||
),
|
||||
if (_globalError != null)
|
||||
Text(
|
||||
_globalError!,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
color: Colors.redAccent, fontSize: 11),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 双指捏合缩放预览;overlay 与纹理同几何(CameraPreview child)
|
||||
class _ZoomablePreview extends StatefulWidget {
|
||||
final CameraController controller;
|
||||
|
||||
/// 检测框 overlay(随帧更新,作为 CameraPreview 的 child 与纹理同区域)
|
||||
final Widget? overlay;
|
||||
|
||||
/// 当前帧图像尺寸(用于按 buffer 比例约束预览,保证无拉伸变形)
|
||||
final int imageWidthPx;
|
||||
final int imageHeightPx;
|
||||
|
||||
const _ZoomablePreview({
|
||||
required this.controller,
|
||||
this.overlay,
|
||||
this.imageWidthPx = 0,
|
||||
this.imageHeightPx = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_ZoomablePreview> createState() => _ZoomablePreviewState();
|
||||
}
|
||||
|
||||
class _ZoomablePreviewState extends State<_ZoomablePreview> {
|
||||
double _minZoom = 1.0;
|
||||
double _maxZoom = 1.0;
|
||||
double _currentZoom = 1.0;
|
||||
double _gestureStartZoom = 1.0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.controller.getMinZoomLevel().then((v) {
|
||||
if (mounted) setState(() => _minZoom = v);
|
||||
});
|
||||
widget.controller.getMaxZoomLevel().then((v) {
|
||||
if (mounted) setState(() => _maxZoom = v);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final preview = GestureDetector(
|
||||
onScaleStart: (_) => _gestureStartZoom = _currentZoom,
|
||||
onScaleUpdate: (d) {
|
||||
final target =
|
||||
(_gestureStartZoom * d.scale).clamp(_minZoom, _maxZoom);
|
||||
if ((target - _currentZoom).abs() < 0.01) return;
|
||||
_currentZoom = target;
|
||||
widget.controller.setZoomLevel(target);
|
||||
},
|
||||
child: CameraPreview(widget.controller, child: widget.overlay),
|
||||
);
|
||||
|
||||
final w = widget.imageWidthPx.toDouble();
|
||||
final h = widget.imageHeightPx.toDouble();
|
||||
if (w <= 0 || h <= 0) return preview;
|
||||
// 按 buffer 比例约束显示区域:纹理与 overlay 同区域等比显示(无变形)
|
||||
return Center(
|
||||
child: AspectRatio(aspectRatio: w / h, child: preview),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CameraTopBar extends StatelessWidget {
|
||||
final VoidCallback onClose;
|
||||
|
||||
const _CameraTopBar({
|
||||
required this.onClose,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Colors.black54,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(
|
||||
tooltip: '返回',
|
||||
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
||||
onPressed: onClose,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PermissionGuide extends StatelessWidget {
|
||||
final VoidCallback onRequest;
|
||||
|
||||
const _PermissionGuide({required this.onRequest});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'需要相机权限才能进行实时识别',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FilledButton(onPressed: onRequest, child: const Text('授权相机')),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../detection/detection_result.dart';
|
||||
import '../detection/motion_aggregator.dart';
|
||||
import '../detection/tflite_detector.dart';
|
||||
import '../reminder/reminder.dart';
|
||||
|
||||
@immutable
|
||||
class CameraUiState {
|
||||
final bool modelReady;
|
||||
final List<DetectionResult> results;
|
||||
final int rotation;
|
||||
final int imageWidthPx;
|
||||
final int imageHeightPx;
|
||||
final double debugHighestScore;
|
||||
final int debugDetectCalls;
|
||||
final int debugDetectErrors;
|
||||
final String? debugLastError;
|
||||
final int framesReceived;
|
||||
final int debugLastMs;
|
||||
|
||||
const CameraUiState({
|
||||
this.modelReady = false,
|
||||
this.results = const [],
|
||||
this.rotation = 90,
|
||||
this.imageWidthPx = 0,
|
||||
this.imageHeightPx = 0,
|
||||
this.debugHighestScore = 0,
|
||||
this.debugDetectCalls = 0,
|
||||
this.debugDetectErrors = 0,
|
||||
this.debugLastError,
|
||||
this.framesReceived = 0,
|
||||
this.debugLastMs = 0,
|
||||
});
|
||||
}
|
||||
|
||||
/// 检测结果置信度分级与轨迹确认。
|
||||
///
|
||||
/// - [lowConf](模型阈值 0.10):低于此分的框在检测阶段已丢弃。
|
||||
/// - [highConf](0.35):高于此分直接确认显示;真实野鸡多为 0.1~0.2,
|
||||
/// 高于 0.35 视为强证据。
|
||||
/// - 0.10~0.35 之间:需要多帧稳定([confirmFrames] 帧)或 活动证据
|
||||
/// (运动区域/背景新出现区域重叠)才确认显示。
|
||||
class CameraViewModel extends ChangeNotifier {
|
||||
static const int maxTracks = 30;
|
||||
static const double motionBoost = 0.15;
|
||||
static const double lowConf = TfliteDetector.minScore;
|
||||
static const double highConf = 0.35;
|
||||
static const int confirmFrames = 3;
|
||||
static const double associateRadius = 0.12;
|
||||
static const int displayAgeMs = 500;
|
||||
static const int forgetMs = 2000;
|
||||
|
||||
/// 推理后台 isolate 是否就绪(由相机页创建 worker 后设置)
|
||||
bool modelReady = false;
|
||||
|
||||
final Reminder reminder;
|
||||
|
||||
CameraUiState _state;
|
||||
CameraUiState get state => _state;
|
||||
|
||||
final Map<int, _Track> _tracks = {};
|
||||
int _nextTrackId = 0;
|
||||
|
||||
CameraViewModel({required this.reminder}) : _state = const CameraUiState();
|
||||
|
||||
void setModelReady(bool ready) {
|
||||
if (modelReady == ready) return;
|
||||
modelReady = ready;
|
||||
_state = CameraUiState(modelReady: ready);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 帧分析回调(分析流调用)
|
||||
void onFramesAnalyzed(
|
||||
List<DetectionResult> results,
|
||||
int rotation,
|
||||
int imageWidthPx,
|
||||
int imageHeightPx,
|
||||
List<MotionRegion> motionRegions,
|
||||
List<MotionRegion> noveltyRegions, {
|
||||
int detectCalls = 0,
|
||||
int detectErrors = 0,
|
||||
String? lastError,
|
||||
int framesReceived = 0,
|
||||
int lastProcessMs = 0,
|
||||
}) {
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
_associate(results, motionRegions, noveltyRegions, now);
|
||||
|
||||
final visible = <DetectionResult>[];
|
||||
for (final t in _tracks.values) {
|
||||
if (now - t.firstSeenMs < displayAgeMs) continue;
|
||||
if (now - t.lastSeenMs > forgetMs) continue;
|
||||
if (!_shouldDisplay(t, motionRegions, noveltyRegions)) continue;
|
||||
var r = t.result;
|
||||
// 低分确认目标 + 活动证据 → 分数提升,便于视觉区分
|
||||
if (t.confirmed &&
|
||||
r.score < highConf &&
|
||||
_hasActivity(r, motionRegions, noveltyRegions)) {
|
||||
r = r.copyWith(score: (r.score + motionBoost).clamp(0.0, 1.0));
|
||||
}
|
||||
visible.add(r.copyWith(confirmed: t.confirmed));
|
||||
}
|
||||
|
||||
// 提醒:仅新确认的野鸡轨迹(确认瞬间触发一次,10s 同类冷却在 Reminder 内)
|
||||
for (final t in _tracks.values) {
|
||||
if (t.label != 'pheasant' || !t.confirmed || t.reminded) continue;
|
||||
final age = now - t.firstSeenMs;
|
||||
if (age >= displayAgeMs && age <= displayAgeMs + 1600 &&
|
||||
now - t.lastSeenMs <= 300) {
|
||||
t.reminded = true;
|
||||
reminder.onDetected(t.label);
|
||||
}
|
||||
}
|
||||
|
||||
var highest = 0.0;
|
||||
for (final r in results) {
|
||||
if (r.score > highest) highest = r.score;
|
||||
}
|
||||
_state = CameraUiState(
|
||||
modelReady: modelReady,
|
||||
results: visible,
|
||||
rotation: rotation,
|
||||
imageWidthPx: imageWidthPx,
|
||||
imageHeightPx: imageHeightPx,
|
||||
debugHighestScore: highest,
|
||||
debugDetectCalls: detectCalls,
|
||||
debugDetectErrors: detectErrors,
|
||||
debugLastError: lastError,
|
||||
framesReceived: framesReceived,
|
||||
debugLastMs: lastProcessMs,
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 检测框 → 轨迹关联:按中心距离就近匹配(同标签优先,跨标签收紧距离),
|
||||
/// 未匹配则新建候选轨迹。
|
||||
void _associate(
|
||||
List<DetectionResult> results,
|
||||
List<MotionRegion> motionRegions,
|
||||
List<MotionRegion> noveltyRegions,
|
||||
int now) {
|
||||
final matched = <int>{};
|
||||
for (final r in results) {
|
||||
if (!_plausible(r)) continue;
|
||||
_Track? best;
|
||||
var bestD = associateRadius;
|
||||
for (final t in _tracks.values) {
|
||||
if (matched.contains(t.id)) continue;
|
||||
final d = _centerDist(t.result, r);
|
||||
// 同标签宽松匹配;跨标签(野鸡↔疑似 抖动)收紧到 60%
|
||||
final limit = t.label == r.label ? bestD : associateRadius * 0.6;
|
||||
if (d < limit) {
|
||||
bestD = d;
|
||||
best = t;
|
||||
}
|
||||
}
|
||||
if (best != null) {
|
||||
matched.add(best.id);
|
||||
best.update(r, now);
|
||||
best.seenCount++;
|
||||
if (best.seenCount >= confirmFrames || r.score >= highConf ||
|
||||
_hasActivity(r, motionRegions, noveltyRegions)) {
|
||||
best.confirmed = true;
|
||||
}
|
||||
} else {
|
||||
final t = _Track(_nextTrackId++, now, r);
|
||||
t.seenCount = 1;
|
||||
t.confirmed = r.score >= highConf ||
|
||||
_hasActivity(r, motionRegions, noveltyRegions);
|
||||
_tracks[t.id] = t;
|
||||
}
|
||||
}
|
||||
_tracks.removeWhere(
|
||||
(id, t) => !matched.contains(id) && now - t.lastSeenMs > forgetMs);
|
||||
}
|
||||
|
||||
/// 显示判定(按类别策略):
|
||||
/// - 疑似(生境预警):设计意图是常驻静态预警,始终显示(渲染侧弱化)
|
||||
/// - 野鸡:确认轨迹直接显示;未确认的只有在高分或活动证据时才显示
|
||||
bool _shouldDisplay(_Track t, List<MotionRegion> motionRegions,
|
||||
List<MotionRegion> noveltyRegions) {
|
||||
if (t.label == 'suspect') return true;
|
||||
if (t.confirmed) return true;
|
||||
return t.result.score >= highConf ||
|
||||
_hasActivity(t.result, motionRegions, noveltyRegions);
|
||||
}
|
||||
|
||||
/// 活动证据:与运动区域或背景新出现区域重叠
|
||||
bool _hasActivity(DetectionResult r, List<MotionRegion> motionRegions,
|
||||
List<MotionRegion> noveltyRegions) =>
|
||||
motionRegions.any((m) => MotionAggregator.centerInRegion(r, m)) ||
|
||||
noveltyRegions.any((m) => MotionAggregator.centerInRegion(r, m));
|
||||
|
||||
/// 物理合理性过滤:宽高比与相对尺寸(野鸡 20-100px@720 量级,参照标注脚本)
|
||||
bool _plausible(DetectionResult r) {
|
||||
final h = r.height;
|
||||
final w = r.width;
|
||||
if (w <= 0 || h <= 0) return false;
|
||||
final aspect = w / h;
|
||||
if (aspect < 0.3 || aspect > 3.0) return false;
|
||||
if (r.label == 'suspect') return h >= 0.01 && h <= 0.5;
|
||||
return h >= 0.01 && h <= 0.3;
|
||||
}
|
||||
|
||||
double _centerDist(DetectionResult a, DetectionResult b) =>
|
||||
math.sqrt(math.pow(a.centerX - b.centerX, 2) +
|
||||
math.pow(a.centerY - b.centerY, 2));
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
reminder.release();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class _Track {
|
||||
final int id;
|
||||
final String label;
|
||||
int firstSeenMs;
|
||||
int lastSeenMs;
|
||||
int seenCount = 0;
|
||||
bool confirmed = false;
|
||||
bool reminded = false;
|
||||
DetectionResult result;
|
||||
|
||||
_Track(this.id, this.firstSeenMs, this.result)
|
||||
: lastSeenMs = firstSeenMs,
|
||||
label = result.label;
|
||||
|
||||
void update(DetectionResult r, int now) {
|
||||
lastSeenMs = now;
|
||||
result = r;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../detection/coordinate_mapper.dart';
|
||||
import '../detection/detection_result.dart';
|
||||
|
||||
/// 检测框绘制分级:
|
||||
/// - 野鸡 confirmed:红色实线 3px(强证据)
|
||||
/// - 野鸡 candidate:红色虚线 2px 半透明(待确认,弱提示)
|
||||
/// - 疑似(生境预警):黄色虚线 2px 半透明(常驻静态预警,弱化渲染)
|
||||
/// 标签附带距离估计(针孔模型 焦距px×参考体型/框高px)。
|
||||
class DetectionOverlay extends StatelessWidget {
|
||||
final List<DetectionResult> results;
|
||||
final int rotation;
|
||||
final int imageWidthPx;
|
||||
final int imageHeightPx;
|
||||
|
||||
const DetectionOverlay({
|
||||
super.key,
|
||||
required this.results,
|
||||
required this.rotation,
|
||||
required this.imageWidthPx,
|
||||
required this.imageHeightPx,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IgnorePointer(
|
||||
child: CustomPaint(
|
||||
painter: _OverlayPainter(results, rotation, imageWidthPx, imageHeightPx),
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OverlayPainter extends CustomPainter {
|
||||
final List<DetectionResult> results;
|
||||
final int rotation;
|
||||
final int imageWidthPx;
|
||||
final int imageHeightPx;
|
||||
|
||||
_OverlayPainter(this.results, this.rotation, this.imageWidthPx,
|
||||
this.imageHeightPx);
|
||||
|
||||
static const _colors = {
|
||||
'pheasant': Color(0xFFE53935),
|
||||
'suspect': Color(0xFFFDD835),
|
||||
};
|
||||
static const _labels = {'pheasant': '野鸡', 'suspect': '疑似'};
|
||||
|
||||
/// 参考体型(米):野鸡身高 / 植被高度(参照旧 DistanceEstimator)
|
||||
static const _refSizeM = {'pheasant': 0.45, 'suspect': 0.50};
|
||||
|
||||
/// iPhone 13 主摄在 1280 高预览下的估算焦距 px(5.1mm / 5.30mm 传感器),
|
||||
/// 单目误差 ±30%,仅作参考
|
||||
static const double focalPx = 1230;
|
||||
static const double maxDistanceM = 120;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
for (final r in results) {
|
||||
final rect = CoordinateMapper.mapToView(
|
||||
r.left,
|
||||
r.top,
|
||||
r.right,
|
||||
r.bottom,
|
||||
rotation,
|
||||
imageWidthPx,
|
||||
imageHeightPx,
|
||||
size.width,
|
||||
size.height,
|
||||
);
|
||||
final color = _colors[r.label] ?? Colors.white;
|
||||
final isSuspect = r.label == 'suspect';
|
||||
final confirmed = r.confirmed && !isSuspect;
|
||||
final paint = Paint()
|
||||
..color = color.withValues(alpha: confirmed ? 1.0 : 0.55)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = confirmed ? 3 : 2
|
||||
..isAntiAlias = true;
|
||||
final box = Rect.fromLTRB(rect.left, rect.top, rect.right, rect.bottom);
|
||||
if (confirmed) {
|
||||
canvas.drawRect(box, paint);
|
||||
} else {
|
||||
_drawDashedRect(canvas, box, paint);
|
||||
}
|
||||
|
||||
// 标签:框上方,含距离
|
||||
final dist = _distanceLabel(r);
|
||||
final text =
|
||||
'${_labels[r.label] ?? r.label} ${(r.score * 100).toInt()}%$dist';
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: text,
|
||||
style: TextStyle(
|
||||
color: color.withValues(alpha: confirmed ? 1.0 : 0.8),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
shadows: const [Shadow(color: Colors.black, blurRadius: 3)],
|
||||
),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout();
|
||||
final top = math.max(0.0, rect.top - 22);
|
||||
final left = math.max(0.0, rect.left);
|
||||
textPainter.paint(canvas, Offset(left + 4, top));
|
||||
}
|
||||
}
|
||||
|
||||
String _distanceLabel(DetectionResult r) {
|
||||
final refH = _refSizeM[r.label];
|
||||
if (refH == null) return '';
|
||||
final hPx = r.height * imageHeightPx;
|
||||
if (hPx < 8) return '';
|
||||
final m = focalPx * refH / hPx;
|
||||
if (m > maxDistanceM) return '';
|
||||
return ' ≈${m.round()}m';
|
||||
}
|
||||
|
||||
void _drawDashedRect(Canvas canvas, Rect r, Paint paint,
|
||||
{double dash = 10, double gap = 6}) {
|
||||
void dashLine(Offset a, Offset b) {
|
||||
final total = (b - a).distance;
|
||||
if (total <= 0) return;
|
||||
final dir = (b - a) / total;
|
||||
var d = 0.0;
|
||||
while (d < total) {
|
||||
final e = math.min(d + dash, total);
|
||||
canvas.drawLine(a + dir * d, a + dir * e, paint);
|
||||
d += dash + gap;
|
||||
}
|
||||
}
|
||||
|
||||
dashLine(r.topLeft, r.topRight);
|
||||
dashLine(r.topRight, r.bottomRight);
|
||||
dashLine(r.bottomRight, r.bottomLeft);
|
||||
dashLine(r.bottomLeft, r.topLeft);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_OverlayPainter oldDelegate) =>
|
||||
oldDelegate.results != results ||
|
||||
oldDelegate.rotation != rotation ||
|
||||
oldDelegate.imageWidthPx != imageWidthPx ||
|
||||
oldDelegate.imageHeightPx != imageHeightPx;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import 'package:camera/camera.dart';
|
||||
|
||||
import '../detection/detection_result.dart';
|
||||
import '../detection/detector_worker.dart';
|
||||
import 'camera_view_model.dart';
|
||||
|
||||
/// 抽帧节流 + 后台推理(对应 Kotlin FrameAnalyzer)。
|
||||
/// 推理在后台 isolate(DetectorWorker)执行,主 isolate 只投递帧与收结果。
|
||||
class FrameAnalyzer {
|
||||
/// 连续检测:100ms 一帧
|
||||
int intervalMs = 100;
|
||||
|
||||
/// null = 模型加载失败,仅预览不分析
|
||||
final DetectorWorker? worker;
|
||||
final CameraViewModel viewModel;
|
||||
|
||||
int _lastDetectMs = 0;
|
||||
|
||||
/// 诊断计数:推理调用/异常次数
|
||||
int detectCalls = 0;
|
||||
int detectErrors = 0;
|
||||
String? lastError;
|
||||
|
||||
/// 最近一次完整处理(预处理+推理+运动检测)耗时 ms(worker 侧)
|
||||
int lastProcessMs = 0;
|
||||
|
||||
/// 图像流回调是否到达(诊断用)
|
||||
int framesReceived = 0;
|
||||
|
||||
FrameAnalyzer({required this.worker, required this.viewModel}) {
|
||||
worker?.onResult = _onResult;
|
||||
worker?.onError = _onError;
|
||||
}
|
||||
|
||||
void recordStreamError(String msg) {
|
||||
lastError = msg;
|
||||
detectErrors++;
|
||||
}
|
||||
|
||||
void _onResult(
|
||||
List<DetectionResult> results,
|
||||
List<MotionRegion> motion,
|
||||
List<MotionRegion> novelty,
|
||||
int rotation,
|
||||
int width,
|
||||
int height,
|
||||
int processMs) {
|
||||
detectCalls++;
|
||||
lastProcessMs = processMs;
|
||||
viewModel.onFramesAnalyzed(
|
||||
results,
|
||||
rotation,
|
||||
width,
|
||||
height,
|
||||
motion,
|
||||
novelty,
|
||||
detectCalls: detectCalls,
|
||||
detectErrors: detectErrors,
|
||||
lastError: lastError,
|
||||
framesReceived: framesReceived,
|
||||
lastProcessMs: lastProcessMs,
|
||||
);
|
||||
}
|
||||
|
||||
void _onError(String msg) {
|
||||
detectErrors++;
|
||||
lastError = msg;
|
||||
viewModel.onFramesAnalyzed(
|
||||
const [],
|
||||
90,
|
||||
0,
|
||||
0,
|
||||
const [],
|
||||
const [],
|
||||
detectCalls: detectCalls,
|
||||
detectErrors: detectErrors,
|
||||
lastError: lastError,
|
||||
framesReceived: framesReceived,
|
||||
lastProcessMs: lastProcessMs,
|
||||
);
|
||||
}
|
||||
|
||||
void analyze(CameraImage image, int rotationDegrees) {
|
||||
framesReceived++;
|
||||
final w = worker;
|
||||
if (w == null) return;
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
if (now - _lastDetectMs < intervalMs) return;
|
||||
_lastDetectMs = now;
|
||||
if (w.busy) return; // 上一帧未返回则丢帧,避免在途积压
|
||||
w.analyze(image, rotationDegrees);
|
||||
}
|
||||
|
||||
void reset() => _lastDetectMs = 0;
|
||||
|
||||
void dispose() => worker?.dispose();
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import '../detection/detection_result.dart';
|
||||
import '../detection/motion_aggregator.dart';
|
||||
|
||||
/// 轻量运动检测:相邻帧 Y 通道差分 + 分块聚合。
|
||||
/// 小尺寸工作(约 128x128 内),在分析流中串行调用。
|
||||
/// 相机大幅移动时(全屏帧差)自动忽略本帧,避免误报。
|
||||
class MotionDetector {
|
||||
final int maxWidth;
|
||||
final int maxHeight;
|
||||
|
||||
List<int>? _prevGray;
|
||||
|
||||
MotionDetector({this.maxWidth = 128, this.maxHeight = 128});
|
||||
|
||||
/// 后台 isolate 用原始数据接口(不依赖 CameraImage)。
|
||||
List<MotionRegion> detectMotionRaw(
|
||||
Uint8List yPlane, int yStride, int width, int height) {
|
||||
final w = width, h = height;
|
||||
final scale = maxWidth / w < maxHeight / h ? maxWidth / w : maxHeight / h;
|
||||
final tw = (w * scale).toInt().clamp(1, maxWidth);
|
||||
final th = (h * scale).toInt().clamp(1, maxHeight);
|
||||
if (tw == 0 || th == 0) return const [];
|
||||
|
||||
// 取 Y 平面缩放灰度(最近邻下采样到 128x128 内)
|
||||
final y = yPlane;
|
||||
final gray = List<int>.filled(tw * th, 0);
|
||||
for (var oy = 0; oy < th; oy++) {
|
||||
final sy = (oy / scale).toInt().clamp(0, h - 1);
|
||||
for (var ox = 0; ox < tw; ox++) {
|
||||
final sx = (ox / scale).toInt().clamp(0, w - 1);
|
||||
gray[oy * tw + ox] = y[sy * yStride + sx];
|
||||
}
|
||||
}
|
||||
|
||||
final prev = _prevGray;
|
||||
_prevGray = List.of(gray);
|
||||
if (prev == null || prev.length != gray.length) return const [];
|
||||
|
||||
final diff = MotionAggregator.diffMask(gray, prev);
|
||||
final motionTotal = diff.fold(0, (a, b) => a + b);
|
||||
// 全屏大差异 → 相机移动/大范围变化,忽略本帧
|
||||
if (motionTotal > tw * th / 2) return const [];
|
||||
if (motionTotal < 12) return const [];
|
||||
return MotionAggregator.aggregate(diff, tw, th);
|
||||
}
|
||||
|
||||
/// 相机切换后重置参考帧,避免旧帧误差
|
||||
void reset() {
|
||||
_prevGray = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/// 全局配置占位:接入真实支付前需替换以下值。
|
||||
class AppConfig {
|
||||
/// 后端服务器地址(订单创建/授权查询/套餐价格)
|
||||
/// 默认 Android 模拟器 10.0.2.2;iOS 模拟器构建时传
|
||||
/// --dart-define=API_BASE_URL=http://127.0.0.1:8080
|
||||
static const String apiBaseUrl =
|
||||
String.fromEnvironment('API_BASE_URL', defaultValue: 'http://10.0.2.2:8080');
|
||||
|
||||
/// 微信开放平台 AppID(需在微信开放平台注册包名+签名)
|
||||
static const String wechatAppId = 'wx0000000000000000';
|
||||
static const String wechatUniversalLink =
|
||||
'https://YOUR_DOMAIN.com/wechat/';
|
||||
|
||||
/// 支付宝开放平台 AppID
|
||||
static const String alipayAppId = '2020000000000000';
|
||||
static const String alipayUniversalLink =
|
||||
'https://YOUR_DOMAIN.com/alipay/';
|
||||
|
||||
/// iOS URL Scheme(微信/支付宝拉起回调,需与 Info.plist 一致)
|
||||
static const String wechatUrlScheme = 'wx0000000000000000';
|
||||
static const String alipayUrlScheme = 'alipay0000000000';
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import 'auth/auth_view_model.dart';
|
||||
import 'auth/session_store.dart';
|
||||
import 'home/home_view_model.dart';
|
||||
import 'payment/license_service.dart';
|
||||
import 'payment/models.dart';
|
||||
import 'payment/order_api.dart';
|
||||
import 'payment/paywall_view_model.dart';
|
||||
import 'payment/payment_service.dart';
|
||||
|
||||
/// 手动依赖注入容器(对应原 Kotlin AppContainer)
|
||||
class AppContainer {
|
||||
late final SessionStore sessionStore;
|
||||
late final OrderApi orderApi;
|
||||
late final LicenseService licenseService;
|
||||
late final AuthViewModel authViewModel;
|
||||
late final HomeViewModel homeViewModel;
|
||||
late final PaywallViewModel paywallViewModel;
|
||||
|
||||
AppContainer() {
|
||||
sessionStore = SessionStore();
|
||||
orderApi = OrderApi(sessionStore: sessionStore);
|
||||
licenseService =
|
||||
LicenseService(orderApi: orderApi, sessionStore: sessionStore);
|
||||
authViewModel = AuthViewModel(orderApi: orderApi, sessionStore: sessionStore);
|
||||
homeViewModel = HomeViewModel(licenseService: licenseService);
|
||||
paywallViewModel = PaywallViewModel(
|
||||
orderApi: orderApi,
|
||||
licenseService: licenseService,
|
||||
services: {
|
||||
PayChannel.wechat: WechatPayService(orderApi: orderApi),
|
||||
PayChannel.alipay: AlipayService(orderApi: orderApi),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import 'dart:math' as math;
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'detection_result.dart';
|
||||
import 'motion_aggregator.dart';
|
||||
|
||||
/// 静态场景背景建模:运行均值 + 方差,帧差高于自适应阈值的像素记为"新出现",
|
||||
/// 分块聚合为新颖区域(novelty)。
|
||||
///
|
||||
/// 固定机位下,常驻物体(键盘/石头/文字)永远属于背景、不产生新颖区域;
|
||||
/// 走进画面的目标(野鸡移动/新出现)才会触发。比相邻帧差分更强的证据:
|
||||
/// 风吹草动是持续的背景更新,不会长期标记为新颖。
|
||||
class BackgroundModel {
|
||||
final int maxWidth;
|
||||
final int maxHeight;
|
||||
|
||||
static const double learnRate = 0.05;
|
||||
static const double kSigma = 2.5;
|
||||
static const int minDiff = 15;
|
||||
static const int minPixels = 12;
|
||||
|
||||
Float32List? _mean;
|
||||
Float32List? _var;
|
||||
int _tw = 0;
|
||||
|
||||
BackgroundModel({this.maxWidth = 128, this.maxHeight = 128});
|
||||
|
||||
/// 后台 isolate 用原始数据接口(与 MotionDetector 同源:直接取 planes[0])。
|
||||
List<MotionRegion> updateRaw(
|
||||
Uint8List yPlane, int yStride, int width, int height) {
|
||||
final scale =
|
||||
maxWidth / width < maxHeight / height ? maxWidth / width : maxHeight / height;
|
||||
final tw = (width * scale).toInt().clamp(1, maxWidth);
|
||||
final th = (height * scale).toInt().clamp(1, maxHeight);
|
||||
if (tw == 0 || th == 0) return const [];
|
||||
|
||||
final gray = Float32List(tw * th);
|
||||
for (var oy = 0; oy < th; oy++) {
|
||||
final sy = (oy / scale).toInt().clamp(0, height - 1);
|
||||
final idx = oy * tw;
|
||||
for (var ox = 0; ox < tw; ox++) {
|
||||
final sx = (ox / scale).toInt().clamp(0, width - 1);
|
||||
gray[idx + ox] = yPlane[sy * yStride + sx].toDouble();
|
||||
}
|
||||
}
|
||||
return update(gray, tw, th);
|
||||
}
|
||||
|
||||
List<MotionRegion> update(Float32List gray, int tw, int th) {
|
||||
final n = gray.length;
|
||||
final mean = _mean;
|
||||
final variance = _var;
|
||||
if (mean == null || variance == null || mean.length != n || _tw != tw) {
|
||||
_mean = Float32List.fromList(gray);
|
||||
_var = Float32List(n);
|
||||
_tw = tw;
|
||||
return const [];
|
||||
}
|
||||
|
||||
final diff = Uint8List(n);
|
||||
var fgCount = 0;
|
||||
for (var i = 0; i < n; i++) {
|
||||
final g = gray[i];
|
||||
final m = mean[i];
|
||||
final d = (g - m).abs();
|
||||
if (d > kSigma * math.sqrt(variance[i]) + minDiff) {
|
||||
diff[i] = 1;
|
||||
fgCount++;
|
||||
// 前景像素不更新背景,避免把移动目标吸收进背景
|
||||
} else {
|
||||
// 静态像素缓慢吸收进背景,适应光照漂移
|
||||
final nm = m + learnRate * (g - m);
|
||||
mean[i] = nm;
|
||||
variance[i] =
|
||||
variance[i] + learnRate * ((g - nm) * (g - nm) - variance[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// 全屏大变化 → 相机移动/场景切换,重建背景
|
||||
if (fgCount > n ~/ 2) {
|
||||
_mean = null;
|
||||
_var = null;
|
||||
return const [];
|
||||
}
|
||||
if (fgCount < minPixels) return const [];
|
||||
return MotionAggregator.aggregate(diff, tw, th);
|
||||
}
|
||||
|
||||
/// 相机切换后重置,避免旧场景背景
|
||||
void reset() {
|
||||
_mean = null;
|
||||
_var = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
class ViewRect {
|
||||
final double left;
|
||||
final double top;
|
||||
final double right;
|
||||
final double bottom;
|
||||
|
||||
const ViewRect(this.left, this.top, this.right, this.bottom);
|
||||
|
||||
double get width => right - left;
|
||||
double get height => bottom - top;
|
||||
double get centerX => (left + right) / 2;
|
||||
double get centerY => (top + bottom) / 2;
|
||||
}
|
||||
|
||||
/// 模型归一化坐标 → 预览视图坐标(含传感器旋转与 FIT_CENTER 裁剪)。
|
||||
class CoordinateMapper {
|
||||
static ViewRect mapToView(
|
||||
double normLeft,
|
||||
double normTop,
|
||||
double normRight,
|
||||
double normBottom,
|
||||
int rotation,
|
||||
int imageW,
|
||||
int imageH,
|
||||
double viewW,
|
||||
double viewH,
|
||||
) {
|
||||
// 1) 旋转校正:图像方向 → 竖屏视图方向(归一化坐标)
|
||||
late final double x0, y0, x1, y1;
|
||||
switch (rotation) {
|
||||
case 90:
|
||||
x0 = 1 - normBottom;
|
||||
y0 = normLeft;
|
||||
x1 = 1 - normTop;
|
||||
y1 = normRight;
|
||||
case 180:
|
||||
x0 = 1 - normRight;
|
||||
y0 = 1 - normBottom;
|
||||
x1 = 1 - normLeft;
|
||||
y1 = 1 - normTop;
|
||||
case 270:
|
||||
x0 = normTop;
|
||||
y0 = 1 - normRight;
|
||||
x1 = normBottom;
|
||||
y1 = 1 - normLeft;
|
||||
default:
|
||||
x0 = normLeft;
|
||||
y0 = normTop;
|
||||
x1 = normRight;
|
||||
y1 = normBottom;
|
||||
}
|
||||
// 2) 旋转后图像在竖屏方向上的尺寸
|
||||
final portrait = rotation == 90 || rotation == 270;
|
||||
final portW = portrait ? imageH : imageW;
|
||||
final portH = portrait ? imageW : imageH;
|
||||
// 3) FIT_CENTER 缩放与居中偏移
|
||||
final scale = viewW / portW < viewH / portH
|
||||
? viewW / portW
|
||||
: viewH / portH;
|
||||
final offsetX = (viewW - portW * scale) / 2;
|
||||
final offsetY = (viewH - portH * scale) / 2;
|
||||
return ViewRect(
|
||||
x0 * portW * scale + offsetX,
|
||||
y0 * portH * scale + offsetY,
|
||||
x1 * portW * scale + offsetX,
|
||||
y1 * portH * scale + offsetY,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
class DetectionResult {
|
||||
final String label;
|
||||
final double score;
|
||||
final double left;
|
||||
final double top;
|
||||
final double right;
|
||||
final double bottom;
|
||||
|
||||
/// 轨迹已确认(多帧稳定/高分/活动确认),false = 候选,渲染为虚线
|
||||
final bool confirmed;
|
||||
|
||||
const DetectionResult({
|
||||
required this.label,
|
||||
required this.score,
|
||||
required this.left,
|
||||
required this.top,
|
||||
required this.right,
|
||||
required this.bottom,
|
||||
this.confirmed = true,
|
||||
});
|
||||
|
||||
double get width => right - left;
|
||||
double get height => bottom - top;
|
||||
double get centerX => (left + right) / 2;
|
||||
double get centerY => (top + bottom) / 2;
|
||||
|
||||
DetectionResult copyWith({
|
||||
double? score,
|
||||
double? left,
|
||||
double? top,
|
||||
double? right,
|
||||
double? bottom,
|
||||
bool? confirmed,
|
||||
}) =>
|
||||
DetectionResult(
|
||||
label: label,
|
||||
score: score ?? this.score,
|
||||
left: left ?? this.left,
|
||||
top: top ?? this.top,
|
||||
right: right ?? this.right,
|
||||
bottom: bottom ?? this.bottom,
|
||||
confirmed: confirmed ?? this.confirmed,
|
||||
);
|
||||
}
|
||||
|
||||
class MotionRegion {
|
||||
final double left;
|
||||
final double top;
|
||||
final double right;
|
||||
final double bottom;
|
||||
|
||||
const MotionRegion(this.left, this.top, this.right, this.bottom);
|
||||
|
||||
double get centerX => (left + right) / 2;
|
||||
double get centerY => (top + bottom) / 2;
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
import 'dart:async';
|
||||
import 'dart:isolate';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/foundation.dart' show debugPrint;
|
||||
import 'package:flutter/services.dart' show rootBundle;
|
||||
|
||||
import '../camera/motion_detector.dart';
|
||||
import 'background_model.dart';
|
||||
import 'detection_result.dart';
|
||||
import 'tflite_detector.dart';
|
||||
import 'visual_prior.dart';
|
||||
|
||||
/// 推理工作单元:模型加载与检测全部在后台 isolate 执行,
|
||||
/// 主 isolate 只投递帧数据、接收结果,UI 不被推理阻塞(iOS 真机卡顿根因)。
|
||||
class DetectorWorker {
|
||||
static const String modelAsset = 'assets/model.tflite';
|
||||
static const String labelsAsset = 'assets/labels.txt';
|
||||
|
||||
final Isolate _isolate;
|
||||
final ReceivePort _responses;
|
||||
|
||||
final _controlPort = Completer<SendPort>();
|
||||
final _ready = Completer<void>();
|
||||
|
||||
SendPort? _port;
|
||||
|
||||
/// 在途帧数(主 isolate 侧计数,用于丢帧)
|
||||
int _inFlight = 0;
|
||||
bool _dead = false;
|
||||
|
||||
/// 结果回调:结果 / 运动区域 / 新颖区域 / 旋转角 / 图宽 / 图高 / 处理耗时 ms
|
||||
void Function(List<DetectionResult>, List<MotionRegion>, List<MotionRegion>,
|
||||
int, int, int, int)? onResult;
|
||||
|
||||
/// 单帧处理异常回调(不影响相机流)
|
||||
void Function(String)? onError;
|
||||
|
||||
/// 最近一次创建失败的诊断原因(UI 展示用)
|
||||
static String? lastLoadError;
|
||||
|
||||
/// worker 最近上报的执行步骤(诊断用)
|
||||
static String? lastLog;
|
||||
|
||||
DetectorWorker._(this._isolate, this._responses) {
|
||||
_responses.listen(_onMessage, onDone: () {
|
||||
_dead = true;
|
||||
if (!_ready.isCompleted) {
|
||||
_ready.completeError(StateError('推理进程异常退出'));
|
||||
}
|
||||
onError?.call('推理进程异常退出');
|
||||
});
|
||||
}
|
||||
|
||||
/// 读取模型资产并启动后台推理 isolate;加载失败返回 null(App 降级为仅预览)。
|
||||
static Future<DetectorWorker?> create() async {
|
||||
try {
|
||||
final data = await rootBundle.load(modelAsset);
|
||||
final modelBytes =
|
||||
data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
|
||||
final labels = (await rootBundle.loadString(labelsAsset))
|
||||
.split('\n')
|
||||
.where((l) => l.trim().isNotEmpty)
|
||||
.toList();
|
||||
|
||||
final responses = ReceivePort();
|
||||
final isolate = await Isolate.spawn(_workerMain, responses.sendPort);
|
||||
final worker = DetectorWorker._(isolate, responses);
|
||||
|
||||
final port = await worker._controlPort.future
|
||||
.timeout(const Duration(seconds: 10),
|
||||
onTimeout: () => throw TimeoutException('worker port timeout'));
|
||||
worker._port = port;
|
||||
port.send(['load', modelBytes, labels]);
|
||||
await worker._ready.future
|
||||
.timeout(const Duration(seconds: 20), onTimeout: () {
|
||||
throw TimeoutException('model load timeout');
|
||||
});
|
||||
return worker;
|
||||
} catch (e) {
|
||||
lastLoadError = e.toString();
|
||||
debugPrint('[DetectorWorker] create failed: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 是否忙(上一帧尚未返回):忙则丢帧,避免在途积压
|
||||
bool get busy => _inFlight > 0;
|
||||
|
||||
void analyze(CameraImage image, int rotationDegrees) {
|
||||
final port = _port;
|
||||
if (port == null || _dead) return;
|
||||
_inFlight++;
|
||||
port.send([
|
||||
'frame',
|
||||
[
|
||||
image.planes.map((p) => p.bytes).toList(),
|
||||
image.planes.map((p) => p.bytesPerRow).toList(),
|
||||
image.width,
|
||||
image.height,
|
||||
image.format.group == ImageFormatGroup.bgra8888,
|
||||
rotationDegrees,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
void _onMessage(dynamic msg) {
|
||||
final list = msg as List;
|
||||
switch (list[0] as String) {
|
||||
case 'port':
|
||||
_controlPort.complete(list[1] as SendPort);
|
||||
break;
|
||||
case 'ready':
|
||||
_ready.complete();
|
||||
break;
|
||||
case 'load-error':
|
||||
_ready.completeError(StateError(
|
||||
list.length > 1 ? list[1] as String : 'model load failed'));
|
||||
break;
|
||||
case 'result':
|
||||
_inFlight--;
|
||||
final dets = (list[4] as List).map((d) {
|
||||
final v = d as List;
|
||||
return DetectionResult(
|
||||
label: v[0] as String,
|
||||
score: v[1] as double,
|
||||
left: v[2] as double,
|
||||
top: v[3] as double,
|
||||
right: v[4] as double,
|
||||
bottom: v[5] as double,
|
||||
);
|
||||
}).toList();
|
||||
final motion = (list[5] as List)
|
||||
.map((m) => m as List)
|
||||
.map((v) => MotionRegion(
|
||||
v[0] as double, v[1] as double, v[2] as double, v[3] as double))
|
||||
.toList();
|
||||
final novelty = (list[6] as List)
|
||||
.map((m) => m as List)
|
||||
.map((v) => MotionRegion(
|
||||
v[0] as double, v[1] as double, v[2] as double, v[3] as double))
|
||||
.toList();
|
||||
onResult?.call(dets, motion, novelty, list[1] as int, list[2] as int,
|
||||
list[3] as int, list[7] as int);
|
||||
break;
|
||||
case 'log':
|
||||
lastLog = list[1] as String;
|
||||
debugPrint('[DetectorWorker] $lastLog');
|
||||
break;
|
||||
case 'error':
|
||||
_inFlight--;
|
||||
onError?.call(list[1] as String);
|
||||
}
|
||||
}
|
||||
|
||||
/// 相机切换/场景变化后重置运动与背景参考
|
||||
void reset() {
|
||||
final port = _port;
|
||||
if (port == null || _dead) return;
|
||||
port.send(['reset']);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_dead = true;
|
||||
_isolate.kill(priority: Isolate.immediate);
|
||||
_responses.close();
|
||||
}
|
||||
}
|
||||
|
||||
/// 后台 isolate 入口:串行处理 load / frame / reset 命令。
|
||||
/// 所有回发必须走 [mainPort](主 isolate 的端口);control 是 worker 自己的
|
||||
/// 收件箱,往 control.sendPort 发消息等于发给自己,主 isolate 永远收不到。
|
||||
Future<void> _workerMain(SendPort mainPort) async {
|
||||
final control = ReceivePort();
|
||||
mainPort.send(['port', control.sendPort]);
|
||||
mainPort.send(['log', 'worker-start']);
|
||||
|
||||
TfliteDetector? detector;
|
||||
MotionDetector? motion;
|
||||
BackgroundModel? background;
|
||||
await for (final msg in control) {
|
||||
try {
|
||||
final list = msg as List;
|
||||
switch (list[0] as String) {
|
||||
case 'load':
|
||||
mainPort.send(['log', 'load-received']);
|
||||
try {
|
||||
detector = await TfliteDetector.fromBuffer(
|
||||
list[1] as Uint8List, (list[2] as List).cast<String>());
|
||||
if (detector == null) {
|
||||
mainPort.send(['load-error', 'fromBuffer 返回 null']);
|
||||
} else {
|
||||
mainPort.send(['log', 'fromBuffer-ok']);
|
||||
motion = MotionDetector();
|
||||
background = BackgroundModel();
|
||||
mainPort.send(['ready']);
|
||||
}
|
||||
} catch (e) {
|
||||
mainPort.send(['load-error', '$e']);
|
||||
}
|
||||
break;
|
||||
case 'frame':
|
||||
final d = detector;
|
||||
final m = motion;
|
||||
final b = background;
|
||||
if (d == null || m == null || b == null) break;
|
||||
final frame = list[1] as List;
|
||||
final planes = (frame[0] as List).cast<Uint8List>();
|
||||
final strides = (frame[1] as List).cast<int>();
|
||||
final width = frame[2] as int;
|
||||
final height = frame[3] as int;
|
||||
final isBgra = frame[4] as bool;
|
||||
final rotation = frame[5] as int;
|
||||
|
||||
final sw = Stopwatch()..start();
|
||||
var results = d.detectRaw(
|
||||
planes: planes,
|
||||
strides: strides,
|
||||
width: width,
|
||||
height: height,
|
||||
isBgra: isBgra,
|
||||
);
|
||||
// 低分野鸡框过视觉先验(颜色/位置),减少户外误报
|
||||
results = VisualPrior.filter(
|
||||
results,
|
||||
planes: planes,
|
||||
strides: strides,
|
||||
width: width,
|
||||
height: height,
|
||||
isBgra: isBgra,
|
||||
);
|
||||
final motionRegions = m.detectMotionRaw(
|
||||
planes[0], strides[0], width, height);
|
||||
final noveltyRegions =
|
||||
b.updateRaw(planes[0], strides[0], width, height);
|
||||
sw.stop();
|
||||
|
||||
mainPort.send([
|
||||
'result',
|
||||
rotation,
|
||||
width,
|
||||
height,
|
||||
results
|
||||
.map((r) =>
|
||||
[r.label, r.score, r.left, r.top, r.right, r.bottom])
|
||||
.toList(),
|
||||
motionRegions
|
||||
.map((mr) => [mr.left, mr.top, mr.right, mr.bottom])
|
||||
.toList(),
|
||||
noveltyRegions
|
||||
.map((mr) => [mr.left, mr.top, mr.right, mr.bottom])
|
||||
.toList(),
|
||||
sw.elapsedMilliseconds,
|
||||
]);
|
||||
break;
|
||||
case 'reset':
|
||||
motion?.reset();
|
||||
background?.reset();
|
||||
}
|
||||
} catch (e) {
|
||||
mainPort.send(['error', '$e']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'detection_result.dart';
|
||||
|
||||
/// 帧差运动聚合:每像素 0/1 差分掩码 → 8x8 分块统计 → 连通块聚合为运动区域。
|
||||
class MotionAggregator {
|
||||
static const int blockGrid = 8;
|
||||
static const double blockActiveRatio = 0.30;
|
||||
static const int maxRegions = 3;
|
||||
static const int diffThreshold = 25;
|
||||
|
||||
static List<MotionRegion> aggregate(List<int> diff, int width, int height) {
|
||||
final bw = width ~/ blockGrid;
|
||||
final bh = height ~/ blockGrid;
|
||||
if (bw == 0 || bh == 0) return const [];
|
||||
|
||||
final active = List<bool>.filled(blockGrid * blockGrid, false);
|
||||
for (var by = 0; by < blockGrid; by++) {
|
||||
for (var bx = 0; bx < blockGrid; bx++) {
|
||||
final blockW = bx == blockGrid - 1 ? width - bx * bw : bw;
|
||||
final blockH = by == blockGrid - 1 ? height - by * bh : bh;
|
||||
var motion = 0;
|
||||
for (var y = by * bh; y < by * bh + blockH; y++) {
|
||||
var idx = y * width + bx * bw;
|
||||
for (var x = 0; x < blockW; x++) {
|
||||
motion += diff[idx + x];
|
||||
}
|
||||
idx += width;
|
||||
}
|
||||
active[by * blockGrid + bx] =
|
||||
motion > blockW * blockH * blockActiveRatio;
|
||||
}
|
||||
}
|
||||
|
||||
final regions = <MotionRegion>[];
|
||||
final visited = List<bool>.filled(active.length, false);
|
||||
for (var i = 0; i < active.length; i++) {
|
||||
if (!active[i] || visited[i]) continue;
|
||||
var minX = blockGrid, minY = blockGrid, maxX = -1, maxY = -1;
|
||||
final stack = <int>[i];
|
||||
visited[i] = true;
|
||||
while (stack.isNotEmpty) {
|
||||
final cur = stack.removeLast();
|
||||
final bx = cur % blockGrid;
|
||||
final by = cur ~/ blockGrid;
|
||||
if (bx < minX) minX = bx;
|
||||
if (bx > maxX) maxX = bx;
|
||||
if (by < minY) minY = by;
|
||||
if (by > maxY) maxY = by;
|
||||
for (final nb in neighbors(cur)) {
|
||||
if (active[nb] && !visited[nb]) {
|
||||
visited[nb] = true;
|
||||
stack.add(nb);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (maxX - minX > 3 || maxY - minY > 3) continue; // 全屏噪声过滤
|
||||
regions.add(MotionRegion(
|
||||
minX * bw / width,
|
||||
minY * bh / height,
|
||||
math.min((maxX + 1) * bw, width) / width,
|
||||
math.min((maxY + 1) * bh, height) / height,
|
||||
));
|
||||
if (regions.length >= maxRegions) break;
|
||||
}
|
||||
return regions;
|
||||
}
|
||||
|
||||
static List<int> neighbors(int i) {
|
||||
final bx = i % blockGrid;
|
||||
final by = i ~/ blockGrid;
|
||||
final list = <int>[];
|
||||
if (bx > 0) list.add(i - 1);
|
||||
if (bx < blockGrid - 1) list.add(i + 1);
|
||||
if (by > 0) list.add(i - blockGrid);
|
||||
if (by < blockGrid - 1) list.add(i + blockGrid);
|
||||
return list;
|
||||
}
|
||||
|
||||
/// 检测框中心是否落在运动区域内(用于置信度提升判定)
|
||||
static bool centerInRegion(DetectionResult box, MotionRegion region) =>
|
||||
box.centerX >= region.left &&
|
||||
box.centerX <= region.right &&
|
||||
box.centerY >= region.top &&
|
||||
box.centerY <= region.bottom;
|
||||
|
||||
/// 帧差掩码:|g - prev| > threshold → 1
|
||||
static List<int> diffMask(List<int> gray, List<int> prev,
|
||||
[int threshold = diffThreshold]) {
|
||||
final diff = List<int>.filled(gray.length, 0);
|
||||
for (var i = 0; i < gray.length; i++) {
|
||||
diff[i] = (gray[i] - prev[i]).abs() > threshold ? 1 : 0;
|
||||
}
|
||||
return diff;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import 'detection_result.dart';
|
||||
|
||||
double iou(DetectionResult a, DetectionResult b) {
|
||||
final x0 = a.left > b.left ? a.left : b.left;
|
||||
final y0 = a.top > b.top ? a.top : b.top;
|
||||
final x1 = a.right < b.right ? a.right : b.right;
|
||||
final y1 = a.bottom < b.bottom ? a.bottom : b.bottom;
|
||||
if (x1 <= x0 || y1 <= y0) return 0;
|
||||
final inter = (x1 - x0) * (y1 - y0);
|
||||
final union = a.width * a.height + b.width * b.height - inter;
|
||||
return union <= 0 ? 0 : inter / union;
|
||||
}
|
||||
|
||||
List<DetectionResult> nms(List<DetectionResult> boxes, double iouThreshold) {
|
||||
final sorted = [...boxes]..sort((a, b) => b.score.compareTo(a.score));
|
||||
final kept = <DetectionResult>[];
|
||||
for (final b in sorted) {
|
||||
if (!kept.any((k) => iou(b, k) > iouThreshold)) kept.add(b);
|
||||
}
|
||||
return kept;
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:tflite_flutter/tflite_flutter.dart';
|
||||
|
||||
import 'detection_result.dart';
|
||||
import 'nms.dart';
|
||||
|
||||
/// YOLOv8n 端侧推理实现(对应 Kotlin TFLiteDetector)。
|
||||
/// 模型输出布局(ultralytics litert 导出):[1, 4 + nc, anchors],
|
||||
/// cx/cy/w/h 已归一化,类别得分已过 sigmoid;按 out[dim][anchor] 索引。
|
||||
/// 输入为 NCHW [1, 3, 704, 704](litert 导出保留 torch 布局)。
|
||||
class TfliteDetector {
|
||||
static const int inputSize = 704;
|
||||
// 野鸡数据置信度普遍偏低(0.1~0.2 量级),保留低分池供运动检测提升
|
||||
static const double minScore = 0.10;
|
||||
static const double iouThreshold = 0.45;
|
||||
static const int maxDetections = 20;
|
||||
static const String modelAsset = 'assets/model.tflite';
|
||||
static const String labelsAsset = 'assets/labels.txt';
|
||||
|
||||
final Interpreter _interpreter;
|
||||
final List<String> _labels;
|
||||
final int _numClasses;
|
||||
final int _numAnchors;
|
||||
|
||||
final Float32List _input =
|
||||
Float32List(1 * inputSize * inputSize * 3);
|
||||
|
||||
/// 输出按模型形状 [1, 4+nc, anchors] 的嵌套 List 组织,
|
||||
/// run() 要求输出对象形状与模型完全一致(扁平 List 会被拒)。
|
||||
final List<List<List<double>>> _output;
|
||||
|
||||
TfliteDetector._(this._interpreter, this._labels, this._numClasses,
|
||||
this._numAnchors, this._output);
|
||||
|
||||
/// 模型缺失或加载失败返回 null(App 降级为仅预览)。
|
||||
/// 在后台 isolate 内调用(模型字节由主 isolate 读取后传入)。
|
||||
static Future<TfliteDetector?> fromBuffer(
|
||||
Uint8List bytes, List<String> labels) async {
|
||||
try {
|
||||
final interpreter = Interpreter.fromBuffer(
|
||||
bytes,
|
||||
options: InterpreterOptions()..threads = 4,
|
||||
);
|
||||
return TfliteDetector._fromModel(interpreter, labels);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 输出布局 [1, 4+nc, anchors] 取自模型本身,类别数不与 labels 文件长度耦合。
|
||||
factory TfliteDetector._fromModel(
|
||||
Interpreter interpreter, List<String> labels) {
|
||||
final shape = interpreter.getOutputTensor(0).shape;
|
||||
final numClasses =
|
||||
shape.length >= 3 && shape[1] > 4 ? shape[1] - 4 : labels.length;
|
||||
final numAnchors = shape.length >= 3 && shape[2] > 0 ? shape[2] : 2100;
|
||||
final output = List.generate(
|
||||
1,
|
||||
(_) => List.generate(
|
||||
numClasses + 4,
|
||||
(_) => List<double>.filled(numAnchors, 0),
|
||||
),
|
||||
);
|
||||
return TfliteDetector._(
|
||||
interpreter, labels, numClasses, numAnchors, output);
|
||||
}
|
||||
|
||||
/// 原始数据接口(后台 isolate 用,不依赖 CameraImage)。
|
||||
/// 输出坐标统一反算为原图归一化空间(与 MotionDetector 一致),
|
||||
/// 否则 CENTER_CROP 裁剪偏移会让检测框系统性偏移。
|
||||
List<DetectionResult> detectRaw({
|
||||
required List<Uint8List> planes,
|
||||
required List<int> strides,
|
||||
required int width,
|
||||
required int height,
|
||||
required bool isBgra,
|
||||
}) {
|
||||
preprocess(
|
||||
planes: planes,
|
||||
strides: strides,
|
||||
width: width,
|
||||
height: height,
|
||||
isBgra: isBgra);
|
||||
// 传原始字节视图而非 Float32List:tflite_flutter 会对非 ByteBuffer/Uint8List
|
||||
// 输入调用 resizeInputTensor(1 维 [1486848]),使 node 0 TRANSPOSE prepare 失败
|
||||
_interpreter.run(_input.buffer.asUint8List(), _output);
|
||||
final dets = postprocess();
|
||||
// 反算与 preprocess 的 scale/dx/dy 公式一致(704 输入空间 → 原图归一化)
|
||||
final scale = inputSize / width < inputSize / height
|
||||
? inputSize / width
|
||||
: inputSize / height;
|
||||
final dx = (inputSize - width * scale) / 2;
|
||||
final dy = (inputSize - height * scale) / 2;
|
||||
if (dx == 0 && dy == 0) return dets;
|
||||
return dets
|
||||
.map((r) => r.copyWith(
|
||||
left: (r.left * inputSize - dx) / (width * scale),
|
||||
right: (r.right * inputSize - dx) / (width * scale),
|
||||
top: (r.top * inputSize - dy) / (height * scale),
|
||||
bottom: (r.bottom * inputSize - dy) / (height * scale),
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// 按像素格式分派:iOS bgra8888 单平面 / Android yuv420 多平面。
|
||||
void preprocess({
|
||||
required List<Uint8List> planes,
|
||||
required List<int> strides,
|
||||
required int width,
|
||||
required int height,
|
||||
required bool isBgra,
|
||||
}) {
|
||||
if (isBgra) {
|
||||
_preprocessBgra(planes[0], strides[0], width, height);
|
||||
} else {
|
||||
_preprocessYuv(planes, strides, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
/// BGRA8888 单平面(iOS):每像素 4 字节 [b,g,r,a],双线性采样,
|
||||
/// letterbox(等比缩到长边 704,短边黑边补 0,与 YOLO 训练一致)。
|
||||
void _preprocessBgra(Uint8List src, int stride, int srcW, int srcH) {
|
||||
final plane = inputSize * inputSize;
|
||||
final scale = inputSize / srcW < inputSize / srcH
|
||||
? inputSize / srcW
|
||||
: inputSize / srcH;
|
||||
final dx = (inputSize - srcW * scale) / 2;
|
||||
final dy = (inputSize - srcH * scale) / 2;
|
||||
|
||||
for (var oy = 0; oy < inputSize; oy++) {
|
||||
final syf = (oy - dy) / scale;
|
||||
if (syf < 0 || syf >= srcH) {
|
||||
for (var ox = 0; ox < inputSize; ox++) {
|
||||
final p = oy * inputSize + ox;
|
||||
_input[p] = 0;
|
||||
_input[p + plane] = 0;
|
||||
_input[p + 2 * plane] = 0;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for (var ox = 0; ox < inputSize; ox++) {
|
||||
final p = oy * inputSize + ox;
|
||||
final sxf = (ox - dx) / scale;
|
||||
if (sxf < 0 || sxf >= srcW) {
|
||||
_input[p] = 0;
|
||||
_input[p + plane] = 0;
|
||||
_input[p + 2 * plane] = 0;
|
||||
continue;
|
||||
}
|
||||
final x0 = sxf.floor(), y0 = syf.floor();
|
||||
final x1 = x0 < srcW - 1 ? x0 + 1 : x0;
|
||||
final y1 = y0 < srcH - 1 ? y0 + 1 : y0;
|
||||
final fx = sxf - x0, fy = syf - y0;
|
||||
|
||||
// BGRA 字节序:+0 B、+1 G、+2 R、+3 A
|
||||
final i00 = y0 * stride + x0 * 4;
|
||||
final i10 = y0 * stride + x1 * 4;
|
||||
final i01 = y1 * stride + x0 * 4;
|
||||
final i11 = y1 * stride + x1 * 4;
|
||||
final r00 = src[i00 + 2].toDouble();
|
||||
final g00 = src[i00 + 1].toDouble();
|
||||
final b00 = src[i00].toDouble();
|
||||
final r10 = src[i10 + 2].toDouble();
|
||||
final g10 = src[i10 + 1].toDouble();
|
||||
final b10 = src[i10].toDouble();
|
||||
final r01 = src[i01 + 2].toDouble();
|
||||
final g01 = src[i01 + 1].toDouble();
|
||||
final b01 = src[i01].toDouble();
|
||||
final r11 = src[i11 + 2].toDouble();
|
||||
final g11 = src[i11 + 1].toDouble();
|
||||
final b11 = src[i11].toDouble();
|
||||
|
||||
_input[p] = _bl(r00, r10, r01, r11, fx, fy) / 255.0;
|
||||
_input[p + plane] = _bl(g00, g10, g01, g11, fx, fy) / 255.0;
|
||||
_input[p + 2 * plane] = _bl(b00, b10, b01, b11, fx, fy) / 255.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// letterbox 缩放 + YUV → RGB 归一化 0~1(NCHW),双线性采样。
|
||||
/// 兼容 NV12(iOS 双平面,UV 交错)与 I420(Android 三平面)。
|
||||
void _preprocessYuv(
|
||||
List<Uint8List> planes, List<int> strides, int srcW, int srcH) {
|
||||
final plane = inputSize * inputSize;
|
||||
final y = planes[0];
|
||||
final nv12 = planes.length == 2;
|
||||
final uv = nv12 ? planes[1] : null;
|
||||
final u = nv12 ? null : planes[1];
|
||||
final v = nv12 ? null : planes[2];
|
||||
final yStride = strides[0];
|
||||
final uvStride = strides[1];
|
||||
|
||||
// U/V 平面采样(nv12:偶位 U 奇位 V;i420:三平面分离)
|
||||
double uAt(int x, int y) => nv12
|
||||
? uv![y * uvStride + x * 2] - 128.0
|
||||
: u![y * uvStride + x] - 128.0;
|
||||
double vAt(int x, int y) => nv12
|
||||
? uv![y * uvStride + x * 2 + 1] - 128.0
|
||||
: v![y * uvStride + x] - 128.0;
|
||||
|
||||
final scale = inputSize / srcW < inputSize / srcH
|
||||
? inputSize / srcW
|
||||
: inputSize / srcH;
|
||||
final dx = (inputSize - srcW * scale) / 2;
|
||||
final dy = (inputSize - srcH * scale) / 2;
|
||||
|
||||
for (var oy = 0; oy < inputSize; oy++) {
|
||||
final syf = (oy - dy) / scale;
|
||||
if (syf < 0 || syf >= srcH) {
|
||||
for (var ox = 0; ox < inputSize; ox++) {
|
||||
final p = oy * inputSize + ox;
|
||||
_input[p] = 0;
|
||||
_input[p + plane] = 0;
|
||||
_input[p + 2 * plane] = 0;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for (var ox = 0; ox < inputSize; ox++) {
|
||||
final p = oy * inputSize + ox;
|
||||
final sxf = (ox - dx) / scale;
|
||||
if (sxf < 0 || sxf >= srcW) {
|
||||
_input[p] = 0;
|
||||
_input[p + plane] = 0;
|
||||
_input[p + 2 * plane] = 0;
|
||||
continue;
|
||||
}
|
||||
final x0 = sxf.floor(), y0 = syf.floor();
|
||||
final x1 = x0 < srcW - 1 ? x0 + 1 : x0;
|
||||
final y1 = y0 < srcH - 1 ? y0 + 1 : y0;
|
||||
final fx = sxf - x0, fy = syf - y0;
|
||||
|
||||
// Y 双线性
|
||||
final y00 = y[y0 * yStride + x0].toDouble();
|
||||
final y10 = y[y0 * yStride + x1].toDouble();
|
||||
final y01 = y[y1 * yStride + x0].toDouble();
|
||||
final y11 = y[y1 * yStride + x1].toDouble();
|
||||
final yy = _bl(y00, y10, y01, y11, fx, fy);
|
||||
|
||||
// U/V 双线性(4:2:0 半分辨率,按像素坐标定位后除 2)
|
||||
final maxUx = srcW ~/ 2 - 1;
|
||||
final maxUy = srcH ~/ 2 - 1;
|
||||
final ux0 = (x0 ~/ 2).clamp(0, maxUx).toInt();
|
||||
final uy0 = (y0 ~/ 2).clamp(0, maxUy).toInt();
|
||||
final ux1 = (x1 ~/ 2).clamp(0, maxUx).toInt();
|
||||
final uy1 = (y1 ~/ 2).clamp(0, maxUy).toInt();
|
||||
final u00 = uAt(ux0, uy0);
|
||||
final u10 = uAt(ux1, uy0);
|
||||
final u01 = uAt(ux0, uy1);
|
||||
final u11 = uAt(ux1, uy1);
|
||||
final uu = _bl(u00, u10, u01, u11, fx, fy);
|
||||
|
||||
final v00 = vAt(ux0, uy0);
|
||||
final v10 = vAt(ux1, uy0);
|
||||
final v01 = vAt(ux0, uy1);
|
||||
final v11 = vAt(ux1, uy1);
|
||||
final vv = _bl(v00, v10, v01, v11, fx, fy);
|
||||
|
||||
// 有限范围展开(VideoRange Y 16~235,Cb/Cr 16~240)
|
||||
final yr = (yy - 16.0) * (255.0 / 219.0);
|
||||
final un = uu * (255.0 / 224.0);
|
||||
final vn = vv * (255.0 / 224.0);
|
||||
|
||||
// NCHW:r/g/b 分平面存储
|
||||
_input[p] = (yr + 1.402 * vn) / 255.0;
|
||||
_input[p + plane] = (yr - 0.344136 * un - 0.714136 * vn) / 255.0;
|
||||
_input[p + 2 * plane] = (yr + 1.772 * un) / 255.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static double _bl(double a, double b, double c, double d, double fx,
|
||||
double fy) =>
|
||||
(1 - fx) * (1 - fy) * a + fx * (1 - fy) * b +
|
||||
(1 - fx) * fy * c + fx * fy * d;
|
||||
|
||||
List<DetectionResult> postprocess() {
|
||||
final out = _output[0];
|
||||
final boxes = <DetectionResult>[];
|
||||
for (var a = 0; a < _numAnchors; a++) {
|
||||
final cx = out[0][a];
|
||||
final cy = out[1][a];
|
||||
final w = out[2][a];
|
||||
final h = out[3][a];
|
||||
var bestCls = 0;
|
||||
var bestScore = 0.0;
|
||||
for (var c = 0; c < _numClasses; c++) {
|
||||
final s = out[4 + c][a];
|
||||
if (s > bestScore) {
|
||||
bestScore = s;
|
||||
bestCls = c;
|
||||
}
|
||||
}
|
||||
final label =
|
||||
bestCls < _labels.length ? _labels[bestCls] : 'unknown';
|
||||
// 低分池保留,供运动检测提升显示
|
||||
if (bestScore < minScore) continue;
|
||||
boxes.add(DetectionResult(
|
||||
label: label,
|
||||
score: bestScore,
|
||||
left: (cx - w / 2).clamp(0.0, 1.0),
|
||||
top: (cy - h / 2).clamp(0.0, 1.0),
|
||||
right: (cx + w / 2).clamp(0.0, 1.0),
|
||||
bottom: (cy + h / 2).clamp(0.0, 1.0),
|
||||
));
|
||||
}
|
||||
final kept = nms(boxes, iouThreshold);
|
||||
return kept.take(maxDetections).toList();
|
||||
}
|
||||
|
||||
void dispose() => _interpreter.close();
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import 'dart:math' as math;
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'detection_result.dart';
|
||||
|
||||
/// 运行时视觉先验:对低置信度野鸡框做多线索过滤,降低户外误报。
|
||||
///
|
||||
/// 仅对 score < [maxScore](0.35)的 pheasant 框生效;高分框与
|
||||
/// suspect(生境预警)不参与过滤,避免误杀。
|
||||
///
|
||||
/// 线索:
|
||||
/// - 颜色:绿色主导(草/叶)、蓝色主导(天空/水)、平坦低饱和(键盘/石头/文字)
|
||||
/// - 位置:中心在画面上部 15%(天空区)——野鸡是地栖动物,不会出现在天空
|
||||
///
|
||||
/// 采样在原始 planes 上进行(后台 isolate 内,不依赖 UI 线程)。
|
||||
class VisualPrior {
|
||||
static const double maxScore = 0.35;
|
||||
static const double skyTopRatio = 0.15;
|
||||
|
||||
// 颜色判定阈值(与 tflite_detector 的 YUV 有限范围展开一致)
|
||||
static const double greenDiff = 20;
|
||||
static const double blueDiff = 10;
|
||||
static const double flatRange = 10;
|
||||
|
||||
// 采样点中满足条件的比例超过即拒绝
|
||||
static const double greenRatio = 0.5;
|
||||
static const double blueRatio = 0.4;
|
||||
static const double flatRatio = 0.6;
|
||||
|
||||
static List<DetectionResult> filter(
|
||||
List<DetectionResult> results, {
|
||||
required List<Uint8List> planes,
|
||||
required List<int> strides,
|
||||
required int width,
|
||||
required int height,
|
||||
required bool isBgra,
|
||||
}) {
|
||||
if (results.isEmpty || width <= 0 || height <= 0) return results;
|
||||
final kept = <DetectionResult>[];
|
||||
for (final r in results) {
|
||||
final lowConfPheasant = r.label == 'pheasant' && r.score < maxScore;
|
||||
if (lowConfPheasant && _reject(r, planes, strides, width, height, isBgra)) {
|
||||
continue;
|
||||
}
|
||||
kept.add(r);
|
||||
}
|
||||
return kept;
|
||||
}
|
||||
|
||||
static bool _reject(DetectionResult r, List<Uint8List> planes,
|
||||
List<int> strides, int width, int height, bool isBgra) {
|
||||
// 位置线索:detectRaw 输出为图像坐标系,centerY 直接可判天空区
|
||||
if (r.centerY < skyTopRatio) return true;
|
||||
|
||||
// 颜色线索:框中心 ±20% 区域 5×5 采样(小框采样点重合也没关系)
|
||||
final cx = (r.centerX * width).round().clamp(0, width - 1).toInt();
|
||||
final cy = (r.centerY * height).round().clamp(0, height - 1).toInt();
|
||||
final halfW = math.max(1.0, r.width * width * 0.2);
|
||||
final halfH = math.max(1.0, r.height * height * 0.2);
|
||||
|
||||
var green = 0, blue = 0, flat = 0, total = 0;
|
||||
for (var gy = -2; gy <= 2; gy++) {
|
||||
for (var gx = -2; gx <= 2; gx++) {
|
||||
final px = (cx + gx * halfW / 2).round().clamp(0, width - 1).toInt();
|
||||
final py = (cy + gy * halfH / 2).round().clamp(0, height - 1).toInt();
|
||||
final (r_, g_, b_) = _pixel(planes, strides, px, py, width, height, isBgra);
|
||||
total++;
|
||||
final mn = math.min(r_, math.min(g_, b_));
|
||||
final mx = math.max(r_, math.max(g_, b_));
|
||||
if (g_ - r_ > greenDiff && g_ - b_ > greenDiff) green++;
|
||||
if (b_ > r_ + blueDiff) blue++;
|
||||
if (mx - mn < flatRange) flat++;
|
||||
}
|
||||
}
|
||||
if (total == 0) return false;
|
||||
if (green / total > greenRatio) return true;
|
||||
if (blue / total > blueRatio) return true;
|
||||
if (flat / total > flatRatio) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// 读取单像素 RGB(0~255)。
|
||||
/// BGRA 单平面:每像素 4 字节 [b,g,r,a];
|
||||
/// YUV:y 平面 + 4:2:0 半分辨率 U/V(NV12 交错或 I420 分离)。
|
||||
static (double, double, double) _pixel(List<Uint8List> planes,
|
||||
List<int> strides, int x, int y, int width, int height, bool isBgra) {
|
||||
if (isBgra) {
|
||||
final src = planes[0];
|
||||
final i = y * strides[0] + x * 4;
|
||||
return (src[i + 2].toDouble(), src[i + 1].toDouble(), src[i].toDouble());
|
||||
}
|
||||
final yy =
|
||||
(planes[0][y * strides[0] + x] - 16.0) * (255.0 / 219.0);
|
||||
final nv12 = planes.length == 2;
|
||||
final ux = (x ~/ 2).clamp(0, width ~/ 2 - 1).toInt();
|
||||
final uy = (y ~/ 2).clamp(0, height ~/ 2 - 1).toInt();
|
||||
final uvStride = strides[1];
|
||||
final un = ((nv12
|
||||
? planes[1][uy * uvStride + ux * 2].toDouble()
|
||||
: planes[1][uy * uvStride + ux].toDouble()) -
|
||||
128.0) *
|
||||
(255.0 / 224.0);
|
||||
final vn = ((nv12
|
||||
? planes[1][uy * uvStride + ux * 2 + 1].toDouble()
|
||||
: planes[2][uy * uvStride + ux].toDouble()) -
|
||||
128.0) *
|
||||
(255.0 / 224.0);
|
||||
final r = yy + 1.402 * vn;
|
||||
final g = yy - 0.344136 * un - 0.714136 * vn;
|
||||
final b = yy + 1.772 * un;
|
||||
return (r, g, b);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../auth/session_store.dart';
|
||||
import 'home_view_model.dart';
|
||||
|
||||
/// 主界面:当前账号到期时间 + 搜索按钮(强制服务端校验后进相机)+ 充值入口
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
|
||||
@override
|
||||
State<HomeScreen> createState() => _HomeScreenState();
|
||||
}
|
||||
|
||||
class _HomeScreenState extends State<HomeScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.read<HomeViewModel>().refresh();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _onSearch() async {
|
||||
final vm = context.read<HomeViewModel>();
|
||||
final allowed = await vm.verifyForCamera();
|
||||
if (!mounted) return;
|
||||
if (vm.sessionExpired) {
|
||||
_toLogin();
|
||||
return;
|
||||
}
|
||||
if (!allowed) {
|
||||
if (vm.error != null) {
|
||||
// 网络/服务端异常:无法确认授权状态,提示错误
|
||||
_showBlocked(vm.error);
|
||||
return;
|
||||
}
|
||||
// 服务端确认无有效授权:直接进充值页
|
||||
await _onRecharge();
|
||||
return;
|
||||
}
|
||||
await Navigator.of(context).pushNamed('/camera');
|
||||
if (mounted) vm.refresh();
|
||||
}
|
||||
|
||||
Future<void> _onRecharge() async {
|
||||
await Navigator.of(context).pushNamed('/paywall');
|
||||
if (mounted) context.read<HomeViewModel>().refresh();
|
||||
}
|
||||
|
||||
Future<void> _toLogin() async {
|
||||
final container = context.read<SessionStore>();
|
||||
await container.clear();
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pushReplacementNamed('/login');
|
||||
}
|
||||
|
||||
void _showBlocked(String? error) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('授权不可用'),
|
||||
content: Text(error ?? '授权已过期,请充值后续费'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.of(ctx).pop();
|
||||
_onRecharge();
|
||||
},
|
||||
child: const Text('去充值'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final vm = context.watch<HomeViewModel>();
|
||||
final license = vm.license;
|
||||
final active = license?.isActive ?? false;
|
||||
final statusText = vm.loading
|
||||
? '加载中…'
|
||||
: (license == null || license.expiresAt == null
|
||||
? '未充值'
|
||||
: (active
|
||||
? '有效至 ${_fmt(license.expiresAt!)}'
|
||||
: '已过期(${_fmt(license.expiresAt!)})'));
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('视野'),
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: '退出登录',
|
||||
icon: const Icon(Icons.logout),
|
||||
onPressed: _toLogin,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
_StatusCard(
|
||||
licenseText: statusText,
|
||||
active: active,
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
SizedBox(
|
||||
height: 88,
|
||||
child: FilledButton.icon(
|
||||
onPressed: vm.verifying ? null : _onSearch,
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Colors.green,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(44),
|
||||
),
|
||||
),
|
||||
icon: vm.verifying
|
||||
? const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2, color: Colors.white),
|
||||
)
|
||||
: const Icon(Icons.visibility, size: 32),
|
||||
label: Text(
|
||||
vm.verifying ? '正在校验授权…' : '打开视野',
|
||||
style: const TextStyle(
|
||||
fontSize: 22, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
height: 52,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: _onRecharge,
|
||||
icon: const Icon(Icons.payment),
|
||||
label: const Text('充值', style: TextStyle(fontSize: 16)),
|
||||
),
|
||||
),
|
||||
if (vm.error != null && vm.license != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
vm.error!,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static String _fmt(DateTime t) {
|
||||
String p(int n) => n.toString().padLeft(2, '0');
|
||||
return '${t.year}-${p(t.month)}-${p(t.day)} ${p(t.hour)}:${p(t.minute)}';
|
||||
}
|
||||
}
|
||||
|
||||
class _StatusCard extends StatelessWidget {
|
||||
final String licenseText;
|
||||
final bool active;
|
||||
|
||||
const _StatusCard({
|
||||
required this.licenseText,
|
||||
required this.active,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
active ? Icons.verified_user : Icons.error_outline,
|
||||
color: active ? Colors.green : Colors.orange,
|
||||
size: 36,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('当前账户',
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade600, fontSize: 13)),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
licenseText,
|
||||
style: const TextStyle(
|
||||
fontSize: 18, fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../payment/license_service.dart';
|
||||
import '../payment/models.dart';
|
||||
import '../payment/order_api.dart';
|
||||
|
||||
/// 主界面状态:到期时间展示 + 搜索入口强制校验 + 退出登录
|
||||
class HomeViewModel extends ChangeNotifier {
|
||||
final LicenseService licenseService;
|
||||
|
||||
LicenseStatus? _license;
|
||||
bool _loading = true;
|
||||
bool _verifying = false;
|
||||
String? _error;
|
||||
bool _sessionExpired = false;
|
||||
|
||||
LicenseStatus? get license => _license;
|
||||
bool get loading => _loading;
|
||||
bool get verifying => _verifying;
|
||||
String? get error => _error;
|
||||
bool get sessionExpired => _sessionExpired;
|
||||
|
||||
HomeViewModel({required this.licenseService});
|
||||
|
||||
/// 进入主界面/支付返回后刷新(本地缓存优先,服务端为准)
|
||||
Future<void> refresh() async {
|
||||
_loading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
try {
|
||||
_license = await licenseService.check();
|
||||
} catch (_) {
|
||||
// check 内部已兜底,这里仅防御
|
||||
} finally {
|
||||
_loading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// 搜索按钮:强制服务端校验授权,active 才允许进入相机。
|
||||
/// 网络失败/会话失效一律不放行;返回值表示是否可进入。
|
||||
Future<bool> verifyForCamera() async {
|
||||
_verifying = true;
|
||||
_sessionExpired = false;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
try {
|
||||
final license = await licenseService.verifyServer();
|
||||
_license = license;
|
||||
return license.isActive;
|
||||
} on SessionExpiredException {
|
||||
_sessionExpired = true;
|
||||
return false;
|
||||
} on OrderApiException catch (e) {
|
||||
_error = e.message;
|
||||
return false;
|
||||
} finally {
|
||||
_verifying = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void clearSessionExpired() {
|
||||
_sessionExpired = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'package:fluwx/fluwx.dart' as fluwx;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tobias/tobias.dart' as tobias;
|
||||
|
||||
import 'app.dart';
|
||||
import 'config/app_config.dart';
|
||||
import 'container.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// 注册微信/支付宝 SDK(AppID 占位,接入真实商户后替换 app_config.dart)
|
||||
try {
|
||||
await fluwx.Fluwx().registerApi(
|
||||
appId: AppConfig.wechatAppId,
|
||||
universalLink: AppConfig.wechatUniversalLink,
|
||||
);
|
||||
} catch (_) {}
|
||||
try {
|
||||
await tobias.Tobias().registerApp(
|
||||
AppConfig.alipayAppId,
|
||||
universalLink: AppConfig.alipayUniversalLink,
|
||||
);
|
||||
} catch (_) {}
|
||||
|
||||
runApp(ObserverApp(container: AppContainer()));
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
import '../auth/session_store.dart';
|
||||
import 'models.dart';
|
||||
import 'order_api.dart';
|
||||
|
||||
/// 授权管理:本地缓存用于主界面展示到期时间;识别入口强制服务端校验。
|
||||
/// 缓存按手机号隔离(键含账号),切换账号不会读到上一账号的到期时间。
|
||||
class LicenseService {
|
||||
static const _storage = FlutterSecureStorage();
|
||||
|
||||
final OrderApi orderApi;
|
||||
final SessionStore sessionStore;
|
||||
|
||||
LicenseService({required this.orderApi, required this.sessionStore});
|
||||
|
||||
/// 主界面展示/启动加载:先读本账号本地缓存,有效则直接用;
|
||||
/// 无效或缺失时询问服务端(网络失败按缓存兜底,未登录按过期处理)。
|
||||
Future<LicenseStatus> check() async {
|
||||
final phone = await sessionStore.readPhone();
|
||||
if (phone == null) return const LicenseStatus(active: false);
|
||||
|
||||
final cached = await _readCache(phone);
|
||||
final now = DateTime.now();
|
||||
if (cached != null && cached.expiresAt!.isAfter(now)) return cached;
|
||||
|
||||
if (await sessionStore.readToken() == null) {
|
||||
return const LicenseStatus(active: false);
|
||||
}
|
||||
try {
|
||||
final remote = await orderApi.fetchLicense();
|
||||
try {
|
||||
if (remote.active && remote.expiresAt != null) {
|
||||
await _writeCache(phone, remote.expiresAt!);
|
||||
} else if (!remote.active) {
|
||||
await _clearCache(phone);
|
||||
}
|
||||
} catch (_) {
|
||||
// 本地缓存不可用不影响授权状态展示
|
||||
}
|
||||
return remote;
|
||||
} on OrderApiException {
|
||||
return cached ?? const LicenseStatus(active: false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 识别入口强制校验:必须走服务端且 active 才放行,失败即抛错(不进相机)
|
||||
Future<LicenseStatus> verifyServer() async {
|
||||
if (await sessionStore.readToken() == null) {
|
||||
throw const SessionExpiredException();
|
||||
}
|
||||
return orderApi.fetchLicense();
|
||||
}
|
||||
|
||||
/// 支付成功后立即刷新(清缓存强制走服务端,避免旧授权干扰)
|
||||
Future<LicenseStatus> refresh() async {
|
||||
final phone = await sessionStore.readPhone();
|
||||
if (phone != null) await _clearCache(phone);
|
||||
return orderApi.fetchLicense();
|
||||
}
|
||||
|
||||
Future<LicenseStatus?> _readCache(String phone) async {
|
||||
try {
|
||||
final raw = await _storage.read(key: _cacheKey(phone));
|
||||
final t = raw == null ? null : DateTime.tryParse(raw);
|
||||
if (t == null || !t.isAfter(DateTime.now())) return null;
|
||||
return LicenseStatus(active: true, expiresAt: t);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _writeCache(String phone, DateTime expiresAt) =>
|
||||
_storage.write(key: _cacheKey(phone), value: expiresAt.toIso8601String());
|
||||
|
||||
Future<void> _clearCache(String phone) => _storage.delete(key: _cacheKey(phone));
|
||||
|
||||
static String _cacheKey(String phone) => 'license_expires_at:$phone';
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/// 充值套餐(来自后端 GET /api/v1/plans,价格以服务端 config.yml 为准)
|
||||
class Plan {
|
||||
final String id;
|
||||
final int days;
|
||||
final int priceYuan;
|
||||
|
||||
const Plan({
|
||||
required this.id,
|
||||
required this.days,
|
||||
required this.priceYuan,
|
||||
});
|
||||
|
||||
/// 展示名由天数派生(接口无 label 字段)
|
||||
String get label => '$days天';
|
||||
|
||||
factory Plan.fromJson(Map<String, dynamic> json) => Plan(
|
||||
id: json['planId'] as String,
|
||||
days: json['days'] as int,
|
||||
priceYuan: (json['priceCents'] as num) ~/ 100,
|
||||
);
|
||||
}
|
||||
|
||||
/// 支付渠道
|
||||
enum PayChannel { wechat, alipay }
|
||||
|
||||
/// 订单(由后端创建)
|
||||
class Order {
|
||||
final String orderId;
|
||||
|
||||
/// 微信支付参数(prepay_id / partner_id / nonce_str / time_stamp / sign 等)
|
||||
final Map<String, dynamic> wechatParams;
|
||||
|
||||
/// 支付宝订单串(orderStr)
|
||||
final String? alipayOrderStr;
|
||||
|
||||
const Order({
|
||||
required this.orderId,
|
||||
this.wechatParams = const {},
|
||||
this.alipayOrderStr,
|
||||
});
|
||||
}
|
||||
|
||||
/// 授权状态(服务端为准)
|
||||
class LicenseStatus {
|
||||
final bool active;
|
||||
final DateTime? expiresAt;
|
||||
|
||||
const LicenseStatus({required this.active, this.expiresAt});
|
||||
|
||||
bool get isActive => active && (expiresAt?.isAfter(DateTime.now()) ?? false);
|
||||
|
||||
factory LicenseStatus.fromJson(Map<String, dynamic> json) => LicenseStatus(
|
||||
active: json['active'] == true,
|
||||
expiresAt: json['expiresAt'] != null
|
||||
? DateTime.tryParse(json['expiresAt'] as String)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
/// 支付结果
|
||||
class PayResult {
|
||||
final bool success;
|
||||
final String? message;
|
||||
final String? orderId;
|
||||
|
||||
const PayResult({required this.success, this.message, this.orderId});
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:cupertino_http/cupertino_http.dart';
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../auth/session_store.dart';
|
||||
import '../config/app_config.dart';
|
||||
import 'models.dart';
|
||||
|
||||
/// 后端账号/支付/授权 API 客户端(契约见 docs/PaymentApi.md)。
|
||||
/// 后端未部署或请求失败时抛出 [OrderApiException];登录失效抛出 [SessionExpiredException],
|
||||
/// 由 UI 层回登录页。
|
||||
class OrderApiException implements Exception {
|
||||
final String message;
|
||||
const OrderApiException(this.message);
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
|
||||
/// 登录已失效(后端返回 code 61):token 过期/被清,UI 应清除会话回登录页
|
||||
class SessionExpiredException extends OrderApiException {
|
||||
const SessionExpiredException() : super('登录已失效,请重新登录');
|
||||
}
|
||||
|
||||
class OrderApi {
|
||||
final String baseUrl;
|
||||
final SessionStore sessionStore;
|
||||
final http.Client _client;
|
||||
|
||||
// iOS 26 对 dart:io 原生 socket 访问本地网络存在拦截 bug(权限已允许仍
|
||||
// 拒绝连接),改用 NSURLSession 网络栈(CupertinoClient)绕过;非 Apple
|
||||
// 平台回退 IOClient。
|
||||
OrderApi({String? baseUrl, required this.sessionStore, http.Client? client})
|
||||
: baseUrl = baseUrl ?? AppConfig.apiBaseUrl,
|
||||
_client = client ?? _defaultHttpClient();
|
||||
|
||||
static http.Client _defaultHttpClient() {
|
||||
if (!kIsWeb && Platform.isIOS) {
|
||||
return CupertinoClient.defaultSessionConfiguration();
|
||||
}
|
||||
return http.Client();
|
||||
}
|
||||
|
||||
/// 注册账号;重复注册等业务错误由 [_decode] 抛出
|
||||
Future<void> register({
|
||||
required String phone,
|
||||
required String password,
|
||||
}) async {
|
||||
final res = await _post('/api/v1/auth/register',
|
||||
jsonEncode({'phone': phone, 'password': password}),
|
||||
auth: false);
|
||||
_decode(res);
|
||||
}
|
||||
|
||||
/// 登录,成功返回 token(由调用方存入 SessionStore)
|
||||
Future<String> login({
|
||||
required String phone,
|
||||
required String password,
|
||||
}) async {
|
||||
final res = await _post('/api/v1/auth/login',
|
||||
jsonEncode({'phone': phone, 'password': password}),
|
||||
auth: false);
|
||||
final json = _decode(res);
|
||||
return json['token'] as String;
|
||||
}
|
||||
|
||||
/// 创建订单,返回支付参数(微信 prepay 参数或支付宝 orderStr)
|
||||
Future<Order> createOrder({
|
||||
required String planId,
|
||||
required PayChannel channel,
|
||||
}) async {
|
||||
final body = jsonEncode({'planId': planId, 'channel': channel.name});
|
||||
final res = await _post('/api/v1/orders', body);
|
||||
final json = _decode(res);
|
||||
final params = (json['payParams'] as Map?)?.cast<String, dynamic>() ?? {};
|
||||
return Order(
|
||||
orderId: json['orderId'] as String,
|
||||
wechatParams: params,
|
||||
alipayOrderStr: params['orderStr'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
/// 客户端支付完成后通知服务端(幂等),服务端据异步回调落授权
|
||||
Future<void> confirmOrder(String orderId) async {
|
||||
final res =
|
||||
await _post('/api/v1/orders/$orderId/confirm', jsonEncode({}));
|
||||
_decode(res);
|
||||
}
|
||||
|
||||
/// 拉取套餐价格方案(config.yml 静态定价,客户端不硬编码)
|
||||
Future<List<Plan>> fetchPlans() async {
|
||||
final http.Response res;
|
||||
try {
|
||||
res = await _client.get(
|
||||
Uri.parse('$baseUrl/api/v1/plans'),
|
||||
headers: {'Accept': 'application/json', ...await _authHeaders()},
|
||||
).timeout(const Duration(seconds: 15));
|
||||
} catch (e) {
|
||||
if (e is OrderApiException) rethrow;
|
||||
throw OrderApiException('网络请求失败: $e');
|
||||
}
|
||||
final json = _decode(res);
|
||||
return (json['list'] as List)
|
||||
.map((e) => Plan.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// 查询授权状态(服务端为准)
|
||||
Future<LicenseStatus> fetchLicense() async {
|
||||
final http.Response res;
|
||||
try {
|
||||
res = await _client.get(
|
||||
Uri.parse('$baseUrl/api/v1/license'),
|
||||
headers: {'Accept': 'application/json', ...await _authHeaders()},
|
||||
).timeout(const Duration(seconds: 15));
|
||||
} catch (e) {
|
||||
if (e is OrderApiException) rethrow;
|
||||
throw OrderApiException('网络请求失败: $e');
|
||||
}
|
||||
final json = _decode(res);
|
||||
return LicenseStatus.fromJson(json);
|
||||
}
|
||||
|
||||
Future<http.Response> _post(String path, String body,
|
||||
{bool auth = true}) async {
|
||||
try {
|
||||
return await _client
|
||||
.post(
|
||||
Uri.parse('$baseUrl$path'),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...auth ? await _authHeaders() : const <String, String>{},
|
||||
},
|
||||
body: body,
|
||||
)
|
||||
.timeout(const Duration(seconds: 15));
|
||||
} catch (e) {
|
||||
if (e is OrderApiException) rethrow;
|
||||
throw OrderApiException('网络请求失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, String>> _authHeaders() async {
|
||||
final token = await sessionStore.readToken();
|
||||
if (token == null || token.isEmpty) {
|
||||
throw const SessionExpiredException();
|
||||
}
|
||||
return {'Authorization': 'Bearer $token'};
|
||||
}
|
||||
|
||||
Map<String, dynamic> _decode(http.Response res) {
|
||||
final Map<String, dynamic> json;
|
||||
try {
|
||||
json = jsonDecode(res.body) as Map<String, dynamic>;
|
||||
} catch (_) {
|
||||
throw OrderApiException('服务端响应异常 (${res.statusCode})');
|
||||
}
|
||||
if (res.statusCode != 200 || json['code'] != 0) {
|
||||
final message = json['message'] as String? ?? '服务端错误 (${res.statusCode})';
|
||||
if (json['code'] == 61) {
|
||||
throw const SessionExpiredException();
|
||||
}
|
||||
throw OrderApiException(message);
|
||||
}
|
||||
return json['data'] as Map<String, dynamic>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fluwx/fluwx.dart' as fluwx;
|
||||
import 'package:tobias/tobias.dart' as tobias;
|
||||
|
||||
import '../config/app_config.dart';
|
||||
import 'models.dart';
|
||||
import 'order_api.dart';
|
||||
|
||||
/// 支付服务抽象:创建订单 + 拉起渠道支付 + 返回支付结果
|
||||
abstract class PaymentService {
|
||||
/// 支付完成后会通知服务端落授权;成功与否以 [PayResult.success] 为准
|
||||
Future<PayResult> pay(Plan plan, PayChannel channel);
|
||||
}
|
||||
|
||||
/// 微信支付(fluwx 实现)
|
||||
class WechatPayService implements PaymentService {
|
||||
WechatPayService({required this.orderApi});
|
||||
|
||||
final OrderApi orderApi;
|
||||
|
||||
@override
|
||||
Future<PayResult> pay(Plan plan, PayChannel channel) async {
|
||||
assert(channel == PayChannel.wechat);
|
||||
final order =
|
||||
await orderApi.createOrder(planId: plan.id, channel: channel);
|
||||
final p = order.wechatParams;
|
||||
|
||||
final launched = await fluwx.Fluwx().pay(
|
||||
which: fluwx.Payment(
|
||||
appId: AppConfig.wechatAppId,
|
||||
partnerId: p['partnerId'] as String? ?? '',
|
||||
prepayId: p['prepayId'] as String? ?? '',
|
||||
packageValue: p['packageValue'] as String? ?? 'Sign=WXPay',
|
||||
nonceStr: p['nonceStr'] as String? ?? '',
|
||||
timestamp: int.tryParse('${p['timeStamp'] ?? p['timestamp']}') ?? 0,
|
||||
sign: p['sign'] as String? ?? '',
|
||||
),
|
||||
);
|
||||
if (!launched) {
|
||||
return const PayResult(
|
||||
success: false, message: '未安装微信或拉起支付失败,请稍后再试');
|
||||
}
|
||||
|
||||
// 等待微信回调(errCode == 0 为成功),10s 超时
|
||||
final completer = Completer<PayResult>();
|
||||
final cancel = fluwx.Fluwx().addSubscriber((response) {
|
||||
if (response is! fluwx.WeChatPaymentResponse) return;
|
||||
completer.complete(PayResult(
|
||||
success: response.isSuccessful,
|
||||
message: response.isSuccessful ? null : (response.errStr ?? '微信支付未完成'),
|
||||
orderId: order.orderId,
|
||||
));
|
||||
});
|
||||
try {
|
||||
final result =
|
||||
await completer.future.timeout(const Duration(seconds: 10));
|
||||
if (result.success) await orderApi.confirmOrder(order.orderId);
|
||||
return result;
|
||||
} on TimeoutException {
|
||||
return PayResult(
|
||||
success: false,
|
||||
message: '等待微信支付结果超时,请确认支付状态',
|
||||
orderId: order.orderId);
|
||||
} finally {
|
||||
cancel.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 支付宝支付(tobias 实现,resultStatus 9000 为成功)
|
||||
class AlipayService implements PaymentService {
|
||||
AlipayService({required this.orderApi});
|
||||
|
||||
final OrderApi orderApi;
|
||||
|
||||
@override
|
||||
Future<PayResult> pay(Plan plan, PayChannel channel) async {
|
||||
assert(channel == PayChannel.alipay);
|
||||
final order =
|
||||
await orderApi.createOrder(planId: plan.id, channel: channel);
|
||||
final orderStr = order.alipayOrderStr;
|
||||
if (orderStr == null) {
|
||||
return const PayResult(success: false, message: '订单缺少支付参数');
|
||||
}
|
||||
|
||||
final map = await tobias.Tobias().pay(
|
||||
orderStr,
|
||||
universalLink: AppConfig.alipayUniversalLink,
|
||||
);
|
||||
final status = map['resultStatus']?.toString() ?? '';
|
||||
final ok = status == '9000';
|
||||
if (ok) await orderApi.confirmOrder(order.orderId);
|
||||
return PayResult(
|
||||
success: ok,
|
||||
message: ok ? null : _alipayMessage(status),
|
||||
orderId: order.orderId,
|
||||
);
|
||||
}
|
||||
|
||||
static String _alipayMessage(String status) => switch (status) {
|
||||
'8000' => '支付结果确认中,请稍后查看',
|
||||
'6001' => '用户取消支付',
|
||||
'6002' => '网络异常,支付未完成',
|
||||
'6004' => '支付结果未知,请查询订单状态',
|
||||
_ => '支付宝支付未完成',
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'models.dart';
|
||||
import 'paywall_view_model.dart';
|
||||
|
||||
/// 付费墙:拉取套餐价格 → 选套餐 → 微信/支付宝支付 → 解锁进入相机
|
||||
class PaywallScreen extends StatefulWidget {
|
||||
const PaywallScreen({super.key});
|
||||
|
||||
@override
|
||||
State<PaywallScreen> createState() => _PaywallScreenState();
|
||||
}
|
||||
|
||||
class _PaywallScreenState extends State<PaywallScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.read<PaywallViewModel>().loadPlans();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final vm = context.watch<PaywallViewModel>();
|
||||
final loading = vm.state.state == PayState.loading;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('视野 · 会员'), centerTitle: true),
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.fromLTRB(24, 16, 24, 16),
|
||||
child: Text(
|
||||
'开通会员解锁完整功能,按自然日计费,到期自动失效',
|
||||
style: TextStyle(color: Colors.grey, fontSize: 13),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
children: [
|
||||
_PlansSection(vm: vm, loading: loading),
|
||||
const SizedBox(height: 24),
|
||||
_PayButton(
|
||||
label: '微信支付',
|
||||
icon: Icons.wechat,
|
||||
color: const Color(0xFF07C160),
|
||||
enabled: !loading && vm.state.selectedPlan != null,
|
||||
onTap: loading
|
||||
? null
|
||||
: () async {
|
||||
final license = await vm.pay(PayChannel.wechat);
|
||||
if (license != null && context.mounted) {
|
||||
_onPaid(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_PayButton(
|
||||
label: '支付宝支付',
|
||||
icon: Icons.account_balance_wallet,
|
||||
color: const Color(0xFF1677FF),
|
||||
enabled: !loading && vm.state.selectedPlan != null,
|
||||
onTap: loading
|
||||
? null
|
||||
: () async {
|
||||
final license = await vm.pay(PayChannel.alipay);
|
||||
if (license != null && context.mounted) {
|
||||
_onPaid(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
if (vm.state.state == PayState.failed)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
child: Text(
|
||||
vm.state.message ?? '支付失败',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 支付成功:返回主界面(主界面刷新展示新到期时间)
|
||||
void _onPaid(BuildContext context) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
|
||||
/// 套餐区三态:加载中 / 失败可重试 / 套餐卡片列表
|
||||
class _PlansSection extends StatelessWidget {
|
||||
final PaywallViewModel vm;
|
||||
final bool loading;
|
||||
|
||||
const _PlansSection({required this.vm, required this.loading});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (vm.plansLoading && vm.plans.isEmpty) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 48),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
if (vm.plansError != null && vm.plans.isEmpty) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 32),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
vm.plansError!,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
OutlinedButton(
|
||||
onPressed: () => context.read<PaywallViewModel>().loadPlans(),
|
||||
child: const Text('重试'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
...vm.plans.map((p) => _PlanCard(
|
||||
plan: p,
|
||||
selected: vm.state.selectedPlan?.id == p.id,
|
||||
enabled: !loading,
|
||||
onTap: () => vm.selectPlan(p),
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PlanCard extends StatelessWidget {
|
||||
final Plan plan;
|
||||
final bool selected;
|
||||
final bool enabled;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _PlanCard({
|
||||
required this.plan,
|
||||
required this.selected,
|
||||
required this.enabled,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
elevation: selected ? 3 : 1,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(
|
||||
color: selected ? Colors.orange : Colors.grey.shade300,
|
||||
width: selected ? 2 : 1,
|
||||
),
|
||||
),
|
||||
child: ListTile(
|
||||
onTap: enabled ? onTap : null,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||
title: Text(
|
||||
plan.label,
|
||||
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 17),
|
||||
),
|
||||
subtitle: Text('${plan.days} 天 · 自然日'),
|
||||
trailing: Text(
|
||||
'¥${plan.priceYuan}',
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PayButton extends StatelessWidget {
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final bool enabled;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const _PayButton({
|
||||
required this.label,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.enabled,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: FilledButton.icon(
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: color,
|
||||
disabledBackgroundColor: Colors.grey.shade300,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
||||
),
|
||||
onPressed: enabled ? onTap : null,
|
||||
icon: Icon(icon, size: 20),
|
||||
label: Text(label, style: const TextStyle(fontSize: 16)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'license_service.dart';
|
||||
import 'models.dart';
|
||||
import 'order_api.dart';
|
||||
import 'payment_service.dart';
|
||||
|
||||
enum PayState { idle, loading, success, failed }
|
||||
|
||||
@immutable
|
||||
class PaywallUiState {
|
||||
final PayState state;
|
||||
final String? message;
|
||||
final Plan? selectedPlan;
|
||||
|
||||
const PaywallUiState({
|
||||
this.state = PayState.idle,
|
||||
this.message,
|
||||
this.selectedPlan,
|
||||
});
|
||||
}
|
||||
|
||||
class PaywallViewModel extends ChangeNotifier {
|
||||
final OrderApi orderApi;
|
||||
final LicenseService licenseService;
|
||||
final Map<PayChannel, PaymentService> services;
|
||||
|
||||
PaywallUiState _state = const PaywallUiState();
|
||||
PaywallUiState get state => _state;
|
||||
|
||||
List<Plan> _plans = const [];
|
||||
bool _plansLoading = false;
|
||||
String? _plansError;
|
||||
|
||||
/// 套餐价格方案(来自后端,客户端不硬编码)
|
||||
List<Plan> get plans => _plans;
|
||||
bool get plansLoading => _plansLoading;
|
||||
String? get plansError => _plansError;
|
||||
|
||||
PaywallViewModel({
|
||||
required this.orderApi,
|
||||
required this.licenseService,
|
||||
required this.services,
|
||||
});
|
||||
|
||||
/// 进充值页时拉取套餐;已有数据或加载中跳过,失败可重试
|
||||
Future<void> loadPlans() async {
|
||||
if (_plans.isNotEmpty || _plansLoading) return;
|
||||
_plansLoading = true;
|
||||
_plansError = null;
|
||||
notifyListeners();
|
||||
try {
|
||||
_plans = await orderApi.fetchPlans();
|
||||
} on SessionExpiredException {
|
||||
_plansError = '登录已失效,请重新登录后再充值';
|
||||
} on OrderApiException catch (e) {
|
||||
_plansError = e.message;
|
||||
} finally {
|
||||
_plansLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void selectPlan(Plan? plan) {
|
||||
_state = PaywallUiState(selectedPlan: plan);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 发起支付;成功后刷新授权并通知页面进入相机
|
||||
Future<LicenseStatus?> pay(PayChannel channel) async {
|
||||
final plan = _state.selectedPlan;
|
||||
if (plan == null) return null;
|
||||
final service = services[channel];
|
||||
if (service == null) return null;
|
||||
|
||||
_state = PaywallUiState(state: PayState.loading, selectedPlan: plan);
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final result = await service.pay(plan, channel);
|
||||
if (!result.success) {
|
||||
_state = PaywallUiState(
|
||||
state: PayState.failed,
|
||||
message: result.message ?? '支付失败,请重试',
|
||||
selectedPlan: plan,
|
||||
);
|
||||
notifyListeners();
|
||||
return null;
|
||||
}
|
||||
final license = await licenseService.refresh();
|
||||
_state = PaywallUiState(
|
||||
state: PayState.success,
|
||||
selectedPlan: plan,
|
||||
);
|
||||
notifyListeners();
|
||||
return license;
|
||||
} on SessionExpiredException {
|
||||
_state = PaywallUiState(
|
||||
state: PayState.failed,
|
||||
message: '登录已失效,请重新登录后再充值',
|
||||
selectedPlan: plan,
|
||||
);
|
||||
notifyListeners();
|
||||
return null;
|
||||
} on OrderApiException catch (e) {
|
||||
_state = PaywallUiState(
|
||||
state: PayState.failed,
|
||||
message: '支付不可用: ${e.message}',
|
||||
selectedPlan: plan,
|
||||
);
|
||||
notifyListeners();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void reset() {
|
||||
_state = const PaywallUiState();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
import 'package:vibration/vibration.dart';
|
||||
|
||||
/// 提醒:同类目标 10s 内只提醒一次。
|
||||
/// 震动/提示音默认开启,不提供关闭入口。
|
||||
class Reminder {
|
||||
final AudioPlayer _player = AudioPlayer();
|
||||
String? _lastAlertLabel;
|
||||
int _lastAlertAt = 0;
|
||||
|
||||
/// 同类目标 10s 内只提醒一次
|
||||
void onDetected(String label) {
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
if (label == _lastAlertLabel && now - _lastAlertAt < 10000) return;
|
||||
_lastAlertAt = now;
|
||||
_lastAlertLabel = label;
|
||||
|
||||
_vibrate();
|
||||
_playTone();
|
||||
}
|
||||
|
||||
void _vibrate() => Vibration.vibrate(duration: 200);
|
||||
|
||||
Future<void> _playTone() async {
|
||||
await _player.stop();
|
||||
await _player.play(AssetSource('beep.wav'));
|
||||
}
|
||||
|
||||
void release() {
|
||||
_player.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,882 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.7.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.13.1"
|
||||
audioplayers:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: audioplayers
|
||||
sha256: "2ba4bb2944baacbdd5372ff8254a8e7feb8c10d7739545e392f5605a8f618745"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.8.1"
|
||||
audioplayers_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_android
|
||||
sha256: f5ff5b15620fbab8cb0849e9636c48e2b96c3f0f71723bbbe2ad3c761b205f05
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.3.0"
|
||||
audioplayers_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_darwin
|
||||
sha256: "1ca553add991384ecf421b9569da850f3ab2472ffb83f6970b0416365abc51be"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.5.0"
|
||||
audioplayers_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_linux
|
||||
sha256: "15178b726b7cdee5364d0463c8d445630c4e0fb7d26612b73c767e7d25de9417"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.3.0"
|
||||
audioplayers_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_platform_interface
|
||||
sha256: "765f6f0e6dca55cb471c9483fc77700564b3484d19198aca4ebb5147c6c85acb"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "7.2.0"
|
||||
audioplayers_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_web
|
||||
sha256: ae1e0103c865a03e273f6d13d97b93f5595eac09915729cd5e37ef96e2857319
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.3.0"
|
||||
audioplayers_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audioplayers_windows
|
||||
sha256: a70ae82bba2dfcb6eb03dd4815d737a2d46d33ea5a96a03f535cfcaac490e413
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.4.1"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
camera:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: camera
|
||||
sha256: "4142a19a38e388d3bab444227636610ba88982e36dff4552d5191a86f65dc437"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.11.4"
|
||||
camera_android_camerax:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: camera_android_camerax
|
||||
sha256: "8516fe308bc341a5067fb1a48edff0ddfa57c0d3cdcc9dbe7ceca3ba119e2577"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.6.30"
|
||||
camera_avfoundation:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: camera_avfoundation
|
||||
sha256: "11b4aee2f5e5e038982e152b4a342c749b414aa27857899d20f4323e94cb5f0b"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.9.23+2"
|
||||
camera_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: camera_platform_interface
|
||||
sha256: "4524ca6eb4176b066864036ad4fe02c3e4863e63b77eadc21a5bf56824f43498"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.13.1"
|
||||
camera_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: camera_web
|
||||
sha256: "081441bd2f92b5841aa70ff4d6eddfe34078d97f9d085cc9e0f9d0102f145925"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.3.5+5"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: clock
|
||||
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
code_assets:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_assets
|
||||
sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
cross_file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cross_file
|
||||
sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.3.5+4"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
cupertino_http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cupertino_http
|
||||
sha256: "3c8c69cc1b94b9c7570d9454bf1e11fa7010b248547a0760843adc71f0c08fbe"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cupertino_icons
|
||||
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.9"
|
||||
dbus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dbus
|
||||
sha256: a48d5da28e89bd02196e80d81ed8d7954923d00a0f4a68cc20b575038f023383
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.7.15"
|
||||
device_info_plus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: device_info_plus
|
||||
sha256: "98f28b42168cc509abc92f88518882fd58061ea372d7999aecc424345c7bff6a"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "11.5.0"
|
||||
device_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: device_info_plus_platform_interface
|
||||
sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "7.0.3"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.3.3"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fixnum
|
||||
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_lints
|
||||
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_plugin_android_lifecycle
|
||||
sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.35"
|
||||
flutter_secure_storage:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_secure_storage
|
||||
sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "9.2.4"
|
||||
flutter_secure_storage_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_linux
|
||||
sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.2.3"
|
||||
flutter_secure_storage_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_macos
|
||||
sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
flutter_secure_storage_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_platform_interface
|
||||
sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
flutter_secure_storage_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_web
|
||||
sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
flutter_secure_storage_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_windows
|
||||
sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
fluwx:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: fluwx
|
||||
sha256: d25c26a12a1fa280833105869ea255a5877695e03dc1190f0d2c027a7c989ef3
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.6.3"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: glob
|
||||
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
hooks:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hooks
|
||||
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.6.0"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
http_profile:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_profile
|
||||
sha256: "7e679e355b09aaee2ab5010915c932cce3f2d1c11c3b2dc177891687014ffa78"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.1.0"
|
||||
jni:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: jni
|
||||
sha256: f038e58b4dc2c9037f50e233175086337e0b305e356d28211bf55f21c504cbd3
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
jni_flutter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: jni_flutter
|
||||
sha256: "7b717011ea40d04fd47c2731d3d1d36eb99eba3435c2753d62489e8c3c9991d5"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
jni_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: jni_util
|
||||
sha256: "1ba86da04a5f2bf18fde2edb235587e70c5b0fc5bd4ba955f46b00942c3fc35f"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: js
|
||||
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.6.7"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "11.0.2"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.10"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: logging
|
||||
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.12.19"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.13.0"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
native_toolchain_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: native_toolchain_c
|
||||
sha256: f9c168717100ae6d9fee9ffb0be379bf1f8b26b0f6bcbd4fdddcd931993a6a72
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.19.2"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: nested
|
||||
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
objective_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: objective_c
|
||||
sha256: b7fb95a6d9a4f009edd63dc5ac69f07420b23a16161c6dd8660290b59c602e8e
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "9.5.0"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_config
|
||||
sha256: ffcf4cf3d6c0b74ac43708d9f56625506e8a68aa935abe9d267a7330f320eb5d
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
package_info_plus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: "468c26b4254ab01979fa5e4a98cb343ea3631b9acee6f21028997419a80e1a20"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "9.0.1"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus_platform_interface
|
||||
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.6"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.6.0"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.2"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
permission_handler:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: permission_handler
|
||||
sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "11.4.0"
|
||||
permission_handler_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_android
|
||||
sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "12.1.0"
|
||||
permission_handler_apple:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_apple
|
||||
sha256: f49cb15a064ea9d974fc7fbb302099353b7b170d07284e86e264561579e5bcf8
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "9.6.1"
|
||||
permission_handler_html:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_html
|
||||
sha256: "6ea98b3f17f60d3b527f2647ed2ab4dc0f6bfe25b22cb1c363f5d8f62252f6ac"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.1.4+1"
|
||||
permission_handler_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_platform_interface
|
||||
sha256: a5c8a97ecf5616112a5b16d4b8e9ec0e5ae90ef63ac69c0d7b8ae240be760b23
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.4.0"
|
||||
permission_handler_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_windows
|
||||
sha256: caeae01858a0a7d2df67a445ac98e1ad95e55a0e77c73044f4e9b1c8c2289cbd
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.2.2"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "7.0.2"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.6"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: provider
|
||||
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.1.5+1"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
quiver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: quiver
|
||||
sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
record_use:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: record_use
|
||||
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.6.0"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.10.2"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.12.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
stream_transform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_transform
|
||||
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
synchronized:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: synchronized
|
||||
sha256: "3a7b5d17422dd0f8d5c6c14feaa5a1c65638b9455f871a96f08437562c046931"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.4.1+2"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.7.11"
|
||||
tflite_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: tflite_flutter
|
||||
sha256: "48e6fde2ad97162bb66a16a142f4c4698add9e8cd397ce9d1cc7451b55537ac1"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.11.0"
|
||||
tobias:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: tobias
|
||||
sha256: "1cfd203bf57f8d4daa3e734d24ef97568eb5265af62d1498c32da4e69905c70f"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.3.4"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: uuid
|
||||
sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.6.0"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
vibration:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: vibration
|
||||
sha256: "3b08a0579c2f9c18d5d78cb5c74f1005f731e02eeca6d72561a2e8059bf98ec3"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
vibration_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vibration_platform_interface
|
||||
sha256: "6ffeee63547562a6fef53c05a41d4fdcae2c0595b83ef59a4813b0612cd2bc36"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.0.3"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "5f37239c4851efcef929cea7824e76df7f2f0970aef85d66bbc430afa40e72f0"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "15.3.0"
|
||||
wakelock_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: wakelock_plus
|
||||
sha256: ddf3db70eaa10c37558ff817519b85d527dbd21034fd5d8e1c2e85f31588f1c1
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.5.2"
|
||||
wakelock_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: wakelock_plus_platform_interface
|
||||
sha256: "0618d1799f0b28bcf98255b4ee8313e6fc4d38589dc4ee5fe5840d57d1aff6da"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.6.0"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket
|
||||
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.15.0"
|
||||
win32_registry:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32_registry
|
||||
sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yaml
|
||||
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.12.2 <4.0.0"
|
||||
flutter: ">=3.44.0"
|
||||