First Commit

This commit is contained in:
2026-04-17 00:37:21 +08:00
parent f445b408a4
commit fb33d42ab1
83 changed files with 3832 additions and 2 deletions
+45
View File
@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
+36
View File
@@ -0,0 +1,36 @@
# 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: "db50e20168db8fee486b9abf32fc912de3bc5b6a"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
- platform: android
create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
- platform: ios
create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
- platform: web
create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
# 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'
+16 -2
View File
@@ -1,3 +1,17 @@
# house_hunter
# home_hunter
看屋筆記
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
+28
View File
@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
+14
View File
@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks
+44
View File
@@ -0,0 +1,44 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "tw.com.gex.homehunter.home_hunter"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "tw.com.gex.homehunter.home_hunter"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+47
View File
@@ -0,0 +1,47 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="home_hunter"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -0,0 +1,5 @@
package tw.com.gex.homehunter.home_hunter
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+24
View File
@@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
+2
View File
@@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
+5
View File
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
+26
View File
@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")
+34
View File
@@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3
+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
</dict>
</plist>
+1
View File
@@ -0,0 +1 @@
#include "Generated.xcconfig"
+1
View File
@@ -0,0 +1 @@
#include "Generated.xcconfig"
+620
View File
@@ -0,0 +1,620 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = tw.com.gex.homehunter.homeHunter;
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 = tw.com.gex.homehunter.homeHunter.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 = tw.com.gex.homehunter.homeHunter.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 = tw.com.gex.homehunter.homeHunter.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = tw.com.gex.homehunter.homeHunter;
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 = tw.com.gex.homehunter.homeHunter;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
+16
View File
@@ -0,0 +1,16 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
}
@@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

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

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

