Initial commit: Coin scan demo with blur detection and coin contour cropping

- YOLOv5 TFLite coin detection with NMS
- Blur classifier for image quality gating
- Circular contour refinement and cropping
- Camera capture and gallery pick

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
a1518 2026-07-13 20:35:28 -07:00
commit 7cee2ae28f
78 changed files with 3615 additions and 0 deletions

46
.gitignore vendored Normal file
View File

@ -0,0 +1,46 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
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
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# 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
# Native phash host build (CLI / hashgen)
/native/phash/build/

33
.metadata Normal file
View File

@ -0,0 +1,33 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "5dcb86f68f239346676ceb1ed1ea385bd215fba1"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1
base_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1
- platform: android
create_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1
base_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1
- platform: ios
create_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1
base_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1
# 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'

23
README.md Normal file
View File

@ -0,0 +1,23 @@
# Coin Scan Demo
对照 CoinSnap 反编译:`detect.tflite`YOLOv5 硬币检测)+ `blur.tflite`(模糊度二分类)。
流程对齐 card-dex-demo但换成硬币圆框与双模型
拍照 / 选图 → **blur 清晰度检测****detect 目标框****圆形轮廓精修** → 圆形裁剪预览。
模型来自 CoinSnap APK `assets/`
| 模型 | 输入 | 输出 |
|------|------|------|
| `detect.tflite` | `[1,320,320,3]` NHWC, `/255`, letterbox | `[1,6300,6]` → cx,cy,w,h,obj,cls |
| `blur.tflite` | `[1,3,224,224]` NCHW, `/255`, 白底 letterbox | `[1,2]``[blur, clear]` |
## 运行
```bash
export PATH="$HOME/flutter/bin:$PATH"
cd /Users/a1518/Desktop/project/coin-dex-demo
flutter pub get
flutter run
```

28
analysis_options.yaml Normal file
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

13
android/.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
**/*.keystore
**/*.jks

58
android/app/build.gradle Normal file
View File

@ -0,0 +1,58 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file("local.properties")
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader("UTF-8") { reader ->
localProperties.load(reader)
}
}
def flutterVersionCode = localProperties.getProperty("flutter.versionCode")
if (flutterVersionCode == null) {
flutterVersionCode = "1"
}
def flutterVersionName = localProperties.getProperty("flutter.versionName")
if (flutterVersionName == null) {
flutterVersionName = "1.0"
}
android {
namespace = "com.coindex.coin_dex_demo"
compileSdk = 35
ndkVersion "27.0.12077973"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
defaultConfig {
applicationId = "com.coindex.coin_dex_demo"
minSdk = 26
targetSdk = 35
versionCode = flutterVersionCode.toInteger()
versionName = flutterVersionName
}
buildTypes {
release {
signingConfig = signingConfigs.debug
minifyEnabled false
shrinkResources false
}
}
}
flutter {
source = "../.."
}

View File

@ -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>

View File

@ -0,0 +1,39 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-feature android:name="android.hardware.camera" android:required="true" />
<application
android:label="Coin Scan Demo"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<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>
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@ -0,0 +1,5 @@
package com.coindex.coin_dex_demo
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity()

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

18
android/build.gradle Normal file
View File

@ -0,0 +1,18 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = "../build"
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

View File

@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx4G -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true

View File

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip

25
android/settings.gradle Normal file
View File

@ -0,0 +1,25 @@
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.7.0" apply false
id "org.jetbrains.kotlin.android" version "2.0.21" apply false
}
include ":app"

BIN
assets/ml/blur.tflite Normal file

Binary file not shown.

BIN
assets/ml/detect.tflite Normal file

Binary file not shown.

34
ios/.gitignore vendored Normal file
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

View File

@ -0,0 +1,26 @@
<?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>
<key>MinimumOSVersion</key>
<string>12.0</string>
</dict>
</plist>

View File

@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

View File

@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"

42
ios/Podfile Normal file
View File

@ -0,0 +1,42 @@
platform :ios, '12.0'
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. Run flutter pub get 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}"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
use_modular_headers!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0'
end
end
end

View File

@ -0,0 +1,616 @@
// !$*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 */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
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 = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.coindex.coinDexDemo;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.coindex.coinDexDemo.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.coindex.coinDexDemo.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.coindex.coinDexDemo.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 = 12.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 = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.coindex.coinDexDemo;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.coindex.coinDexDemo;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
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"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
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>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,13 @@
import Flutter
import UIKit
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

53
ios/Runner/Info.plist Normal file
View File

@ -0,0 +1,53 @@
<?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>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Coin Scan Demo</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>coin_dex_demo</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>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>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>Camera access is required to scan and crop coins.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Photo library access is required to pick a coin image.</string>
</dict>
</plist>

View File

@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View File

@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

34
lib/main.dart Normal file
View File

@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'screens/capture_screen.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
]);
runApp(const CoinScanDemoApp());
}
class CoinScanDemoApp extends StatelessWidget {
const CoinScanDemoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Coin Scan Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
brightness: Brightness.dark,
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFFEAB308),
brightness: Brightness.dark,
),
useMaterial3: true,
scaffoldBackgroundColor: const Color(0xFF0B0C10),
),
home: const CaptureScreen(),
);
}
}

View File

