迁移 Flutter 端与训练脚本,模型/训练产物移出 git(遵循纯代码约定)

This commit is contained in:
2026-08-24 12:35:24 +08:00
parent d056f01965
commit 961523d94c
218 changed files with 13391 additions and 3232 deletions
+45
View File
@@ -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
+30
View File
@@ -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'
+33
View File
@@ -0,0 +1,33 @@
# observer
野生动物实时识别 AppFlutter 版)。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 地址),或直接真机验证。
+28
View File
@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
+14
View File
@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks
+53
View File
@@ -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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+24
View File
@@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
+6
View File
@@ -0,0 +1,6 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
# This newDsl flag was added by the Flutter template
android.newDsl=false
# This builtInKotlin flag was added by the Flutter template
android.builtInKotlin=false
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip
+26
View File
@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "9.0.1" apply false
id("org.jetbrains.kotlin.android") version "2.3.20" apply false
}
include(":app")
Binary file not shown.
+2
View File
@@ -0,0 +1,2 @@
pheasant
suspect
+103
View File
@@ -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` 节点(静态定价,改价改配置重启生效)
+34
View File
@@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
</dict>
</plist>
+2
View File
@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"
+2
View File
@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"
+66
View File
@@ -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
+83
View File
@@ -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 &quot;$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh&quot; prepare&#10;">
<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>
+16
View File
@@ -0,0 +1,16 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
}
@@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

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

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

@@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>
@@ -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>
+108
View File
@@ -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.
}
}
+73
View File
@@ -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()),
);
}
}
+137
View File
@@ -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),
),
],
],
),
),
),
),
);
}
}
+57
View File
@@ -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();
}
}
}
+36
View File
@@ -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 用默认 bgra8888420v 在部分 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();
}
}
+389
View File
@@ -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,不阻塞 UIworker 为 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 高预览下的估算焦距 px5.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)。
/// 推理在后台 isolateDetectorWorker)执行,主 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;
}
}
+22
View File
@@ -0,0 +1,22 @@
/// 全局配置占位:接入真实支付前需替换以下值。
class AppConfig {
/// 后端服务器地址(订单创建/授权查询/套餐价格)
/// 默认 Android 模拟器 10.0.2.2iOS 模拟器构建时传
/// --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';
}
+35
View File
@@ -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;
}
}
+21
View File
@@ -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);
// 传原始字节视图而非 Float32Listtflite_flutter 会对非 ByteBuffer/Uint8List
// 输入调用 resizeInputTensor1 维 [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~1NCHW),双线性采样。
/// 兼容 NV12iOS 双平面,UV 交错)与 I420Android 三平面)。
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~235Cb/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);
// NCHWr/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();
}
+113
View File
@@ -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;
}
/// 读取单像素 RGB0~255)。
/// BGRA 单平面:每像素 4 字节 [b,g,r,a]
/// YUVy 平面 + 4:2:0 半分辨率 U/VNV12 交错或 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);
}
}
+216
View File
@@ -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),
),
],
),
),
],
),
),
);
}
}
+67
View File
@@ -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();
}
}
+27
View File
@@ -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';
}
+67
View File
@@ -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});
}
+170
View File
@@ -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' => '支付结果未知,请查询订单状态',
_ => '支付宝支付未完成',
};
}
+221
View File
@@ -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();
}
}
+34
View File
@@ -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();
}
}
+882
View File
@@ -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"

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