@@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>
+26
View File
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>
+70
View File
@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Home Hunter</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>home_hunter</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
+1
View File
@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"
+6
View File
@@ -0,0 +1,6 @@
import Flutter
import UIKit
class SceneDelegate: FlutterSceneDelegate {
}
+12
View File
@@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}
+173
View File
@@ -0,0 +1,173 @@
import 'package:flutter/material.dart';
import './house_note_model.dart';
import './house_note_api.dart';
// import 'package:uuid/uuid.dart';
class HouseEvaluationForm extends StatefulWidget {
final String houseId;
final String houseTitle;
const HouseEvaluationForm({required this.houseId, required this.houseTitle, super.key});
@override
State<HouseEvaluationForm> createState() => _HouseEvaluationFormState();
}
class _HouseEvaluationFormState extends State<HouseEvaluationForm> {
final _api = HouseNoteApiService();
bool _isLoading = true;
List<CheckCategory> _categories = [];
List<CheckItem> _allItems = [];
final Map<String, HouseEvaluation> _evalMap = {}; // Key: checkItemId
@override
void initState() {
super.initState();
_loadData();
}
Future<void> _loadData() async {
try {
final results = await Future.wait([
_api.fetchCategories(),
_api.fetchAllCheckItems(),
_api.fetchEvaluations(widget.houseId),
]);
if (mounted) {
setState(() {
_categories = results[0] as List<CheckCategory>;
_allItems = results[1] as List<CheckItem>;
// 將已有的評估紀錄放入 Map
final existingEvals = results[2] as List<HouseEvaluation>;
for (var e in existingEvals) {
_evalMap[e.checkItemId] = e;
}
_isLoading = false;
});
}
} catch (e) {
debugPrint("載入失敗: $e");
// 【關鍵修正】避免發生錯誤時畫面永遠轉圈圈(無反應)
if (mounted) {
setState(() => _isLoading = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('資料載入失敗,請檢查網路或 API: $e')),
);
}
}
}
Future<void> _saveAll() async {
setState(() => _isLoading = true);
// 只儲存有打分數 (score > 0) 的項目
final toSave = _evalMap.values.where((e) => e.score > 0).toList();
for (var eval in toSave) {
await _api.saveSingleEvaluation(eval);
}
if (mounted) Navigator.pop(context, true);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('${widget.houseTitle} - 賞屋評估'),
actions: [
IconButton(icon: const Icon(Icons.check), onPressed: _isLoading ? null : _saveAll),
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: ListView.builder(
itemCount: _categories.length,
itemBuilder: (context, catIdx) {
final cat = _categories[catIdx];
final items = _allItems.where((i) => i.categoryId == cat.id).toList();
return ExpansionTile(
initiallyExpanded: catIdx == 0,
title: Text(cat.name, style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.blue)),
children: items.map((item) => _buildItemRow(item)).toList(),
);
},
),
);
}
Widget _buildItemRow(CheckItem item) {
// 取得或初始化該項目的評估狀態
final eval = _evalMap.putIfAbsent(
item.id,
() => HouseEvaluation(
houseId: widget.houseId,
checkItemId: item.id,
itemName: item.name, // 【關鍵修正】把項目的中文名稱也傳進去
)
);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(item.name, style: const TextStyle(fontSize: 15)),
const SizedBox(height: 4),
Row(
children: [
// 1-5 分評分 (星星)
...List.generate(5, (index) {
int starValue = index + 1;
return IconButton(
icon: Icon(
starValue <= eval.score ? Icons.star : Icons.star_border,
color: Colors.orange,
),
onPressed: () => setState(() => eval.score = starValue),
);
}),
const Spacer(),
// 備註按鈕
IconButton(
icon: Icon(Icons.note_alt_outlined, color: (eval.itemMemo?.isNotEmpty ?? false) ? Colors.blue : Colors.grey),
onPressed: () => _editMemo(eval),
),
],
),
const Divider(),
],
),
);
}
void _editMemo(HouseEvaluation eval) {
final controller = TextEditingController(text: eval.itemMemo);
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('項目備註'),
content: TextField(
controller: controller,
maxLines: 3,
decoration: const InputDecoration(hintText: '輸入屋況細節 (如:疑似壁癌...)'),
),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('取消')),
TextButton(
onPressed: () {
setState(() => eval.itemMemo = controller.text);
Navigator.pop(context);
},
child: const Text('確定')
),
],
),
);
}
}
+227
View File
@@ -0,0 +1,227 @@
import '../../services/generic_api_service.dart';
// import '../auth_manager.dart';
import 'house_note_model.dart';
import 'package:intl/intl.dart';
import 'package:uuid/uuid.dart';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'dart:convert'; // 補上這行,json.decode 才能運作
class HouseNoteApiService {
final GenericApiService _apiService = GenericApiService();
// 獲取所有房屋列表
Future<List<House>> fetchHouses() async {
// 依據建立時間降冪排序
String queryFilter = "1^100^created_at^*^^^";
return await _apiService.fetchList<House>(
tableName: "hhp_houses",
pk: "id",
queryFilter: queryFilter,
fromJson: (json) => House.fromJson(json),
);
}
// 創建新物件 (UUID 在前端生成)
Future<List<House>> createHouse(House house) async {
final Map<String, dynamic> data = {
"id": house.id,
"title": house.title,
"address": house.address,
"latitude": house.latitude,
"longitude": house.longitude,
"total_price": house.totalPrice,
"total_ping": house.totalPing,
"property_type": house.propertyType,
"age": house.age,
"summary_notes": house.summaryNotes,
"created_at": DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()),
};
return await _apiService.fetchList<House>(
tableName: "hhp_houses",
pk: "id",
queryFilter: "", // 【關鍵修正】補上必填參數,傳入空字串即可
action: "C",
data: data,
fromJson: (json) => House.fromJson(json),
);
}
// 獲取預設檢查清單
Future<List<CheckItem>> fetchDefaultCheckItems() async {
return await _apiService.fetchList<CheckItem>(
tableName: "hhp_check_items",
pk: "id",
queryFilter: "1^200^sort_order^*^^^is_default^1",
fromJson: (json) => CheckItem.fromJson(json),
);
}
// 上傳照片檔案 (參考 expense_api)
Future<String?> uploadImage(File imageFile) async {
try {
final request = http.MultipartRequest(
'POST',
// Uri.parse("${GenericApiService.BASE_IP}/xapi/v2/common/upload_file"),
Uri.parse("${GenericApiService.BASE_IP}/upload/eis_demo/images")
);
// 取得本地檔案名稱
String picFileName = "${GenericApiService.BASE_IP}/upload/eis_demo/images/${imageFile.path.split('/').last}";
request.files.add(await http.MultipartFile.fromPath('file', imageFile.path));
final streamedResponse = await request.send();
final response = await http.Response.fromStream(streamedResponse);
if (response.statusCode == 200) {
// final data = json.decode(response.body);
// return data['fileName'];
return picFileName; // 這是回傳後端產生的檔名
}
} catch (e) {
print("圖片上傳異常: $e");
}
return null;
}
// 儲存照片關聯資訊
Future<bool> saveHousePhoto(String houseId, String fileName, String note) async {
final Map<String, dynamic> data = {
"id": const Uuid().v4(),
"house_id": houseId,
"file_path": fileName,
"description": note,
"created_at": DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()),
};
final result = await _apiService.fetchList<dynamic>(
tableName: "hhp_house_photos",
pk: "id",
queryFilter: "",
action: "C",
data: data,
fromJson: (json) => json,
);
return result.isNotEmpty;
}
// 獲取特定房屋的所有評估紀錄 (需 Join 項目名稱)
Future<List<HouseEvaluation>> fetchEvaluations(String houseId) async {
// 這裡假設後端會處理 Join hhp_check_items 取得名稱
String queryFilter = "1^100^id^*^^hhpm02^house_id^$houseId";
return await _apiService.fetchList<HouseEvaluation>(
tableName: "hhp_house_evaluations",
pk: "id",
queryFilter: queryFilter,
fromJson: (json) => HouseEvaluation.fromJson(json),
);
}
// 獲取特定房屋的所有照片
Future<List<HousePhoto>> fetchPhotos(String houseId) async {
String queryFilter = "1^100^created_at^*^^^house_id^$houseId";
return await _apiService.fetchList<HousePhoto>(
tableName: "hhp_house_photos",
pk: "id",
queryFilter: queryFilter,
fromJson: (json) => HousePhoto.fromJson(json),
);
}
// house_note_api.dart 新增方法
// 1. 獲取所有可用標籤庫 (hhp_tags)
Future<List<Tag>> fetchAllTags() async {
return await _apiService.fetchList<Tag>(
tableName: "hhp_tags",
pk: "id",
queryFilter: "1^100^id^*^^^",
fromJson: (json) => Tag.fromJson(json),
);
}
// 2. 獲取特定房屋已選取的標籤 ID 清單 (hhp_house_tags)
Future<List<String>> fetchHouseTagIds(String houseId) async {
final List<dynamic> result = await _apiService.fetchList<dynamic>(
tableName: "hhp_house_tags",
pk: "house_id", // 複合主鍵通常用其中一個,視 API 規範而定
queryFilter: "1^100^tag_id^*^^^house_id^$houseId",
fromJson: (json) => json,
);
return result.map((e) => e['tag_id'].toString()).toList();
}
// 3. 儲存/更新房屋標籤 (hhp_house_tags)
// 邏輯:通常是先刪除舊的再寫入新的,這裡簡化為寫入邏輯
Future<void> updateHouseTags(String houseId, List<String> selectedTagIds) async {
// A. 刪除該房屋舊有的所有標籤關聯 (假設 action 'D' 為刪除)
await _apiService.fetchList<dynamic>(
tableName: "hhp_house_tags",
pk: "house_id",
queryFilter: "house_id^$houseId",
action: "D",
fromJson: (json) => json,
);
// B. 逐一寫入新的標籤關聯
for (String tagId in selectedTagIds) {
await _apiService.fetchList<dynamic>(
tableName: "hhp_house_tags",
pk: "house_id",
queryFilter: "",
action: "C",
data: {
"house_id": houseId,
"tag_id": tagId,
},
fromJson: (json) => json,
);
}
}
// 1. 獲取所有檢查大類 (hhp_check_categories)
Future<List<CheckCategory>> fetchCategories() async {
return await _apiService.fetchList<CheckCategory>(
tableName: "hhp_check_categories",
pk: "id",
queryFilter: "1^100^sort_order^*^^^",
fromJson: (json) => CheckCategory.fromJson(json),
);
}
// 2. 儲存/更新評估紀錄
// 由於評估通常是多筆,我們這裡實作單筆提交,呼叫端可用迴圈或 Future.wait
Future<bool> saveSingleEvaluation(HouseEvaluation eval) async {
final Map<String, dynamic> data = {
"id": eval.id ?? const Uuid().v4(),
"house_id": eval.houseId,
"check_item_id": eval.checkItemId,
"score": eval.score,
"item_memo": eval.itemMemo ?? "",
"updated_at": DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()),
};
final result = await _apiService.fetchList<dynamic>(
tableName: "hhp_house_evaluations",
pk: "id",
queryFilter: "",
action: "C", // 使用 Create/Update
data: data,
fromJson: (json) => json,
);
return result.isNotEmpty;
}
// 獲取所有檢查項目 (包含非預設項目,用於表單與名稱對應)
Future<List<CheckItem>> fetchAllCheckItems() async {
return await _apiService.fetchList<CheckItem>(
tableName: "hhp_check_items",
pk: "id",
// 撈取所有項目,以 sort_order 排序
queryFilter: "1^200^sort_order^*^^^",
fromJson: (json) => CheckItem.fromJson(json),
);
}
}
+394
View File
@@ -0,0 +1,394 @@
import 'package:flutter/material.dart';
import './house_note_model.dart';
import './house_note_api.dart';
import './house_photo_form.dart';
import './house_tag_form.dart';
import './house_evaluation_form.dart';
class HouseNoteDetail extends StatefulWidget {
final House house;
const HouseNoteDetail({required this.house, super.key});
@override
State<HouseNoteDetail> createState() => _HouseNoteDetailState();
}
class _HouseNoteDetailState extends State<HouseNoteDetail> {
final HouseNoteApiService _api = HouseNoteApiService();
List<HouseEvaluation> _evaluations = [];
List<HousePhoto> _photos = [];
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadAllData();
}
// 同步抓取評估紀錄與照片
Future<void> _loadAllData() async {
setState(() => _isLoading = true);
try {
final results = await Future.wait([
_api.fetchEvaluations(widget.house.id),
_api.fetchPhotos(widget.house.id),
]);
if (mounted) {
setState(() {
_evaluations = results[0] as List<HouseEvaluation>;
_photos = results[1] as List<HousePhoto>;
_isLoading = false;
});
}
} catch (e) {
if (mounted) {
setState(() => _isLoading = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('資料載入失敗: $e')),
);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('賞屋詳細內容'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _loadAllData,
)
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
child: Column(
children: [
// a. 基本資料區塊
_buildSectionHeader('房屋基本資料', Icons.info_outline),
_buildBasicInfoCard(),
// b. 屋況檢查區塊 (與照片主鍵關聯)
_buildSectionHeader('屋況檢查評估', Icons.checklist_rtl),
_buildEvaluationList(),
// c. 照片總覽區塊
_buildSectionHeader('照片總覽 (點擊可放大)', Icons.photo_library_outlined),
_buildPhotoGrid(),
const SizedBox(height: 100), // 留白空間
],
),
),
// 底部懸浮按鈕區
bottomSheet: _buildBottomActions(context),
);
}
// --- UI 元件函式 ---
// 分區標題
Widget _buildSectionHeader(String title, IconData icon) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
color: Colors.grey.shade100,
child: Row(
children: [
Icon(icon, size: 20, color: Colors.blueAccent),
const SizedBox(width: 8),
Text(title, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
],
),
);
}
// 基本資料卡片
Widget _buildBasicInfoCard() {
return Padding(
padding: const EdgeInsets.all(16),
child: Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: Colors.grey.shade200),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
_buildDetailRow(Icons.home, '物件名稱', widget.house.title),
const Divider(),
_buildDetailRow(Icons.location_on, '完整地址', widget.house.address ?? '未填寫'),
const Divider(),
_buildDetailRow(Icons.monetization_on, '預估價格', widget.house.priceTag),
const Divider(),
_buildDetailRow(Icons.layers, '物件規格', widget.house.infoSummary),
],
),
),
),
);
}
// 輔助明細列
Widget _buildDetailRow(IconData icon, String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 18, color: Colors.grey),
const SizedBox(width: 8),
Text(label, style: const TextStyle(color: Colors.grey, fontSize: 14)),
const Spacer(),
Expanded(
child: Text(
value,
textAlign: TextAlign.end,
style: const TextStyle(fontWeight: FontWeight.w600),
),
),
],
),
);
}
// 屋況檢查清單
Widget _buildEvaluationList() {
if (_evaluations.isEmpty) {
return const Padding(
padding: EdgeInsets.all(30),
child: Text('目前尚無評估數據', style: TextStyle(color: Colors.grey)),
);
}
return ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _evaluations.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) {
final eval = _evaluations[index];
// 關鍵:找出有沒有照片是關聯到這個評估項目的 (透過 evaluation_id)
final HousePhoto? linkedPhoto = _photos.cast<HousePhoto?>().firstWhere(
(p) => p?.evaluationId == eval.id,
orElse: () => null,
);
return ListTile(
title: Text(eval.itemName, style: const TextStyle(fontWeight: FontWeight.w500)),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: List.generate(5, (i) => Icon(
Icons.star,
size: 16,
color: i < eval.score ? Colors.orange : Colors.grey.shade300
)),
),
if (eval.itemMemo != null && eval.itemMemo!.isNotEmpty)
Text(eval.itemMemo!, style: const TextStyle(fontSize: 12, color: Colors.blueGrey)),
],
),
trailing: linkedPhoto != null
? GestureDetector(
onTap: () => _showFullScreenImage(context, linkedPhoto.fullUrl),
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: Image.network(
linkedPhoto.fullUrl,
width: 50, height: 50,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => const Icon(Icons.broken_image, size: 30),
),
),
)
: null,
);
},
);
}
// 照片總覽 Grid
Widget _buildPhotoGrid() {
if (_photos.isEmpty) {
return const Padding(
padding: EdgeInsets.all(30),
child: Text('尚未拍攝照片', style: TextStyle(color: Colors.grey)),
);
}
return GridView.builder(
padding: const EdgeInsets.all(16),
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: _photos.length,
itemBuilder: (context, index) {
final photo = _photos[index];
return GestureDetector(
onTap: () => _showFullScreenImage(context, photo.fullUrl),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
photo.fullUrl,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Container(
color: Colors.grey.shade200,
child: const Icon(Icons.broken_image, color: Colors.grey),
),
),
),
);
},
);
}
// --- 互動功能 ---
// 圖片放大功能 (支援雙指縮放)
void _showFullScreenImage(BuildContext context, String imageUrl) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(backgroundColor: Colors.black, foregroundColor: Colors.white),
body: Center(
child: InteractiveViewer(
panEnabled: true,
minScale: 0.5,
maxScale: 4.0,
child: Image.network(
imageUrl,
fit: BoxFit.contain,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return const CircularProgressIndicator(color: Colors.white);
},
),
),
),
),
),
);
}
// 底部按鈕
Widget _buildBottomActions(BuildContext context) {
return Container(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 32),
decoration: BoxDecoration(
color: Colors.white,
border: Border(top: BorderSide(color: Colors.grey.shade200)),
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10)]
),
child: Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () async {
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => HouseEvaluationForm(
houseId: widget.house.id,
houseTitle: widget.house.title,
),
),
);
// 返回後刷新詳情頁
if (result == true) {
_loadAllData();
}
},
icon: const Icon(Icons.rate_review),
label: const Text('賞屋評估'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), // 保持與拍照按鈕一致的圓角
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: () async {
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => HousePhotoForm(
houseId: widget.house.id,
houseTitle: widget.house.title
)
)
);
// 拍完照回來自動刷新頁面
if (result == true) {
_loadAllData();
}
},
icon: const Icon(Icons.camera_alt),
label: const Text('拍照紀錄'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), // 保持與拍照按鈕一致的圓角
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: () async {
// 跳轉至標籤編輯頁面 (HouseTagForm)
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => HouseTagForm(
houseId: widget.house.id,
houseTitle: widget.house.title,
),
),
);
// 當從標籤頁面返回,且 result 為 true 時,自動刷新詳情頁資料
if (result == true) {
_loadAllData();
}
},
icon: const Icon(Icons.local_offer_outlined),
label: const Text('編輯標籤'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.teal, // 背景顏色設定為藍綠色
foregroundColor: Colors.white, // 文字與圖示顏色設定為白色
padding: const EdgeInsets.symmetric(vertical: 12), // 設定垂直內距
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), // 保持與拍照按鈕一致的圓角
),
),
),
),
],
),
);
}
}
+103
View File
@@ -0,0 +1,103 @@
import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';
import 'house_note_model.dart';
import 'house_note_api.dart';
class HouseNoteForm extends StatefulWidget {
final House? house; // 若傳入則為編輯
const HouseNoteForm({this.house, super.key});
@override
State<HouseNoteForm> createState() => _HouseNoteFormState();
}
class _HouseNoteFormState extends State<HouseNoteForm> {
final _formKey = GlobalKey<FormState>();
final _api = HouseNoteApiService();
late String _title;
String? _address;
double? _totalPrice;
double? _totalPing;
String? _propertyType = '電梯大樓';
void _submit() async {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
final newHouse = House(
id: widget.house?.id ?? const Uuid().v4(), // 自動取得 UUID
title: _title,
address: _address,
totalPrice: _totalPrice,
totalPing: _totalPing,
propertyType: _propertyType,
);
await _api.createHouse(newHouse);
if (mounted) Navigator.pop(context, true);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.house == null ? '新增看屋筆記' : '編輯物件')),
body: Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(20),
children: [
TextFormField(
initialValue: widget.house?.title,
decoration: const InputDecoration(labelText: '物件名稱 *', hintText: '例:桃園藝文特區三房'),
validator: (v) => v!.isEmpty ? '請輸入名稱' : null,
onSaved: (v) => _title = v!,
),
const SizedBox(height: 16),
TextFormField(
initialValue: widget.house?.address,
decoration: const InputDecoration(labelText: '地址', suffixIcon: Icon(Icons.my_location)),
onSaved: (v) => _address = v,
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: TextFormField(
initialValue: widget.house?.totalPrice?.toString(),
decoration: const InputDecoration(labelText: '開價 (萬)', prefixIcon: Icon(Icons.monetization_on_outlined)),
keyboardType: TextInputType.number,
onSaved: (v) => _totalPrice = double.tryParse(v ?? ''),
),
),
const SizedBox(width: 16),
Expanded(
child: TextFormField(
initialValue: widget.house?.totalPing?.toString(),
decoration: const InputDecoration(labelText: '坪數', prefixIcon: Icon(Icons.square_foot)),
keyboardType: TextInputType.number,
onSaved: (v) => _totalPing = double.tryParse(v ?? ''),
),
),
],
),
const SizedBox(height: 16),
DropdownButtonFormField<String>(
value: _propertyType,
items: ['電梯大樓', '公寓', '華廈', '透天'].map((e) => DropdownMenuItem(value: e, child: Text(e))).toList(),
onChanged: (v) => setState(() => _propertyType = v),
decoration: const InputDecoration(labelText: '建物型態'),
),
const SizedBox(height: 40),
ElevatedButton(
onPressed: _submit,
style: ElevatedButton.styleFrom(minimumSize: const Size(double.infinity, 54), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))),
child: const Text('儲存筆記', style: TextStyle(fontSize: 18)),
),
],
),
),
);
}
}
+140
View File
@@ -0,0 +1,140 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; // 支援 SystemNavigator.pop()
import 'house_note_model.dart';
import 'house_note_api.dart';
import 'house_note_form.dart';
import 'house_note_detail.dart';
class HouseNoteManager extends StatefulWidget {
const HouseNoteManager({super.key});
@override
State<HouseNoteManager> createState() => _HouseNoteManagerState();
}
class _HouseNoteManagerState extends State<HouseNoteManager> {
final HouseNoteApiService _api = HouseNoteApiService();
late Future<List<House>> _houseFuture;
@override
void initState() {
super.initState();
_refreshList();
}
void _refreshList() {
setState(() {
_houseFuture = _api.fetchHouses();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('🏠 我的賞屋筆記'),
actions: [
IconButton(
icon: const Icon(Icons.exit_to_app, color: Colors.redAccent),
tooltip: '離開系統',
onPressed: () => SystemNavigator.pop(), // 實作離開系統功能
)
],
),
body: FutureBuilder<List<House>>(
future: _houseFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return _buildEmptyState();
}
return ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: snapshot.data!.length,
itemBuilder: (ctx, i) => _buildHouseCard(snapshot.data![i]),
);
},
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () async {
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => const HouseNoteForm()),
);
if (result == true) _refreshList();
},
label: const Text('新增看屋'),
icon: const Icon(Icons.add_location_alt_outlined),
),
);
}
// 修正:補回遺失的 _buildHouseCard 方法
Widget _buildHouseCard(House item) {
return Card(
elevation: 2,
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 4),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: ListTile(
contentPadding: const EdgeInsets.all(12),
onTap: () {
// 導向詳情頁,這會用到 house_note_detail.dart
Navigator.push(
context,
MaterialPageRoute(builder: (context) => HouseNoteDetail(house: item)),
);
},
title: Text(item.title, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
Text(item.address ?? '暫無地址', maxLines: 1, overflow: TextOverflow.ellipsis),
const SizedBox(height: 8),
Row(
children: [
_buildTag(item.priceTag, Colors.blueGrey),
const SizedBox(width: 8),
_buildTag('${item.overallRating}', item.ratingColor),
],
),
],
),
trailing: const Icon(Icons.chevron_right),
),
);
}
// 修正:補回遺失的 _buildTag 方法
Widget _buildTag(String text, Color color) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
border: Border.all(color: color),
),
child: Text(
text,
style: TextStyle(color: color, fontSize: 12, fontWeight: FontWeight.bold),
),
);
}
// 修正:補回遺失的 _buildEmptyState 方法
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.house_siding, size: 80, color: Colors.grey.shade300),
const SizedBox(height: 16),
const Text('還沒有記錄,點擊右下角開始第一筆看屋吧!', style: TextStyle(color: Colors.grey)),
],
),
);
}
}
+190
View File
@@ -0,0 +1,190 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:uuid/uuid.dart';
class House {
final String id;
final String title;
final String? address;
final double? latitude;
final double? longitude;
final double? totalPrice;
final double? totalPing;
final double? age;
final String? propertyType;
final String? summaryNotes;
final double? overallRating;
final bool isFavorite;
final DateTime? createdAt;
House({
required this.id,
required this.title,
this.address,
this.latitude,
this.longitude,
this.totalPrice,
this.totalPing,
this.age,
this.propertyType,
this.summaryNotes,
this.overallRating,
this.isFavorite = false,
this.createdAt,
});
factory House.fromJson(Map<String, dynamic> json) {
return House(
id: json['id'] as String? ?? const Uuid().v4(),
title: json['title'] as String? ?? '未命名物件',
address: json['address'] as String?,
latitude: double.tryParse(json['latitude']?.toString() ?? ''),
longitude: double.tryParse(json['longitude']?.toString() ?? ''),
totalPrice: double.tryParse(json['total_price']?.toString() ?? ''),
totalPing: double.tryParse(json['total_ping']?.toString() ?? ''),
age: double.tryParse(json['age']?.toString() ?? ''),
propertyType: json['property_type'] as String?,
summaryNotes: json['summary_notes'] as String?,
overallRating: double.tryParse(json['overall_rating']?.toString() ?? '0'),
isFavorite: json['is_favorite'] == 1 || json['is_favorite'] == true,
createdAt: json['created_at'] != null ? DateTime.tryParse(json['created_at']) : null,
);
}
// UI Helper: 評分顏色
Color get ratingColor {
if ((overallRating ?? 0) >= 4) return Colors.green;
if ((overallRating ?? 0) >= 3) return Colors.orange;
return Colors.red;
}
// 格式化顯示價格與坪數
String get priceTag => totalPrice != null ? '${totalPrice}' : '價格未定';
String get infoSummary => '${propertyType ?? "未知"} | ${age ?? "?"}年 | ${totalPing ?? "?"}';
}
// 檢查項目定義 (Template)
class CheckItem {
final String id;
final String categoryId;
final String name;
final bool isDefault;
CheckItem({required this.id, required this.categoryId, required this.name, this.isDefault = true});
factory CheckItem.fromJson(Map<String, dynamic> json) {
return CheckItem(
id: json['id'] as String,
categoryId: json['category_id'] as String,
name: json['name'] as String,
isDefault: json['is_default'] == 1,
);
}
}
// 新增照片模型
class HousePhoto {
final String id;
final String houseId;
final String? evaluationId; // 關聯特定評估項目
final String filePath;
final String? description;
HousePhoto({required this.id, required this.houseId, this.evaluationId, required this.filePath, this.description});
factory HousePhoto.fromJson(Map<String, dynamic> json) {
return HousePhoto(
id: json['id'] as String,
houseId: json['house_id'] as String,
evaluationId: json['evaluation_id'] as String?,
filePath: json['file_path'] as String,
description: json['description'] as String?,
);
}
// 取得完整圖片網址
// String get fullUrl => "https://api.gex.com.tw/uploads/$filePath";
String get fullUrl {
if (filePath.startsWith('http')) {
return filePath;
}
return "https://api.gex.com.tw/uploads/$filePath";
}
}
// 新增評估紀錄模型
class HouseEvaluation {
String? id; // 【修改】允許為 null (新建立的評估還沒有 ID)
String houseId; // 移除 final
String checkItemId; // 移除 final
String itemName; // 移除 final,讓前端可以賦值
int score; // 【修改】移除 final,讓表單可以更新分數
String? itemMemo; // 【修改】移除 final,讓表單可以更新備註
HouseEvaluation({
this.id, // 【修改】移除 required
required this.houseId,
required this.checkItemId,
required this.itemName, // 給予預設值,解決報錯
this.score = 0,
this.itemMemo,
});
factory HouseEvaluation.fromJson(Map<String, dynamic> json) {
return HouseEvaluation(
id: json['id'] as String?,
houseId: json['house_id'] as String? ?? '',
checkItemId: json['check_item_id'] as String? ?? '',
itemName: json['itemName'] as String? ?? '',
score: int.tryParse(json['score']?.toString() ?? '0') ?? 0,
itemMemo: json['item_memo'] as String?,
);
}
}
// Tag
class Tag {
final String id;
final String name;
final String colorHex;
bool isSelected; // UI 用:判斷是否被選取
Tag({
required this.id,
required this.name,
required this.colorHex,
this.isSelected = false,
});
factory Tag.fromJson(Map<String, dynamic> json) {
return Tag(
id: json['id'] as String,
name: json['name'] as String,
colorHex: json['color_hex'] as String? ?? '#CCCCCC',
);
}
// 輔助:轉換 Hex String 為 Color 物件
Color get color {
final hexCode = colorHex.replaceAll('#', '');
return Color(int.parse('FF$hexCode', radix: 16));
}
}
// 檢查項目大類模型
class CheckCategory {
final String id;
final String name;
final int sortOrder;
CheckCategory({required this.id, required this.name, required this.sortOrder});
factory CheckCategory.fromJson(Map<String, dynamic> json) {
return CheckCategory(
id: json['id'] as String,
name: json['name'] as String,
sortOrder: int.tryParse(json['sort_order']?.toString() ?? '0') ?? 0,
);
}
}
+103
View File
@@ -0,0 +1,103 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import './house_note_api.dart';
class HousePhotoForm extends StatefulWidget {
final String houseId;
final String houseTitle;
const HousePhotoForm({required this.houseId, required this.houseTitle, super.key});
@override
State<HousePhotoForm> createState() => _HousePhotoFormState();
}
class _HousePhotoFormState extends State<HousePhotoForm> {
final _api = HouseNoteApiService();
final _noteController = TextEditingController();
File? _image;
bool _isSubmitting = false;
@override
void initState() {
super.initState();
_takePhoto(); // 進入頁面直接開啟相機
}
Future<void> _takePhoto() async {
final picker = ImagePicker();
final pickedFile = await picker.pickImage(source: ImageSource.camera, imageQuality: 70);
if (pickedFile != null) {
setState(() => _image = File(pickedFile.path));
}
}
void _submit() async {
if (_image == null) return;
setState(() => _isSubmitting = true);
try {
// 1. 上傳檔案
String? fileName = await _api.uploadImage(_image!);
if (fileName != null) {
// 2. 寫入資料庫
await _api.saveHousePhoto(widget.houseId, fileName, _noteController.text);
if (mounted) Navigator.pop(context, true);
}
} catch (e) {
debugPrint("提交失敗: $e");
} finally {
if (mounted) setState(() => _isSubmitting = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('${widget.houseTitle} - 拍照紀錄')),
body: _isSubmitting
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
children: [
GestureDetector(
onTap: _takePhoto,
child: Container(
height: 300,
width: double.infinity,
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade300),
),
child: _image == null
? const Center(child: Icon(Icons.add_a_photo, size: 50, color: Colors.grey))
: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.file(_image!, fit: BoxFit.cover),
),
),
),
const SizedBox(height: 20),
TextField(
controller: _noteController,
decoration: const InputDecoration(
labelText: '照片註記 (例如:客廳採光、疑似壁癌)',
border: OutlineInputBorder(),
),
maxLines: 3,
),
const SizedBox(height: 30),
ElevatedButton(
onPressed: _image == null ? null : _submit,
style: ElevatedButton.styleFrom(minimumSize: const Size(double.infinity, 50)),
child: const Text('上傳照片紀錄'),
),
],
),
),
);
}
}
+98
View File
@@ -0,0 +1,98 @@
import 'package:flutter/material.dart';
import './house_note_model.dart';
import './house_note_api.dart';
class HouseTagForm extends StatefulWidget {
final String houseId;
final String houseTitle;
const HouseTagForm({required this.houseId, required this.houseTitle, super.key});
@override
State<HouseTagForm> createState() => _HouseTagFormState();
}
class _HouseTagFormState extends State<HouseTagForm> {
final _api = HouseNoteApiService();
List<Tag> _allTags = [];
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadTags();
}
Future<void> _loadTags() async {
try {
// 同時讀取標籤庫與該房屋已選的標籤
final results = await Future.wait([
_api.fetchAllTags(),
_api.fetchHouseTagIds(widget.houseId),
]);
final allTags = results[0] as List<Tag>;
final selectedIds = results[1] as List<String>;
// 標記已選狀態
for (var tag in allTags) {
if (selectedIds.contains(tag.id)) {
tag.isSelected = true;
}
}
setState(() {
_allTags = allTags;
_isLoading = false;
});
} catch (e) {
debugPrint("讀取標籤失敗: $e");
setState(() => _isLoading = false);
}
}
void _save() async {
setState(() => _isLoading = true);
final selectedIds = _allTags.where((t) => t.isSelected).map((t) => t.id).toList();
await _api.updateHouseTags(widget.houseId, selectedIds);
if (mounted) Navigator.pop(context, true);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('${widget.houseTitle} - 編輯標籤'),
actions: [
TextButton(
onPressed: _isLoading ? null : _save,
child: const Text('儲存', style: TextStyle(fontWeight: FontWeight.bold)),
)
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: _allTags.length,
itemBuilder: (context, index) {
final tag = _allTags[index];
return CheckboxListTile(
secondary: CircleAvatar(
backgroundColor: tag.color,
child: const Icon(Icons.local_offer, size: 16, color: Colors.white),
),
title: Text(tag.name),
value: tag.isSelected,
activeColor: tag.color,
onChanged: (val) {
setState(() => tag.isSelected = val ?? false);
},
);
},
),
);
}
}
+88
View File
@@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import './house/house_note_manager.dart';
void main() {
// 確保 Flutter 引擎初始化
WidgetsFlutterBinding.ensureInitialized();
runApp(const HouseHunterApp());
}
class HouseHunterApp extends StatelessWidget {
const HouseHunterApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'House Hunter Pro',
debugShowCheckedModeBanner: false,
// 設定全域主題:採用現代化的 Material 3 設計
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF2196F3), // 專業藍色調
primary: const Color(0xFF2196F3),
secondary: const Color(0xFF00BCD4),
surface: Colors.white,
),
// 設定表單預設樣式 (對應 house_note_form.dart)
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: Colors.grey.shade50,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Color(0xFF2196F3), width: 2),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
),
// 設定卡片預設樣式 (對應 house_note_manager.dart)
cardTheme: CardThemeData(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: Colors.grey.shade200),
),
color: Colors.white,
),
// 設定 AppBar 樣式
appBarTheme: const AppBarTheme(
centerTitle: true,
backgroundColor: Colors.white,
foregroundColor: Colors.black87,
elevation: 0.5,
titleTextStyle: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
),
// 支援語系 (處理日期選擇器等元件的繁體中文顯示)
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('zh', 'TW'), // 繁體中文
],
// 根據需求描述:主頁面直接設定為房屋資訊列表
// 且不必登入即可使用
home: const HouseNoteManager(),
);
}
}
+180
View File
@@ -0,0 +1,180 @@
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:intl/intl.dart';
// import '../auth_manager.dart'; // 確保引入 AuthManager
// 共用 API 常數
//const String BASE_IP = "https://api.gex.com.tw:8033";
//const String COMMON_API_ENDPOINT = "xapi/v2/eis_demo/orm_api_v2/2/";
//const String COMMON_API_URL = "$BASE_IP/$COMMON_API_ENDPOINT";
class GenericApiService {
// [修改] 移除 const COMMON_API_URL,改為方法獲取
static const String BASE_IP = "https://api.gex.com.tw"; // :8033
// 修改後的動態 URL 產生器,支援傳入不同的 store procedure 與回傳 dataset 數
String _getProcedureUrl(String endpoint, String version) {
// final String db = AuthManager().currentCompany ?? "demo"; //
final String dbAll = "eis_demo"; //
return "$BASE_IP/xapi/v2/$dbAll/$endpoint/$version/"; //
}
String _getToken() {
DateTime now = DateTime.now();
// 產生 ddmmyy 格式
// 使用 intl 套件的 DateFormat,或手動組合
String ddmmyy = DateFormat('ddMMyy').format(now);
// 計算 CheckSum: (yyyy + mm + dd) % 10
int checkSum = (now.year + now.month + now.day) % 10;
// 組合最終特殊 Token
String specialToken = "$ddmmyy$checkSum";
return specialToken;
}
/// 呼叫 Store Procedure 的通用方法
/// [T] : 目標資料模型
/// [procedureEndpoint] : 端點名稱 (例如: 'sp_get_channel_sales_stats')
/// [version] : 版本號,預設為 "2"
/// [params] : 傳遞給 SP 的參數 Map
/// [fromJson] : 將 Map 轉換為物件的工廠方法
Future<List<T>> fetchProcedure<T>({
required String procedureEndpoint,
String version = "2",
required Map<String, String> params,
required T Function(Map<String, dynamic>) fromJson,
}) async {
String? token = _getToken(); // await AuthManager.getToken();
if (token == null) throw Exception('未登入:找不到有效 Token');
final Map<String, String> body = {"token": token, ...params};
final String url = _getProcedureUrl(procedureEndpoint, version);
try {
final response = await http.post(
Uri.parse(url),
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: body,
);
if (response.statusCode == 200) {
final Map<String, dynamic> responseData = json.decode(response.body);
final int code = responseData['code'] ?? responseData['Code'] ?? -1;
if (code == 0) {
final dynamic dataObject = responseData['data'];
if (dataObject is List) {
return dataObject.map((j) => fromJson(j as Map<String, dynamic>)).toList();
} else if (dataObject is Map<String, dynamic>) {
return [fromJson(dataObject)];
}
return []; // 數據格式不符回傳空清單
} else {
print("API 錯誤: ${responseData['msg'] ?? responseData['message']}");
return []; // Code 不為 0 回傳空清單
}
} else {
// [關鍵修正] statusCode 不為 200 時拋出異常或回傳空清單
throw Exception('HTTP 錯誤: ${response.statusCode}');
}
} catch (e) {
print("fetchProcedure 發生異常: $e");
rethrow; // 重新拋出異常讓呼叫端處理
}
// [關鍵修正] 確保函式末端一定有回傳值,解決截圖中的錯誤
}
// 以下是 orm_api
String _getDynamicUrl() {
// 從 AuthManager 獲取目前登入的公司別,若無則預設 eis_demo
// final String db = AuthManager().currentCompany ?? "demo";
final String dbAll = "eis_demo";
return "$BASE_IP/xapi/v2/$dbAll/orm_api_v2/2/";
}
/// 通用的獲取列表方法
/// [T] : 目標資料模型 (例如 Announcement 或 Todo)
/// [tableName] : 資料庫表名 (API 參數)
/// [pk] : 主鍵欄位名稱 (API 參數)
/// [queryFilter] : 查詢過濾條件 (API 參數)
/// [fromJson] : 將 Map 轉換為物件的工廠方法 (例如 Announcement.fromJson)
/// [action] : API 動作指令,預設為 'P'
Future<List<T>> fetchList<T>({
required String tableName,
required String pk,
required String queryFilter,
required T Function(Map<String, dynamic>) fromJson,
String action = 'P', // 對應 index.js 中的 _$_action
Map<String, dynamic> data = const {}, // 存放實際要存入資料庫的欄位值
}) async {
// 1. 讀取 Token (index.js 會優先檢查 body.token)
String? token = _getToken(); // await AuthManager.getToken();
if (token == null) {
throw Exception('未登入:找不到有效 Token');
}
// 2. 建立 API 參數
final Map<String, String> params = {
"token": token,
"_\$_tableName": tableName,
"_\$_pk": pk,
"_\$_action": action, // [修改] 使用傳入的參數
"_\$_query_filter": queryFilter,
};
// 3. 處理動態資料欄位 (將所有 value 轉為 String 以符合 http.post body 要求)
data.forEach((key, value) {
if (value != null) {
params[key] = value.toString();
}
});
try {
final response = await http.post(
Uri.parse(_getDynamicUrl()),
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: params,
);
if (response.statusCode == 200) {
final Map<String, dynamic> responseData = json.decode(response.body);
// 【修正】同時相容大寫 Code 與小寫 code
final int code = responseData['code'] ?? responseData['Code'] ?? -1;
// 檢查後端回傳的 code
if (code == 0) {
final dynamic dataObject = responseData['data'];
// 處理 index.js 回傳的資料結構 (data -> items)
if (dataObject is Map && dataObject['items'] is List) {
List<dynamic> listJson = dataObject['items'];
return listJson.map((json) => fromJson(json as Map<String, dynamic>)).toList();
} else if (dataObject is Map) {
// 處理直接回傳單一物件的情況
return [fromJson(dataObject as Map<String, dynamic>)];
}
// 若直接回傳 List (部分 action 可能直接回傳 dataResult)
else if (dataObject is List) {
return dataObject.map((json) => fromJson(json as Map<String, dynamic>)).toList();
}
return [];
} else {
print("API 錯誤: ${responseData['msg'] ?? responseData['message']}");
return [];
}
} else {
throw Exception('HTTP 錯誤: ${response.statusCode}');
}
} catch (e) {
print("GenericApiService 發生異常: $e");
rethrow;
}
}
}
+415
View File
@@ -0,0 +1,415 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
description:
name: async
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev"
source: hosted
version: "2.13.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.1"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
cross_file:
dependency: transitive
description:
name: cross_file
sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937"
url: "https://pub.dev"
source: hosted
version: "0.3.5+2"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.7"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
url: "https://pub.dev"
source: hosted
version: "1.0.9"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
file_selector_linux:
dependency: transitive
description:
name: file_selector_linux
sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
url: "https://pub.dev"
source: hosted
version: "0.9.4"
file_selector_macos:
dependency: transitive
description:
name: file_selector_macos
sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
url: "https://pub.dev"
source: hosted
version: "0.9.5"
file_selector_platform_interface:
dependency: transitive
description:
name: file_selector_platform_interface
sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85"
url: "https://pub.dev"
source: hosted
version: "2.7.0"
file_selector_windows:
dependency: transitive
description:
name: file_selector_windows
sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
url: "https://pub.dev"
source: hosted
version: "0.9.3+5"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
url: "https://pub.dev"
source: hosted
version: "6.0.0"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0"
url: "https://pub.dev"
source: hosted
version: "2.0.34"
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: "direct main"
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev"
source: hosted
version: "1.6.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
image_picker:
dependency: "direct main"
description:
name: image_picker
sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
image_picker_android:
dependency: transitive
description:
name: image_picker_android
sha256: "66810af8e99b2657ee98e5c6f02064f69bb63f7a70e343937f70946c5f8c6622"
url: "https://pub.dev"
source: hosted
version: "0.8.13+16"
image_picker_for_web:
dependency: transitive
description:
name: image_picker_for_web
sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
image_picker_ios:
dependency: transitive
description:
name: image_picker_ios
sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588
url: "https://pub.dev"
source: hosted
version: "0.8.13+6"
image_picker_linux:
dependency: transitive
description:
name: image_picker_linux
sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
url: "https://pub.dev"
source: hosted
version: "0.2.2"
image_picker_macos:
dependency: transitive
description:
name: image_picker_macos
sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91"
url: "https://pub.dev"
source: hosted
version: "0.2.2+1"
image_picker_platform_interface:
dependency: transitive
description:
name: image_picker_platform_interface
sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c"
url: "https://pub.dev"
source: hosted
version: "2.11.1"
image_picker_windows:
dependency: transitive
description:
name: image_picker_windows
sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
url: "https://pub.dev"
source: hosted
version: "0.2.2"
intl:
dependency: "direct main"
description:
name: intl
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev"
source: hosted
version: "0.20.2"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lints:
dependency: transitive
description:
name: lints
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.17.0"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
version: "0.7.10"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
uuid:
dependency: "direct main"
description:
name: uuid
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
url: "https://pub.dev"
source: hosted
version: "4.5.3"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
url: "https://pub.dev"
source: hosted
version: "15.0.2"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
sdks:
dart: ">=3.11.4 <4.0.0"
flutter: ">=3.38.0"
+99
View File
@@ -0,0 +1,99 @@
name: home_hunter
description: "A new Flutter project."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
environment:
sdk: ^3.11.4
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
# 補上這一行,注意縮進
flutter_localizations:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
# 新增以下這兩行
intl: ^0.20.2
uuid: ^4.4.0
http: ^1.2.1
image_picker: ^1.1.2
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^6.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/to/asset-from-package
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package
+30
View File
@@ -0,0 +1,30 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:home_hunter/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

+46
View File
@@ -0,0 +1,46 @@
<!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<base href="$FLUTTER_BASE_HREF">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">
<!-- iOS meta tags & icons -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="home_hunter">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<title>home_hunter</title>
<link rel="manifest" href="manifest.json">
</head>
<body>
<!--
You can customize the "flutter_bootstrap.js" script.
This is useful to provide a custom configuration to the Flutter loader
or to give the user feedback during the initialization process.
For more details:
* https://docs.flutter.dev/platform-integration/web/initialization
-->
<script src="flutter_bootstrap.js" async></script>
</body>
</html>
+35
View File
@@ -0,0 +1,35 @@
{
"name": "home_hunter",
"short_name": "home_hunter",
"start_url": ".",
"display": "standalone",
"background_color": "#0175C2",
"theme_color": "#0175C2",
"description": "A new Flutter project.",
"orientation": "portrait-primary",
"prefer_related_applications": false,
"icons": [
{
"src": "icons/Icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icons/Icon-512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "icons/Icon-maskable-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "icons/Icon-maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}