@ -0,0 +1,323 @@
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import '../services/coin_scan_gate.dart';
import '../widgets/guide_overlay.dart';
import 'preview_screen.dart';
/// Capture only blur + detect run after shutter (CoinSnap flow).
class CaptureScreen extends StatefulWidget {
const CaptureScreen({super.key});
@override
State<CaptureScreen> createState() => _CaptureScreenState();
}
class _CaptureScreenState extends State<CaptureScreen>
with WidgetsBindingObserver {
final _picker = ImagePicker();
CameraController? _camera;
List<CameraDescription> _cameras = const [];
bool _ready = false;
bool _busy = false;
String? _error;
Size? _previewSize;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_initCamera();
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_camera?.dispose();
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
final cam = _camera;
if (cam == null || !cam.value.isInitialized) return;
if (state == AppLifecycleState.inactive) {
cam.dispose();
_camera = null;
_ready = false;
} else if (state == AppLifecycleState.resumed) {
_initCamera();
}
}
Future<void> _initCamera() async {
try {
_cameras = await availableCameras();
if (_cameras.isEmpty) {
setState(() => _error = '没有可用相机');
return;
}
final back = _cameras.firstWhere(
(c) => c.lensDirection == CameraLensDirection.back,
orElse: () => _cameras.first,
);
final controller = CameraController(
back,
ResolutionPreset.high,
enableAudio: false,
);
await controller.initialize();
if (!mounted) {
await controller.dispose();
return;
}
_camera = controller;
setState(() {
_ready = true;
_error = null;
});
} catch (e) {
setState(() => _error = '相机初始化失败: $e');
}
}
Future<void> _openPreviewIfOk(String path) async {
final result = await CoinScanGate.evaluate(
imagePath: path,
captureViewport: _previewSize,
);
if (!mounted) return;
if (!result.ok) {
final msg = switch (result.fail) {
CoinGateFail.blurry =>
'图像过模糊(清晰度 ${(result.blur!.clearScore * 100).toStringAsFixed(0)}%),请重拍',
CoinGateFail.noCoin => '未检测到硬币,请对准圆框后重拍',
CoinGateFail.none => '无法处理该图片',
};
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(msg), duration: const Duration(seconds: 2)),
);
return;
}
await Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => PreviewScreen(
imagePath: path,
captureViewport: _previewSize,
),
),
);
}
Future<void> _capture() async {
if (_busy) return;
final cam = _camera;
if (cam == null || !cam.value.isInitialized) return;
setState(() => _busy = true);
try {
final shot = await cam.takePicture();
await _openPreviewIfOk(shot.path);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('拍照失败: $e')),
);
}
} finally {
if (mounted) setState(() => _busy = false);
}
}
Future<void> _pickGallery() async {
if (_busy) return;
setState(() => _busy = true);
try {
final file = await _picker.pickImage(
source: ImageSource.gallery,
imageQuality: 100,
requestFullMetadata: true,
);
if (file == null) return;
await _openPreviewIfOk(file.path);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('选图失败: $e')),
);
}
} finally {
if (mounted) setState(() => _busy = false);
}
}
Future<void> _switchCamera() async {
if (_cameras.length < 2 || _busy) return;
final current = _camera?.description;
final next = _cameras.firstWhere(
(c) => c.lensDirection != current?.lensDirection,
orElse: () => _cameras.first,
);
setState(() => _ready = false);
await _camera?.dispose();
final controller = CameraController(
next,
ResolutionPreset.high,
enableAudio: false,
);
await controller.initialize();
if (!mounted) {
await controller.dispose();
return;
}
_camera = controller;
setState(() => _ready = true);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
fit: StackFit.expand,
children: [
_buildPreview(),
SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'硬币扫描',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 4),
Text(
'将硬币放入圆框 → blur 清晰度检测 → detect 轮廓锁定',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.white70,
),
),
],
),
),
),
if (_busy)
const ColoredBox(
color: Color(0x66000000),
child: Center(child: CircularProgressIndicator()),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: _buildControls(),
),
],
),
);
}
Widget _buildPreview() {
if (_error != null) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Text(_error!, textAlign: TextAlign.center),
),
);
}
final cam = _camera;
if (!_ready || cam == null || !cam.value.isInitialized) {
return const Center(child: CircularProgressIndicator());
}
return LayoutBuilder(
builder: (context, constraints) {
final previewSize = Size(constraints.maxWidth, constraints.maxHeight);
_previewSize = previewSize;
return Stack(
fit: StackFit.expand,
children: [
FittedBox(
fit: BoxFit.cover,
child: SizedBox(
width: cam.value.previewSize?.height ?? previewSize.width,
height: cam.value.previewSize?.width ?? previewSize.height,
child: CameraPreview(cam),
),
),
GuideOverlay(size: previewSize),
],
);
},
);
}
Widget _buildControls() {
return SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(28, 12, 28, 24),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_RoundButton(
icon: Icons.photo_library_outlined,
onTap: _busy ? null : _pickGallery,
),
GestureDetector(
onTap: _busy ? null : _capture,
child: Container(
width: 76,
height: 76,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 4),
),
padding: const EdgeInsets.all(5),
child: Container(
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
),
),
),
_RoundButton(
icon: Icons.flip_camera_ios_outlined,
onTap: _busy ? null : _switchCamera,
),
],
),
),
);
}
}
class _RoundButton extends StatelessWidget {
const _RoundButton({required this.icon, this.onTap});
final IconData icon;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
return Material(
color: Colors.white12,
shape: const CircleBorder(),
child: InkWell(
customBorder: const CircleBorder(),
onTap: onTap,
child: SizedBox(
width: 52,
height: 52,
child: Icon(icon, color: Colors.white),
),
),
);
}
}

View File

@ -0,0 +1,377 @@
import 'dart:io';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:image/image.dart' as img;
import 'package:path_provider/path_provider.dart';
import '../services/blur_detector.dart';
import '../services/coin_cropper.dart';
import '../services/coin_detector.dart';
import '../widgets/guide_frame.dart';
/// Post-capture: blur check detect contour refine circular crop.
class PreviewScreen extends StatefulWidget {
const PreviewScreen({
super.key,
required this.imagePath,
this.captureViewport,
});
final String imagePath;
final Size? captureViewport;
@override
State<PreviewScreen> createState() => _PreviewScreenState();
}
class _PreviewScreenState extends State<PreviewScreen> {
final _detector = CoinDetector();
final _blur = BlurDetector();
bool _loading = true;
String? _error;
ui.Image? _sourceImage;
CoinBox? _locked;
CropResult? _crop;
BlurResult? _blurResult;
@override
void initState() {
super.initState();
_run();
}
@override
void dispose() {
_sourceImage?.dispose();
_detector.dispose();
_blur.dispose();
super.dispose();
}
Future<void> _run() async {
try {
final raw = await File(widget.imagePath).readAsBytes();
final displayCodec = await ui.instantiateImageCodec(raw);
final displayFrame = await displayCodec.getNextFrame();
final fullImage = displayFrame.image;
final rgba = await fullImage.toByteData(format: ui.ImageByteFormat.rawRgba);
if (rgba == null) {
fullImage.dispose();
throw StateError('无法读取像素');
}
final fullRaster = img.Image.fromBytes(
width: fullImage.width,
height: fullImage.height,
bytes: rgba.buffer,
bytesOffset: rgba.offsetInBytes,
numChannels: 4,
order: img.ChannelOrder.rgba,
);
fullImage.dispose();
final imageSize = Size(
fullRaster.width.toDouble(),
fullRaster.height.toDouble(),
);
final guide = widget.captureViewport != null
? GuideFrame.inCoverImage(
viewport: widget.captureViewport!,
imageSize: imageSize,
)
: GuideFrame.inImage(imageSize);
final gx = guide.left.round().clamp(0, fullRaster.width - 1);
final gy = guide.top.round().clamp(0, fullRaster.height - 1);
final gw = guide.width.round().clamp(1, fullRaster.width - gx);
final gh = guide.height.round().clamp(1, fullRaster.height - gy);
final raster = img.copyCrop(
fullRaster,
x: gx,
y: gy,
width: gw,
height: gh,
);
final blurResult = await _blur.evaluate(raster);
if (blurResult.isBlurry) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'图像过模糊(清晰 ${(blurResult.clearScore * 100).toStringAsFixed(0)}%),请重拍',
),
),
);
Navigator.of(context).pop();
return;
}
final dir = await getTemporaryDirectory();
final guidePath =
'${dir.path}/coin_guide_${DateTime.now().millisecondsSinceEpoch}.jpg';
await File(guidePath).writeAsBytes(
img.encodeJpg(raster, quality: 100),
flush: true,
);
final guideBytes = img.encodeJpg(raster, quality: 100);
final guideCodec = await ui.instantiateImageCodec(guideBytes);
final guideFrame = await guideCodec.getNextFrame();
final displayImage = guideFrame.image;
final boxes = await _detector.detectImage(raster);
final guideSize = Size(
displayImage.width.toDouble(),
displayImage.height.toDouble(),
);
var best = _detector.pickBest(boxes, imageSize: guideSize);
if (best == null) {
displayImage.dispose();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('未检测到硬币,请重拍')),
);
Navigator.of(context).pop();
return;
}
best = _detector.refineContour(raster, best);
final crop = await CoinCropper.cropAndCorrect(
imagePath: guidePath,
box: best,
);
if (!mounted) {
displayImage.dispose();
return;
}
setState(() {
_sourceImage = displayImage;
_locked = best;
_crop = crop;
_blurResult = blurResult;
_loading = false;
});
} catch (e) {
if (!mounted) return;
setState(() {
_error = '处理失败: $e';
_loading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('硬币轮廓锁定'),
backgroundColor: Colors.transparent,
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 8, 20, 24),
child: _loading
? const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('blur → detect → 轮廓精修…'),
],
),
)
: _error != null
? Center(child: Text(_error!, textAlign: TextAlign.center))
: _buildContent(context),
),
),
);
}
Widget _buildContent(BuildContext context) {
final blur = _blurResult;
final locked = _locked;
final meta = [
if (blur != null)
'清晰度 ${(blur.clearScore * 100).toStringAsFixed(0)}%',
if (locked != null)
'检测 ${(locked.confidence * 100).toStringAsFixed(0)}%',
if (locked?.radius != null)
'半径 ${locked!.radius!.round()}px',
].join(' · ');
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
meta,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.white70,
),
),
const SizedBox(height: 8),
Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(
height: 260,
child: _LockedPhotoView(
image: _sourceImage!,
locked: _locked,
),
),
const SizedBox(height: 12),
Text('圆形裁剪', style: Theme.of(context).textTheme.titleSmall),
const SizedBox(height: 6),
SizedBox(
height: 200,
child: Center(
child: ClipOval(
child: ColoredBox(
color: Colors.black26,
child: Image.memory(
_crop!.bytes,
width: 180,
height: 180,
fit: BoxFit.cover,
filterQuality: FilterQuality.high,
),
),
),
),
),
const SizedBox(height: 16),
FilledButton.icon(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.camera_alt_outlined),
label: const Text('重新拍摄'),
style: FilledButton.styleFrom(
minimumSize: const Size.fromHeight(48),
),
),
],
),
),
),
],
);
}
}
class _LockedPhotoView extends StatelessWidget {
const _LockedPhotoView({required this.image, required this.locked});
final ui.Image image;
final CoinBox? locked;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final view = Size(constraints.maxWidth, constraints.maxHeight);
final imgSize = Size(image.width.toDouble(), image.height.toDouble());
final fitted = _contain(imgSize, view);
return ClipRRect(
borderRadius: BorderRadius.circular(12),
child: ColoredBox(
color: Colors.black26,
child: Stack(
children: [
Positioned.fromRect(
rect: fitted,
child: RawImage(
image: image,
fit: BoxFit.fill,
filterQuality: FilterQuality.high,
),
),
if (locked != null)
Positioned.fill(
child: CustomPaint(
painter: _LockPainter(
box: locked!,
imageSize: imgSize,
fitted: fitted,
),
),
),
],
),
),
);
},
);
}
static Rect _contain(Size image, Size view) {
final scale = (view.width / image.width < view.height / image.height)
? view.width / image.width
: view.height / image.height;
final w = image.width * scale;
final h = image.height * scale;
return Rect.fromLTWH(
(view.width - w) / 2,
(view.height - h) / 2,
w,
h,
);
}
}
class _LockPainter extends CustomPainter {
_LockPainter({
required this.box,
required this.imageSize,
required this.fitted,
});
final CoinBox box;
final Size imageSize;
final Rect fitted;
Offset _map(Offset p) => Offset(
fitted.left + p.dx / imageSize.width * fitted.width,
fitted.top + p.dy / imageSize.height * fitted.height,
);
@override
void paint(Canvas canvas, Size size) {
final c = _map(box.effectiveCenter);
final r = box.effectiveRadius / imageSize.width * fitted.width;
final ring = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 3
..color = const Color(0xFFEAB308);
canvas.drawCircle(c, r, ring);
final aabb = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 1.2
..color = const Color(0x66EAB308);
final tl = _map(box.rect.topLeft);
final br = _map(box.rect.bottomRight);
canvas.drawRect(Rect.fromPoints(tl, br), aabb);
final cross = Paint()
..color = const Color(0xFFEAB308)
..strokeWidth = 2;
const len = 10.0;
canvas.drawLine(Offset(c.dx - len, c.dy), Offset(c.dx + len, c.dy), cross);
canvas.drawLine(Offset(c.dx, c.dy - len), Offset(c.dx, c.dy + len), cross);
}
@override
bool shouldRepaint(covariant _LockPainter old) {
return old.box != box || old.fitted != fitted || old.imageSize != imageSize;
}
}

View File

@ -0,0 +1,117 @@
import 'dart:typed_data';
import 'package:image/image.dart' as img;
import 'package:tflite_flutter/tflite_flutter.dart';
/// Blur / sharpness result from CoinSnap `blur.tflite`.
class BlurResult {
const BlurResult({
required this.blurScore,
required this.clearScore,
required this.isBlurry,
});
/// Softmax-ish class 0 = blur, class 1 = clear (from model).
final double blurScore;
final double clearScore;
final bool isBlurry;
double get confidence => isBlurry ? blurScore : clearScore;
}
/// CoinSnap blur classifier (`assets/ml/blur.tflite`).
///
/// Input: `[1, 3, 224, 224]` NCHW float32, letterbox on white, pixels / 255.
/// Output: `[1, 2]` `[blur, clear]`.
class BlurDetector {
BlurDetector({this.clearThreshold = 0.55});
static const int inputSize = 224;
static const String assetPath = 'assets/ml/blur.tflite';
/// Clear score must be this to pass (mirrors app-side threshold).
final double clearThreshold;
Interpreter? _interpreter;
bool _loading = false;
Future<void> ensureLoaded() async {
if (_interpreter != null || _loading) return;
_loading = true;
try {
_interpreter = await Interpreter.fromAsset(assetPath);
} finally {
_loading = false;
}
}
Future<BlurResult> evaluate(img.Image image) async {
await ensureLoaded();
final interp = _interpreter;
if (interp == null) {
return const BlurResult(blurScore: 0, clearScore: 1, isBlurry: false);
}
final canvas = _letterboxWhite(image, inputSize);
final flat = _toNchw(canvas);
// Nested list matches [1, 3, 224, 224] NCHW.
final inputNd = List.generate(
1,
(_) => List.generate(
3,
(c) => List.generate(
inputSize,
(y) => List.generate(inputSize, (x) {
final i = c * inputSize * inputSize + y * inputSize + x;
return flat[i];
}),
),
),
);
final output = List.generate(1, (_) => List<double>.filled(2, 0.0));
interp.run(inputNd, output);
final blur = output[0][0];
final clear = output[0][1];
final isBlurry = clear < clearThreshold;
return BlurResult(blurScore: blur, clearScore: clear, isBlurry: isBlurry);
}
Future<void> dispose() async {
_interpreter?.close();
_interpreter = null;
}
static img.Image _letterboxWhite(img.Image src, int size) {
final scale = size / (src.width > src.height ? src.width : src.height);
final nw = (src.width * scale).round().clamp(1, size);
final nh = (src.height * scale).round().clamp(1, size);
final resized = img.copyResize(
src,
width: nw,
height: nh,
interpolation: img.Interpolation.linear,
);
final canvas = img.Image(width: size, height: size);
img.fill(canvas, color: img.ColorRgb8(255, 255, 255));
final padX = ((size - nw) / 2).floor();
final padY = ((size - nh) / 2).floor();
img.compositeImage(canvas, resized, dstX: padX, dstY: padY);
return canvas;
}
static Float32List _toNchw(img.Image canvas) {
final out = Float32List(1 * 3 * inputSize * inputSize);
const plane = inputSize * inputSize;
for (var y = 0; y < inputSize; y++) {
for (var x = 0; x < inputSize; x++) {
final p = canvas.getPixel(x, y);
final i = y * inputSize + x;
out[i] = p.r / 255.0;
out[plane + i] = p.g / 255.0;
out[plane * 2 + i] = p.b / 255.0;
}
}
return out;
}
}

View File

@ -0,0 +1,96 @@
import 'dart:math' as math;
import 'dart:ui';
import 'package:image/image.dart' as img;
class RefinedCircle {
const RefinedCircle({required this.center, required this.radius});
final Offset center;
final double radius;
}
/// Refine an AABB into a circular coin contour via radial edge search.
class CoinContourRefiner {
static RefinedCircle? refine(img.Image image, Rect aabb) {
if (aabb.width < 16 || aabb.height < 16) return null;
final cx = aabb.center.dx;
final cy = aabb.center.dy;
final maxR = math.max(aabb.width, aabb.height) * 0.62;
final minR = math.min(aabb.width, aabb.height) * 0.28;
if (maxR <= minR + 2) {
return RefinedCircle(
center: Offset(cx, cy),
radius: math.min(aabb.width, aabb.height) / 2,
);
}
// Sample edge strength along rays; vote for radius with strongest ring.
const rays = 64;
final radiusVotes = <double>[];
for (var i = 0; i < rays; i++) {
final ang = (i / rays) * math.pi * 2;
final dx = math.cos(ang);
final dy = math.sin(ang);
var bestR = (minR + maxR) / 2;
var bestMag = -1.0;
for (var r = minR; r <= maxR; r += 1.5) {
final x1 = (cx + dx * r).round();
final y1 = (cy + dy * r).round();
final x0 = (cx + dx * (r - 2)).round();
final y0 = (cy + dy * (r - 2)).round();
if (!_in(image, x0, y0) || !_in(image, x1, y1)) continue;
final g0 = _gray(image, x0, y0);
final g1 = _gray(image, x1, y1);
final mag = (g1 - g0).abs();
if (mag > bestMag) {
bestMag = mag;
bestR = r;
}
}
if (bestMag > 8) radiusVotes.add(bestR);
}
if (radiusVotes.length < rays * 0.35) {
return RefinedCircle(
center: Offset(cx, cy),
radius: math.min(aabb.width, aabb.height) / 2 * 0.96,
);
}
radiusVotes.sort();
final radius = radiusVotes[radiusVotes.length ~/ 2] * 1.04;
// Slight center nudge from opposite ray midpoints.
var nx = 0.0;
var ny = 0.0;
var n = 0;
for (var i = 0; i < rays; i += 2) {
final ang = (i / rays) * math.pi * 2;
final r = radiusVotes[math.min(i, radiusVotes.length - 1)];
nx += cx + math.cos(ang) * (r - radius);
ny += cy + math.sin(ang) * (r - radius);
n++;
}
final center = n == 0
? Offset(cx, cy)
: Offset(
((nx / n) + cx) / 2,
((ny / n) + cy) / 2,
);
final clamped = Offset(
center.dx.clamp(radius, image.width - radius),
center.dy.clamp(radius, image.height - radius),
);
return RefinedCircle(center: clamped, radius: radius);
}
static bool _in(img.Image image, int x, int y) =>
x >= 0 && y >= 0 && x < image.width && y < image.height;
static double _gray(img.Image image, int x, int y) {
final p = image.getPixel(x, y);
return 0.299 * p.r + 0.587 * p.g + 0.114 * p.b;
}
}

View File

@ -0,0 +1,80 @@
import 'dart:io';
import 'dart:math' as math;
import 'dart:typed_data';
import 'dart:ui';
import 'package:image/image.dart' as img;
import 'coin_detector.dart';
class CropResult {
const CropResult({
required this.bytes,
required this.width,
required this.height,
required this.usedDetection,
});
final Uint8List bytes;
final int width;
final int height;
final bool usedDetection;
}
/// Circular crop of the detected coin (square output with transparent-ish bg).
class CoinCropper {
static Future<CropResult> cropAndCorrect({
required String imagePath,
CoinBox? box,
double padding = 0.12,
}) async {
final raw = await File(imagePath).readAsBytes();
var decoded = img.decodeImage(raw);
if (decoded == null) {
throw StateError('Failed to decode image: $imagePath');
}
decoded = img.bakeOrientation(decoded);
final usedDetection = box != null;
final center = box?.effectiveCenter ??
Offset(decoded.width / 2, decoded.height / 2);
var radius = box?.effectiveRadius ??
math.min(decoded.width, decoded.height) * 0.35;
radius *= (1 + padding);
final size = (radius * 2).round().clamp(32, 2048);
final out = img.Image(width: size, height: size, numChannels: 4);
img.fill(out, color: img.ColorRgba8(0, 0, 0, 0));
final r2 = radius * radius;
for (var y = 0; y < size; y++) {
for (var x = 0; x < size; x++) {
final dx = x + 0.5 - size / 2;
final dy = y + 0.5 - size / 2;
if (dx * dx + dy * dy > r2) continue;
final sx = (center.dx + dx).round();
final sy = (center.dy + dy).round();
if (sx < 0 || sy < 0 || sx >= decoded.width || sy >= decoded.height) {
continue;
}
final p = decoded.getPixel(sx, sy);
out.setPixelRgba(x, y, p.r.toInt(), p.g.toInt(), p.b.toInt(), 255);
}
}
const outSize = 320;
final resized = img.copyResize(
out,
width: outSize,
height: outSize,
interpolation: img.Interpolation.cubic,
);
final bytes = Uint8List.fromList(img.encodePng(resized));
return CropResult(
bytes: bytes,
width: resized.width,
height: resized.height,
usedDetection: usedDetection,
);
}
}

View File

@ -0,0 +1,251 @@
import 'dart:math' as math;
import 'dart:typed_data';
import 'dart:ui';
import 'package:image/image.dart' as img;
import 'package:tflite_flutter/tflite_flutter.dart';
import 'coin_contour_refiner.dart';
/// Detected coin region in image pixel coordinates.
class CoinBox {
const CoinBox({
required this.rect,
required this.confidence,
this.center,
this.radius,
});
/// Axis-aligned outer bounds.
final Rect rect;
final double confidence;
/// Refined circle (image pixels). When null, use [rect] inscribed circle.
final Offset? center;
final double? radius;
Offset get effectiveCenter =>
center ?? Offset(rect.center.dx, rect.center.dy);
double get effectiveRadius =>
radius ?? (math.min(rect.width, rect.height) / 2);
CoinBox copyWith({
Rect? rect,
double? confidence,
Offset? center,
double? radius,
}) {
return CoinBox(
rect: rect ?? this.rect,
confidence: confidence ?? this.confidence,
center: center ?? this.center,
radius: radius ?? this.radius,
);
}
}
/// CoinSnap-style YOLOv5 detector (`assets/ml/detect.tflite`).
///
/// Input: `[1, 320, 320, 3]` float32, pixels / 255, letterbox resize.
/// Output: `[1, 6300, 6]` `cx, cy, w, h, obj, cls` (already 01).
class CoinDetector {
CoinDetector();
static const int inputSize = 320;
static const double confThreshold = 0.35;
static const double iouThreshold = 0.45;
static const String assetPath = 'assets/ml/detect.tflite';
Interpreter? _interpreter;
bool _loading = false;
Future<void> ensureLoaded() async {
if (_interpreter != null || _loading) return;
_loading = true;
try {
_interpreter = await Interpreter.fromAsset(assetPath);
} finally {
_loading = false;
}
}
Future<List<CoinBox>> detectImage(img.Image image) async {
await ensureLoaded();
final interp = _interpreter;
if (interp == null) return const [];
final prepared = _letterbox(image, inputSize);
final input = _toInputTensor(prepared.canvas);
// Nested list matches [1, 6300, 6] expected by tflite_flutter.
final output = List.generate(
1,
(_) => List.generate(6300, (_) => List<double>.filled(6, 0.0)),
);
final inputNd = List.generate(
1,
(_) => List.generate(
inputSize,
(y) => List.generate(
inputSize,
(x) {
final i = (y * inputSize + x) * 3;
return [input[i], input[i + 1], input[i + 2]];
},
),
),
);
interp.run(inputNd, output);
final boxes = <CoinBox>[];
final rows = output[0];
for (final row in rows) {
final score = row[4] * row[5];
if (score < confThreshold) continue;
// Normalized cx,cy,w,h on the letterboxed 320 canvas.
final cx = row[0] * inputSize;
final cy = row[1] * inputSize;
final w = row[2] * inputSize;
final h = row[3] * inputSize;
// Map back from letterbox original image pixels.
final left = (cx - w / 2 - prepared.padX) / prepared.scale;
final top = (cy - h / 2 - prepared.padY) / prepared.scale;
final right = (cx + w / 2 - prepared.padX) / prepared.scale;
final bottom = (cy + h / 2 - prepared.padY) / prepared.scale;
final rect = Rect.fromLTRB(left, top, right, bottom).intersect(
Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()),
);
if (rect.width < 8 || rect.height < 8) continue;
boxes.add(CoinBox(rect: rect, confidence: score));
}
return _nms(boxes);
}
/// Prefer largest reasonably confident near-circular box.
CoinBox? pickBest(List<CoinBox> boxes, {Size? imageSize}) {
if (boxes.isEmpty) return null;
CoinBox? best;
var bestScore = -1.0;
for (final b in boxes) {
final area = b.rect.width * b.rect.height;
final imgArea = imageSize == null
? area
: (imageSize.width * imageSize.height).clamp(1.0, double.infinity);
final fill = area / imgArea;
if (fill < 0.02 || fill > 0.95) continue;
final aspect = b.rect.width / math.max(b.rect.height, 1.0);
final roundScore = 1.0 - (aspect - 1.0).abs().clamp(0.0, 1.0);
final score = b.confidence * 0.45 + fill * 0.25 + roundScore * 0.3;
if (score > bestScore) {
bestScore = score;
best = b;
}
}
return best ?? boxes.first;
}
/// Refine AABB into a circle using local edge intensity.
CoinBox refineContour(img.Image image, CoinBox box) {
final refined = CoinContourRefiner.refine(image, box.rect);
if (refined == null) {
final r = math.min(box.rect.width, box.rect.height) / 2;
return box.copyWith(center: box.rect.center, radius: r);
}
final side = refined.radius * 2;
final rect = Rect.fromCenter(
center: refined.center,
width: side,
height: side,
).intersect(
Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()),
);
return box.copyWith(
rect: rect,
center: refined.center,
radius: refined.radius,
);
}
Future<void> dispose() async {
_interpreter?.close();
_interpreter = null;
}
static Float32List _toInputTensor(img.Image canvas) {
final out = Float32List(1 * inputSize * inputSize * 3);
var i = 0;
for (var y = 0; y < inputSize; y++) {
for (var x = 0; x < inputSize; x++) {
final p = canvas.getPixel(x, y);
out[i++] = p.r / 255.0;
out[i++] = p.g / 255.0;
out[i++] = p.b / 255.0;
}
}
return out;
}
static _Letterbox _letterbox(img.Image src, int size) {
final scale = math.min(size / src.width, size / src.height);
final nw = math.max(1, (src.width * scale).round());
final nh = math.max(1, (src.height * scale).round());
final resized = img.copyResize(
src,
width: nw,
height: nh,
interpolation: img.Interpolation.linear,
);
final canvas = img.Image(width: size, height: size);
img.fill(canvas, color: img.ColorRgb8(114, 114, 114));
final padX = ((size - nw) / 2).floor();
final padY = ((size - nh) / 2).floor();
img.compositeImage(canvas, resized, dstX: padX, dstY: padY);
return _Letterbox(canvas: canvas, scale: scale, padX: padX.toDouble(), padY: padY.toDouble());
}
static List<CoinBox> _nms(List<CoinBox> boxes) {
final sorted = [...boxes]..sort((a, b) => b.confidence.compareTo(a.confidence));
final kept = <CoinBox>[];
final suppressed = List<bool>.filled(sorted.length, false);
for (var i = 0; i < sorted.length; i++) {
if (suppressed[i]) continue;
kept.add(sorted[i]);
for (var j = i + 1; j < sorted.length; j++) {
if (suppressed[j]) continue;
if (_iou(sorted[i].rect, sorted[j].rect) > iouThreshold) {
suppressed[j] = true;
}
}
}
return kept;
}
static double _iou(Rect a, Rect b) {
final inter = a.intersect(b);
if (inter.isEmpty) return 0;
final interArea = inter.width * inter.height;
final union = a.width * a.height + b.width * b.height - interArea;
return union <= 0 ? 0 : interArea / union;
}
}
class _Letterbox {
const _Letterbox({
required this.canvas,
required this.scale,
required this.padX,
required this.padY,
});
final img.Image canvas;
final double scale;
final double padX;
final double padY;
}

View File

@ -0,0 +1,122 @@
import 'dart:io';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:image/image.dart' as img;
import 'package:path_provider/path_provider.dart';
import '../widgets/guide_frame.dart';
import 'blur_detector.dart';
import 'coin_detector.dart';
enum CoinGateFail { none, blurry, noCoin }
class CoinGateResult {
const CoinGateResult({
required this.ok,
this.fail = CoinGateFail.none,
this.blur,
this.box,
});
final bool ok;
final CoinGateFail fail;
final BlurResult? blur;
final CoinBox? box;
}
/// Post-capture gate: blur.tflite detect.tflite (CoinSnap order).
class CoinScanGate {
static Future<CoinGateResult> evaluate({
required String imagePath,
Size? captureViewport,
CoinDetector? detector,
BlurDetector? blurDetector,
}) async {
final ownedDet = detector == null;
final ownedBlur = blurDetector == null;
final det = detector ?? CoinDetector();
final blur = blurDetector ?? BlurDetector();
try {
final raster = await _guideCrop(imagePath, captureViewport);
if (raster == null) {
return const CoinGateResult(ok: false, fail: CoinGateFail.noCoin);
}
final blurResult = await blur.evaluate(raster);
if (blurResult.isBlurry) {
return CoinGateResult(
ok: false,
fail: CoinGateFail.blurry,
blur: blurResult,
);
}
final boxes = await det.detectImage(raster);
final best = det.pickBest(
boxes,
imageSize: Size(raster.width.toDouble(), raster.height.toDouble()),
);
if (best == null) {
return CoinGateResult(
ok: false,
fail: CoinGateFail.noCoin,
blur: blurResult,
);
}
return CoinGateResult(ok: true, blur: blurResult, box: best);
} finally {
if (ownedDet) await det.dispose();
if (ownedBlur) await blur.dispose();
}
}
static Future<img.Image?> _guideCrop(
String imagePath,
Size? captureViewport,
) async {
final raw = await File(imagePath).readAsBytes();
final codec = await ui.instantiateImageCodec(raw);
final frame = await codec.getNextFrame();
final fullImage = frame.image;
final rgba = await fullImage.toByteData(format: ui.ImageByteFormat.rawRgba);
if (rgba == null) {
fullImage.dispose();
return null;
}
final fullRaster = img.Image.fromBytes(
width: fullImage.width,
height: fullImage.height,
bytes: rgba.buffer,
bytesOffset: rgba.offsetInBytes,
numChannels: 4,
order: img.ChannelOrder.rgba,
);
fullImage.dispose();
final imageSize = Size(
fullRaster.width.toDouble(),
fullRaster.height.toDouble(),
);
final guide = captureViewport != null
? GuideFrame.inCoverImage(viewport: captureViewport, imageSize: imageSize)
: GuideFrame.inImage(imageSize);
final gx = guide.left.round().clamp(0, fullRaster.width - 1);
final gy = guide.top.round().clamp(0, fullRaster.height - 1);
final gw = guide.width.round().clamp(1, fullRaster.width - gx);
final gh = guide.height.round().clamp(1, fullRaster.height - gy);
return img.copyCrop(fullRaster, x: gx, y: gy, width: gw, height: gh);
}
/// Persist guide crop for preview reuse.
static Future<String> writeGuideJpeg(img.Image raster) async {
final dir = await getTemporaryDirectory();
final path =
'${dir.path}/coin_guide_${DateTime.now().millisecondsSinceEpoch}.jpg';
await File(path).writeAsBytes(img.encodeJpg(raster, quality: 95), flush: true);
return path;
}
}

View File

@ -0,0 +1,44 @@
import 'dart:math' as math;
import 'dart:ui';
/// Circular capture guide (coin frame).
class GuideFrame {
static const double maxDiameterFraction = 0.72;
static const double centerYFraction = 0.46;
static Rect inViewport(Size size) {
final d = math.min(size.width, size.height) * maxDiameterFraction;
return Rect.fromCenter(
center: Offset(size.width / 2, size.height * centerYFraction),
width: d,
height: d,
);
}
static Rect inCoverImage({
required Size viewport,
required Size imageSize,
}) {
final guide = inViewport(viewport);
final scale = math.max(
viewport.width / imageSize.width,
viewport.height / imageSize.height,
);
final dispW = imageSize.width * scale;
final dispH = imageSize.height * scale;
final ox = (viewport.width - dispW) / 2;
final oy = (viewport.height - dispH) / 2;
final mapped = Rect.fromLTRB(
(guide.left - ox) / scale,
(guide.top - oy) / scale,
(guide.right - ox) / scale,
(guide.bottom - oy) / scale,
);
return mapped.intersect(
Rect.fromLTWH(0, 0, imageSize.width, imageSize.height),
);
}
static Rect inImage(Size imageSize) => inViewport(imageSize);
}

View File

@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
import 'guide_frame.dart';
/// Circular shooting guide overlay.
class GuideOverlay extends StatelessWidget {
const GuideOverlay({super.key, required this.size});
final Size size;
@override
Widget build(BuildContext context) {
return CustomPaint(
size: size,
painter: _GuidePainter(),
);
}
}
class _GuidePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final rect = GuideFrame.inViewport(size);
final dim = Paint()..color = Colors.black.withOpacity(0.45);
final path = Path()
..addRect(Offset.zero & size)
..addOval(rect)
..fillType = PathFillType.evenOdd;
canvas.drawPath(path, dim);
final border = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 2
..color = Colors.white70;
canvas.drawOval(rect, border);
// Crosshair ticks
final tick = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 2
..color = const Color(0xFFEAB308);
final c = rect.center;
const len = 14.0;
canvas.drawLine(Offset(c.dx - len, c.dy), Offset(c.dx + len, c.dy), tick);
canvas.drawLine(Offset(c.dx, c.dy - len), Offset(c.dx, c.dy + len), tick);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

554
pubspec.lock Normal file
View File

@ -0,0 +1,554 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
archive:
dependency: transitive
description:
name: archive
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
url: "https://pub.dev"
source: hosted
version: "4.0.9"
async:
dependency: transitive
description:
name: async
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
url: "https://pub.dev"
source: hosted
version: "2.11.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
camera:
dependency: "direct main"
description:
name: camera
sha256: dfa8fc5a1adaeb95e7a54d86a5bd56f4bb0e035515354c8ac6d262e35cec2ec8
url: "https://pub.dev"
source: hosted
version: "0.10.6"
camera_android:
dependency: transitive
description:
name: camera_android
sha256: "32f04948a284b71d938fe275616faf4957d07f9b3aab8021bfc8c418301a289e"
url: "https://pub.dev"
source: hosted
version: "0.10.9+11"
camera_avfoundation:
dependency: transitive
description:
name: camera_avfoundation
sha256: ca36181194f429eef3b09de3c96280f2400693f9735025f90d1f4a27465fdd72
url: "https://pub.dev"
source: hosted
version: "0.9.19"
camera_platform_interface:
dependency: transitive
description:
name: camera_platform_interface
sha256: "2f757024a48696ff4814a789b0bd90f5660c0fb25f393ab4564fb483327930e2"
url: "https://pub.dev"
source: hosted
version: "2.10.0"
camera_web:
dependency: transitive
description:
name: camera_web
sha256: "595f28c89d1fb62d77c73c633193755b781c6d2e0ebcd8dc25b763b514e6ba8f"
url: "https://pub.dev"
source: hosted
version: "0.3.5"
characters:
dependency: transitive
description:
name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
clock:
dependency: transitive
description:
name: clock
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
url: "https://pub.dev"
source: hosted
version: "1.1.1"
collection:
dependency: transitive
description:
name: collection
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
url: "https://pub.dev"
source: hosted
version: "1.18.0"
cross_file:
dependency: transitive
description:
name: cross_file
sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
url: "https://pub.dev"
source: hosted
version: "0.3.4+2"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
url: "https://pub.dev"
source: hosted
version: "1.0.8"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
url: "https://pub.dev"
source: hosted
version: "1.3.1"
ffi:
dependency: transitive
description:
name: ffi
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
file_selector_linux:
dependency: transitive
description:
name: file_selector_linux
sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33"
url: "https://pub.dev"
source: hosted
version: "0.9.3+2"
file_selector_macos:
dependency: transitive
description:
name: file_selector_macos
sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc"
url: "https://pub.dev"
source: hosted
version: "0.9.4+2"
file_selector_platform_interface:
dependency: transitive
description:
name: file_selector_platform_interface
sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b
url: "https://pub.dev"
source: hosted
version: "2.6.2"
file_selector_windows:
dependency: transitive
description:
name: file_selector_windows
sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b"
url: "https://pub.dev"
source: hosted
version: "0.9.3+4"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: "9ee02950848f61c4129af3d6ec84a1cfc0e47931abc746b03e7a3bc3e8ff6eda"
url: "https://pub.dev"
source: hosted
version: "2.0.22"
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"
http:
dependency: transitive
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev"
source: hosted
version: "1.6.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
url: "https://pub.dev"
source: hosted
version: "4.0.2"
image:
dependency: "direct main"
description:
name: image
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
url: "https://pub.dev"
source: hosted
version: "4.8.0"
image_picker:
dependency: "direct main"
description:
name: image_picker
sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a"
url: "https://pub.dev"
source: hosted
version: "1.1.2"
image_picker_android:
dependency: transitive
description:
name: image_picker_android
sha256: "8c5abf0dcc24fe6e8e0b4a5c0b51a5cf30cefdf6407a3213dae61edc75a70f56"
url: "https://pub.dev"
source: hosted
version: "0.8.12+12"
image_picker_for_web:
dependency: transitive
description:
name: image_picker_for_web
sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83"
url: "https://pub.dev"
source: hosted
version: "3.0.6"
image_picker_ios:
dependency: transitive
description:
name: image_picker_ios
sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100"
url: "https://pub.dev"
source: hosted
version: "0.8.12+2"
image_picker_linux:
dependency: transitive
description:
name: image_picker_linux
sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9"
url: "https://pub.dev"
source: hosted
version: "0.2.1+2"
image_picker_macos:
dependency: transitive
description:
name: image_picker_macos
sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1"
url: "https://pub.dev"
source: hosted
version: "0.2.1+2"
image_picker_platform_interface:
dependency: transitive
description:
name: image_picker_platform_interface
sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0"
url: "https://pub.dev"
source: hosted
version: "2.10.1"
image_picker_windows:
dependency: transitive
description:
name: image_picker_windows
sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb"
url: "https://pub.dev"
source: hosted
version: "0.2.1+1"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a"
url: "https://pub.dev"
source: hosted
version: "10.0.4"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8"
url: "https://pub.dev"
source: hosted
version: "3.0.3"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
lints:
dependency: transitive
description:
name: lints
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
url: "https://pub.dev"
source: hosted
version: "3.0.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
url: "https://pub.dev"
source: hosted
version: "0.12.16+1"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
url: "https://pub.dev"
source: hosted
version: "0.8.0"
meta:
dependency: transitive
description:
name: meta
sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
url: "https://pub.dev"
source: hosted
version: "1.12.0"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
path:
dependency: "direct main"
description:
name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
url: "https://pub.dev"
source: hosted
version: "1.9.0"
path_provider:
dependency: "direct main"
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7"
url: "https://pub.dev"
source: hosted
version: "2.2.10"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
url: "https://pub.dev"
source: hosted
version: "6.0.2"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
posix:
dependency: transitive
description:
name: posix
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
url: "https://pub.dev"
source: hosted
version: "6.5.0"
quiver:
dependency: transitive
description:
name: quiver
sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2
url: "https://pub.dev"
source: hosted
version: "3.2.2"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_span:
dependency: transitive
description:
name: source_span
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://pub.dev"
source: hosted
version: "1.10.0"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
url: "https://pub.dev"
source: hosted
version: "1.11.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://pub.dev"
source: hosted
version: "2.1.2"
stream_transform:
dependency: transitive
description:
name: stream_transform
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
url: "https://pub.dev"
source: hosted
version: "2.1.1"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
url: "https://pub.dev"
source: hosted
version: "1.2.1"
test_api:
dependency: transitive
description:
name: test_api
sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
url: "https://pub.dev"
source: hosted
version: "0.7.0"
tflite_flutter:
dependency: "direct main"
description:
name: tflite_flutter
sha256: "48e6fde2ad97162bb66a16a142f4c4698add9e8cd397ce9d1cc7451b55537ac1"
url: "https://pub.dev"
source: hosted
version: "0.11.0"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
url: "https://pub.dev"
source: hosted
version: "1.3.2"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
url: "https://pub.dev"
source: hosted
version: "14.2.1"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
xml:
dependency: transitive
description:
name: xml
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
url: "https://pub.dev"
source: hosted
version: "6.5.0"
sdks:
dart: ">=3.4.0 <4.0.0"
flutter: ">=3.22.0"

29
pubspec.yaml Normal file
View File

@ -0,0 +1,29 @@
name: coin_dex_demo
description: Coin scan demo — capture, blur check, detect contour, crop.
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: '>=3.4.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.6
camera: 0.10.6
tflite_flutter: ^0.11.0
image: ^4.3.0
image_picker: ^1.1.2
path_provider: ^2.1.4
path: ^1.9.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
flutter:
uses-material-design: true
assets:
- assets/ml/detect.tflite
- assets/ml/blur.tflite

10
test/widget_test.dart Normal file
View File

@ -0,0 +1,10 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:coin_dex_demo/main.dart';
void main() {
testWidgets('app builds', (tester) async {
await tester.pumpWidget(const CoinScanDemoApp());
expect(find.text('硬币扫描'), findsOneWidget);
});
}