Compare commits
No commits in common. "6994e14443544ddbfa1501d110bae60001f80dd6" and "db1fdd66e47c806f269fb6b3a859a017f25f457c" have entirely different histories.
6994e14443
...
db1fdd66e4
|
|
@ -1,21 +0,0 @@
|
|||
# This is a generated file; do not edit or check into version control.
|
||||
app_links=C:\\Users\\acer\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\app_links-3.5.1\\
|
||||
gtk=C:\\Users\\acer\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\gtk-2.1.0\\
|
||||
path_provider=C:\\Users\\acer\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\path_provider-2.1.3\\
|
||||
path_provider_android=C:\\Users\\acer\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\path_provider_android-2.2.3\\
|
||||
path_provider_foundation=C:\\Users\\acer\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\path_provider_foundation-2.3.2\\
|
||||
path_provider_linux=C:\\Users\\acer\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\path_provider_linux-2.2.1\\
|
||||
path_provider_windows=C:\\Users\\acer\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\path_provider_windows-2.2.1\\
|
||||
shared_preferences=C:\\Users\\acer\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\shared_preferences-2.2.3\\
|
||||
shared_preferences_android=C:\\Users\\acer\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\shared_preferences_android-2.2.1\\
|
||||
shared_preferences_foundation=C:\\Users\\acer\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\shared_preferences_foundation-2.3.5\\
|
||||
shared_preferences_linux=C:\\Users\\acer\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\shared_preferences_linux-2.3.2\\
|
||||
shared_preferences_web=C:\\Users\\acer\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\shared_preferences_web-2.2.1\\
|
||||
shared_preferences_windows=C:\\Users\\acer\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\shared_preferences_windows-2.3.2\\
|
||||
url_launcher=C:\\Users\\acer\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\url_launcher-6.2.4\\
|
||||
url_launcher_android=C:\\Users\\acer\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\url_launcher_android-6.3.0\\
|
||||
url_launcher_ios=C:\\Users\\acer\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\url_launcher_ios-6.2.4\\
|
||||
url_launcher_linux=C:\\Users\\acer\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\url_launcher_linux-3.1.1\\
|
||||
url_launcher_macos=C:\\Users\\acer\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\url_launcher_macos-3.2.0\\
|
||||
url_launcher_web=C:\\Users\\acer\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\url_launcher_web-2.2.0\\
|
||||
url_launcher_windows=C:\\Users\\acer\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\url_launcher_windows-3.1.1\\
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
# 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
|
||||
45
.metadata
|
|
@ -1,45 +0,0 @@
|
|||
# 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: "02085feb3f5d8a8156e5e28512b9d99351d510c0"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
|
||||
base_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
|
||||
- platform: android
|
||||
create_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
|
||||
base_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
|
||||
- platform: ios
|
||||
create_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
|
||||
base_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
|
||||
- platform: linux
|
||||
create_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
|
||||
base_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
|
||||
- platform: macos
|
||||
create_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
|
||||
base_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
|
||||
- platform: web
|
||||
create_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
|
||||
base_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
|
||||
- platform: windows
|
||||
create_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
|
||||
base_revision: 02085feb3f5d8a8156e5e28512b9d99351d510c0
|
||||
|
||||
# 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'
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
# 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
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
plugins {
|
||||
id "com.android.application"
|
||||
id "kotlin-android"
|
||||
id "dev.flutter.flutter-gradle-plugin"
|
||||
}
|
||||
|
||||
def localProperties = new Properties()
|
||||
def localPropertiesFile = rootProject.file('local.properties')
|
||||
if (localPropertiesFile.exists()) {
|
||||
localPropertiesFile.withReader('UTF-8') { reader ->
|
||||
localProperties.load(reader)
|
||||
}
|
||||
}
|
||||
|
||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode') ?: '1'
|
||||
def flutterVersionName = localProperties.getProperty('flutter.versionName') ?: '1.0'
|
||||
|
||||
android {
|
||||
namespace "com.example.coffee_iot_flutter"
|
||||
compileSdk 36
|
||||
ndkVersion flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '17'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.example.coffee_iot_flutter"
|
||||
minSdk flutter.minSdkVersion
|
||||
targetSdk 35
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Force pin versi androidx agar kompatibel AGP 8.6.1
|
||||
configurations.all {
|
||||
resolutionStrategy {
|
||||
force 'androidx.browser:browser:1.8.0'
|
||||
force 'androidx.core:core:1.15.0'
|
||||
force 'androidx.core:core-ktx:1.15.0'
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source '../..'
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.coffee_iot_flutter">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<application
|
||||
android:label="Iot Kopi"
|
||||
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">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data
|
||||
android:scheme="com.example.coffee_iot_flutter"
|
||||
android:host="reset-callback" />
|
||||
</intent-filter>
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||
|
||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
package com.example.coffee_iot_flutter
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity : FlutterActivity()
|
||||
|
Before Width: | Height: | Size: 69 B |
|
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
|
Before Width: | Height: | Size: 69 B |
|
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
|
Before Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
|
@ -1,19 +0,0 @@
|
|||
<?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">
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</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>
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
<?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>
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</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>
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
<?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">
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</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>
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.buildDir = "../build"
|
||||
|
||||
subprojects {
|
||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||
}
|
||||
|
||||
subprojects {
|
||||
project.evaluationDependsOn(':app')
|
||||
}
|
||||
|
||||
tasks.register("clean", Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
pluginManagement {
|
||||
def flutterSdkPath = {
|
||||
def properties = new Properties()
|
||||
file("local.properties").withInputStream { properties.load(it) }
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
return flutterSdkPath
|
||||
}()
|
||||
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id "com.android.application" version "8.6.1" apply false
|
||||
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
}
|
||||
|
||||
include ":app"
|
||||
|
Before Width: | Height: | Size: 198 KiB |
|
Before Width: | Height: | Size: 186 KiB |
|
|
@ -1,34 +0,0 @@
|
|||
**/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
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
<?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 +0,0 @@
|
|||
#include "Generated.xcconfig"
|
||||
|
|
@ -1 +0,0 @@
|
|||
#include "Generated.xcconfig"
|
||||
|
|
@ -1,620 +0,0 @@
|
|||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
|
||||
remoteInfo = Runner;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
331C8082294A63A400263BE5 /* RunnerTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */,
|
||||
);
|
||||
path = RunnerTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */,
|
||||
);
|
||||
name = Flutter;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146E51CF9000F007C117D = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9740EEB11CF90186004384FC /* Flutter */,
|
||||
97C146F01CF9000F007C117D /* Runner */,
|
||||
97C146EF1CF9000F007C117D /* Products */,
|
||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146EF1CF9000F007C117D /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146F01CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||
97C147021CF9000F007C117D /* Info.plist */,
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||
);
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
331C8080294A63A400263BE5 /* RunnerTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||
buildPhases = (
|
||||
331C807D294A63A400263BE5 /* Sources */,
|
||||
331C807F294A63A400263BE5 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
331C8086294A63A400263BE5 /* PBXTargetDependency */,
|
||||
);
|
||||
name = RunnerTests;
|
||||
productName = RunnerTests;
|
||||
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = Runner;
|
||||
productName = Runner;
|
||||
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastUpgradeCheck = 1510;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
331C8080294A63A400263BE5 = {
|
||||
CreatedOnToolsVersion = 14.0;
|
||||
TestTargetID = 97C146ED1CF9000F007C117D;
|
||||
};
|
||||
97C146ED1CF9000F007C117D = {
|
||||
CreatedOnToolsVersion = 7.3.1;
|
||||
LastSwiftMigration = 1100;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
|
||||
compatibilityVersion = "Xcode 9.3";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 97C146E51CF9000F007C117D;
|
||||
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
97C146ED1CF9000F007C117D /* Runner */,
|
||||
331C8080294A63A400263BE5 /* RunnerTests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
331C807F294A63A400263BE5 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EC1CF9000F007C117D /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||
);
|
||||
name = "Thin Binary";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||
};
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Run Script";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
331C807D294A63A400263BE5 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EA1CF9000F007C117D /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 97C146ED1CF9000F007C117D /* Runner */;
|
||||
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
97C146FB1CF9000F007C117D /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
97C147001CF9000F007C117D /* Base */,
|
||||
);
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
249021D3217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
249021D4217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.coffeeIotFlutter;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
331C8088294A63A400263BE5 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.coffeeIotFlutter.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
331C8089294A63A400263BE5 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.coffeeIotFlutter.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
331C808A294A63A400263BE5 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.coffeeIotFlutter.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
97C147031CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
97C147041CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
97C147061CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.coffeeIotFlutter;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
97C147071CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.coffeeIotFlutter;
|
||||
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 */;
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
<?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>
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
<?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>
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
<?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>
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
<?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>
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
<?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>
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 295 B |
|
Before Width: | Height: | Size: 406 B |
|
Before Width: | Height: | Size: 450 B |
|
Before Width: | Height: | Size: 282 B |
|
Before Width: | Height: | Size: 462 B |
|
Before Width: | Height: | Size: 704 B |
|
Before Width: | Height: | Size: 406 B |
|
Before Width: | Height: | Size: 586 B |
|
Before Width: | Height: | Size: 862 B |
|
Before Width: | Height: | Size: 862 B |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 762 B |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
|
@ -1,23 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 68 B |
|
Before Width: | Height: | Size: 68 B |
|
Before Width: | Height: | Size: 68 B |
|
|
@ -1,5 +0,0 @@
|
|||
# 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.
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
<?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>
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
<?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>
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
<?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>Coffee Iot Flutter</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>coffee_iot_flutter</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 +0,0 @@
|
|||
#import "GeneratedPluginRegistrant.h"
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import Flutter
|
||||
import UIKit
|
||||
|
||||
class SceneDelegate: FlutterSceneDelegate {
|
||||
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
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.
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppColors {
|
||||
// Latar Belakang (Mint Ice yang lembut)
|
||||
static const Color background = Color(0xFFF4F7F6);
|
||||
static const Color cardBg = Colors.white;
|
||||
|
||||
// Warna Utama (Earth Tone Premium - Olive Sage)
|
||||
static const Color primary = Color(0xFF627254);
|
||||
static const Color primaryLight = Color(0xFF76885B);
|
||||
|
||||
// Teks (Menggunakan pilihan barumu: Charcoal & Cool Grey)
|
||||
static const Color textMain = Color(0xFF3D3D3B); // Charcoal Dark Grey (Tegas)
|
||||
static const Color textSecondary = Color(0xFF8B949E); // Cool Grey (Elegan)
|
||||
|
||||
// Warna Status & Peringatan
|
||||
static const Color success = Color(0xFF7DA78C);
|
||||
static const Color error = Color(0xFFE74C3C);
|
||||
static const Color warning = Color(0xFFE67E22);
|
||||
static const Color info = Color(0xFF3498DB);
|
||||
static const Color light = Color(0xFFF1C40F);
|
||||
|
||||
// Latar belakang transparan untuk elemen status (Update .withValues untuk Flutter terbaru)
|
||||
static Color get successBg => success.withValues(alpha: 0.15);
|
||||
static Color get errorBg => error.withValues(alpha: 0.15);
|
||||
static Color get warningBg => warning.withValues(alpha: 0.15);
|
||||
static Color get infoBg => info.withValues(alpha: 0.15);
|
||||
}
|
||||
147
lib/main.dart
|
|
@ -1,147 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:app_links/app_links.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
// --- IMPORT APP COLORS ---
|
||||
import 'constants/app_colors.dart';
|
||||
// -------------------------
|
||||
|
||||
import 'services/mqtt_service.dart';
|
||||
import 'screens/login_screen.dart';
|
||||
import 'screens/register_screen.dart';
|
||||
import 'screens/home_screen.dart';
|
||||
import 'screens/analytics_screen.dart';
|
||||
import 'screens/image_result_screen.dart';
|
||||
import 'screens/change_password_screen.dart';
|
||||
import 'screens/forgot_password_screen.dart';
|
||||
import 'screens/splash_screen.dart';
|
||||
|
||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
await Supabase.initialize(
|
||||
url: 'https://ddmhzzegejbsshihzext.supabase.co',
|
||||
anonKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRkbWh6emVnZWpic3NoaWh6ZXh0Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzcyMjIyNzAsImV4cCI6MjA5Mjc5ODI3MH0._r-viIQLMXuNNdg4tcn7sJTBO9QqSpSlF7hk43hu2Ks',
|
||||
);
|
||||
|
||||
// Listener auth state — handle password recovery
|
||||
Supabase.instance.client.auth.onAuthStateChange.listen((data) {
|
||||
if (data.event == AuthChangeEvent.passwordRecovery) {
|
||||
navigatorKey.currentState?.pushReplacementNamed(
|
||||
'/change_password',
|
||||
arguments: {'dari_reset': true},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle deep link saat app sudah jalan
|
||||
final appLinks = AppLinks();
|
||||
appLinks.uriLinkStream.listen((uri) {
|
||||
final uriStr = uri.toString();
|
||||
if (uriStr.contains('reset-callback') ||
|
||||
uriStr.contains('type=recovery')) {
|
||||
// Supabase akan handle token otomatis via onAuthStateChange
|
||||
debugPrint('Deep link diterima: $uriStr');
|
||||
}
|
||||
});
|
||||
|
||||
runApp(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider(create: (_) => MqttService()..connect()),
|
||||
],
|
||||
child: const MyApp(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
navigatorKey: navigatorKey,
|
||||
title: 'Coffee IoT',
|
||||
debugShowCheckedModeBanner: false,
|
||||
|
||||
// --- KONFIGURASI TEMA LIGHT MODE (TERPUSAT) ---
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.light,
|
||||
scaffoldBackgroundColor: AppColors.background,
|
||||
primaryColor: AppColors.primary,
|
||||
|
||||
// ColorScheme untuk warna dasar komponen Material (Tombol, Dialog, dll)
|
||||
colorScheme: const ColorScheme.light(
|
||||
primary: AppColors.primary,
|
||||
secondary: AppColors.primary,
|
||||
surface: AppColors.cardBg,
|
||||
error: AppColors.error,
|
||||
onPrimary: Colors.white,
|
||||
onSurface: AppColors.textMain,
|
||||
),
|
||||
|
||||
// Menerapkan Google Fonts (Poppins) dengan pewarnaan dari AppColors
|
||||
textTheme: GoogleFonts.poppinsTextTheme(
|
||||
ThemeData.light().textTheme,
|
||||
).copyWith(
|
||||
displayLarge: GoogleFonts.poppins(color: AppColors.textMain, fontWeight: FontWeight.bold),
|
||||
displayMedium: GoogleFonts.poppins(color: AppColors.textMain, fontWeight: FontWeight.bold),
|
||||
headlineLarge: GoogleFonts.poppins(color: AppColors.textMain, fontWeight: FontWeight.bold),
|
||||
headlineMedium: GoogleFonts.poppins(color: AppColors.textMain, fontWeight: FontWeight.w600),
|
||||
titleLarge: GoogleFonts.poppins(color: AppColors.textMain, fontWeight: FontWeight.w600),
|
||||
bodyLarge: GoogleFonts.poppins(color: AppColors.textMain),
|
||||
bodyMedium: GoogleFonts.poppins(color: AppColors.textMain),
|
||||
bodySmall: GoogleFonts.poppins(color: AppColors.textSecondary),
|
||||
labelLarge: GoogleFonts.poppins(color: AppColors.textMain, fontWeight: FontWeight.w500),
|
||||
),
|
||||
|
||||
// Tema AppBar Global (Biar transparan dan tulisannya otomatis ikut tema)
|
||||
appBarTheme: AppBarTheme(
|
||||
backgroundColor: AppColors.background,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
iconTheme: const IconThemeData(color: AppColors.textMain),
|
||||
titleTextStyle: GoogleFonts.poppins(
|
||||
color: AppColors.textMain,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
|
||||
// Tema Card Global (Otomatis membulat dan punya bayangan halus)
|
||||
cardTheme: CardThemeData(
|
||||
color: AppColors.cardBg,
|
||||
elevation: 2,
|
||||
shadowColor: Colors.black.withValues(alpha: 0.05),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
|
||||
// Tema Ikon Global
|
||||
iconTheme: const IconThemeData(
|
||||
color: AppColors.textMain,
|
||||
),
|
||||
),
|
||||
// ----------------------------------------------
|
||||
|
||||
initialRoute: '/',
|
||||
routes: {
|
||||
'/': (context) => const SplashScreen(),
|
||||
'/login': (context) => const LoginScreen(),
|
||||
'/register': (context) => const RegisterScreen(),
|
||||
'/home': (context) => const HomeScreen(),
|
||||
'/analytics': (context) => const AnalyticsScreen(),
|
||||
'/image_result': (context) => const ImageResultScreen(),
|
||||
'/change_password': (context) => const ChangePasswordScreen(),
|
||||
'/forgot_password': (context) => const ForgotPasswordScreen(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,177 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import '../services/supabase_service.dart';
|
||||
import '../constants/app_colors.dart'; // <-- IMPORT APP COLORS DITAMBAHKAN
|
||||
|
||||
class AnalyticsScreen extends StatefulWidget {
|
||||
const AnalyticsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<AnalyticsScreen> createState() => _AnalyticsScreenState();
|
||||
}
|
||||
|
||||
class _AnalyticsScreenState extends State<AnalyticsScreen> {
|
||||
List<Map<String, dynamic>> _logs = [];
|
||||
bool _isLoading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchLogs();
|
||||
}
|
||||
|
||||
Future<void> _fetchLogs() async {
|
||||
try {
|
||||
final data = await SupabaseService().getFotoDataset();
|
||||
setState(() {
|
||||
_logs = data;
|
||||
_isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
debugPrint('Error fetching logs: $e');
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final args = (ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?) ?? {};
|
||||
final String title = args['title'] ?? 'Grafik';
|
||||
final Color color = args['color'] ?? AppColors.primary; // --- BERUBAH: Fallback color ---
|
||||
final String sensorKey = title.contains('Suhu') ? 'suhu' : title.contains('Kelembaban') ? 'kelembapan' : 'intensitas';
|
||||
|
||||
return Scaffold(
|
||||
// --- BERUBAH: Background Scaffold Utama ---
|
||||
backgroundColor: AppColors.background,
|
||||
appBar: AppBar(
|
||||
// --- BERUBAH: Dibuat transparan agar menyatu dengan background ---
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
// --- BERUBAH: Warna Teks Judul ---
|
||||
title: Text(title, style: const TextStyle(color: AppColors.textMain)),
|
||||
leading: IconButton(
|
||||
// --- BERUBAH: Warna Ikon Back ---
|
||||
icon: const Icon(Icons.arrow_back, color: AppColors.primary),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
),
|
||||
body: _isLoading
|
||||
// --- BERUBAH: Warna Loading Indicator ---
|
||||
? const Center(child: CircularProgressIndicator(color: AppColors.primary))
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildChartCard(title, color, _logs, sensorKey),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// --- BERUBAH: Warna Teks Judul Riwayat ---
|
||||
const Text('Riwayat Data', style: TextStyle(color: AppColors.textMain, fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
// --- BERUBAH: Warna Ikon Refresh ---
|
||||
IconButton(icon: const Icon(Icons.refresh, color: AppColors.primary), onPressed: _fetchLogs),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
// --- BERUBAH: Warna Latar Daftar Riwayat ---
|
||||
color: AppColors.cardBg,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
// --- BERUBAH: Menambahkan border halus agar serasi dengan kartu lain ---
|
||||
border: Border.all(color: AppColors.textSecondary.withValues(alpha: 0.15)),
|
||||
),
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.all(15),
|
||||
itemCount: _logs.length,
|
||||
// --- BERUBAH: Warna Garis Pemisah (Divider) antar item ---
|
||||
separatorBuilder: (context, index) => Divider(color: AppColors.textSecondary.withValues(alpha: 0.3)),
|
||||
itemBuilder: (context, index) {
|
||||
final log = _logs[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// --- BERUBAH: Warna Teks Tanggal/Waktu ---
|
||||
Text(log['timestamp'] ?? '', style: const TextStyle(color: AppColors.textSecondary, fontSize: 10)),
|
||||
// --- BERUBAH: Warna Teks Status Kipas ---
|
||||
Text('Kipas 1: ${log['kipas1']} | Kipas 2: ${log['kipas2']}', style: const TextStyle(color: AppColors.primary, fontSize: 10)),
|
||||
],
|
||||
),
|
||||
// Warna nilai sensor tetap mengikuti `color` dinamis (Merah/Cyan/Orange)
|
||||
Text('${log[sensorKey]}', style: TextStyle(color: color, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildChartCard(String title, Color color, List<Map<String, dynamic>> logs, String key) {
|
||||
List<FlSpot> spots = [];
|
||||
if (logs.isNotEmpty) {
|
||||
final reversedLogs = logs.reversed.toList();
|
||||
for (int i = 0; i < reversedLogs.length; i++) {
|
||||
spots.add(FlSpot(i.toDouble(), double.tryParse(reversedLogs[i][key].toString()) ?? 0));
|
||||
}
|
||||
}
|
||||
|
||||
return Container(
|
||||
height: 250,
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
// --- BERUBAH: Warna Latar Kartu Grafik ---
|
||||
color: AppColors.cardBg,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
// --- BERUBAH: Menambahkan border halus agar serasi dengan kartu lain ---
|
||||
border: Border.all(color: AppColors.textSecondary.withValues(alpha: 0.15)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
gridData: FlGridData(show: false),
|
||||
titlesData: FlTitlesData(show: false),
|
||||
borderData: FlBorderData(show: false),
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
spots: spots.isEmpty ? [const FlSpot(0, 0)] : spots,
|
||||
isCurved: true,
|
||||
// Warna garis grafik tetap mengikuti `color` dinamis
|
||||
color: color,
|
||||
barWidth: 4,
|
||||
dotData: FlDotData(show: false),
|
||||
// --- BERUBAH: Gunakan withValues untuk transparansi area bawah grafik ---
|
||||
belowBarData: BarAreaData(show: true, color: color.withValues(alpha: 0.1)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Text(
|
||||
'Terakhir: ${logs.isNotEmpty ? logs.first[key] : "-"}',
|
||||
// Warna teks terakhir tetap mengikuti `color` dinamis
|
||||
style: TextStyle(color: color, fontSize: 12)
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,288 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import '../services/supabase_service.dart';
|
||||
import '../constants/app_colors.dart'; // <-- IMPORT APP COLORS DITAMBAHKAN
|
||||
|
||||
class ChangePasswordScreen extends StatefulWidget {
|
||||
const ChangePasswordScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ChangePasswordScreen> createState() => _ChangePasswordScreenState();
|
||||
}
|
||||
|
||||
class _ChangePasswordScreenState extends State<ChangePasswordScreen> {
|
||||
final _passLamaController = TextEditingController();
|
||||
final _passBaruController = TextEditingController();
|
||||
final _konfirmasiController = TextEditingController();
|
||||
|
||||
bool _isLoading = false;
|
||||
bool _obscurePassLama = true;
|
||||
bool _obscurePassBaru = true;
|
||||
bool _obscureKonfirmasi = true;
|
||||
|
||||
// Deteksi apakah datang dari deep link reset password
|
||||
// Kalau dari deep link, tidak perlu password lama
|
||||
bool _dariResetLink = false;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final args = ModalRoute.of(context)?.settings.arguments;
|
||||
if (args is Map && args['dari_reset'] == true) {
|
||||
_dariResetLink = true;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_passLamaController.dispose();
|
||||
_passBaruController.dispose();
|
||||
_konfirmasiController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _handleGantiPassword() async {
|
||||
final passLama = _passLamaController.text.trim();
|
||||
final passBaru = _passBaruController.text.trim();
|
||||
final konfirmasi = _konfirmasiController.text.trim();
|
||||
|
||||
// Validasi
|
||||
if (!_dariResetLink && passLama.isEmpty) {
|
||||
_showSnackBar('Password lama harus diisi!', isError: true);
|
||||
return;
|
||||
}
|
||||
if (passBaru.isEmpty || konfirmasi.isEmpty) {
|
||||
_showSnackBar('Password baru dan konfirmasi harus diisi!', isError: true);
|
||||
return;
|
||||
}
|
||||
if (passBaru.length < 6) {
|
||||
_showSnackBar('Password baru minimal 6 karakter!', isError: true);
|
||||
return;
|
||||
}
|
||||
if (passBaru != konfirmasi) {
|
||||
_showSnackBar('Konfirmasi password tidak cocok!', isError: true);
|
||||
return;
|
||||
}
|
||||
if (!_dariResetLink && passLama == passBaru) {
|
||||
_showSnackBar('Password baru tidak boleh sama dengan password lama!',
|
||||
isError: true);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
try {
|
||||
if (!_dariResetLink) {
|
||||
// Dari settings — verifikasi password lama dulu
|
||||
final user = SupabaseService().currentUser;
|
||||
if (user == null) throw Exception('User tidak ditemukan');
|
||||
await SupabaseService().signIn(user.email!, passLama);
|
||||
}
|
||||
|
||||
// Update password baru
|
||||
await SupabaseService().updatePassword(passBaru);
|
||||
|
||||
if (mounted) {
|
||||
_showSnackBar('Password berhasil diubah!', isError: false);
|
||||
Future.delayed(const Duration(milliseconds: 1500), () {
|
||||
if (mounted) {
|
||||
if (_dariResetLink) {
|
||||
// Dari reset link → ke login
|
||||
Navigator.pushReplacementNamed(context, '/login');
|
||||
} else {
|
||||
// Dari settings → kembali
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
final errorStr = e.toString().toLowerCase();
|
||||
String pesan = 'Gagal mengubah password, coba lagi.';
|
||||
if (errorStr.contains('invalid_credentials') ||
|
||||
errorStr.contains('invalid login credentials')) {
|
||||
pesan = 'Password lama salah!';
|
||||
} else if (errorStr.contains('network') ||
|
||||
errorStr.contains('socket')) {
|
||||
pesan = 'Tidak ada koneksi internet.';
|
||||
}
|
||||
_showSnackBar(pesan, isError: true);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
void _showSnackBar(String pesan, {required bool isError}) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
Icon(isError ? Icons.error_outline : Icons.check_circle_outline,
|
||||
color: Colors.white), // Tetap putih agar kontras
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(pesan,
|
||||
style: const TextStyle(
|
||||
color: Colors.white, fontWeight: FontWeight.bold)), // Tetap putih
|
||||
),
|
||||
],
|
||||
),
|
||||
// --- BERUBAH: Warna Notifikasi ---
|
||||
backgroundColor:
|
||||
isError ? AppColors.error : AppColors.success,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
margin: const EdgeInsets.all(16),
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
// --- BERUBAH: Background Utama ---
|
||||
backgroundColor: AppColors.background,
|
||||
appBar: AppBar(
|
||||
// --- BERUBAH: AppBar Transparan ---
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
// --- BERUBAH: Warna Ikon Kembali ---
|
||||
icon: const Icon(Icons.arrow_back, color: AppColors.primary),
|
||||
onPressed: () => _dariResetLink
|
||||
? Navigator.pushReplacementNamed(context, '/login')
|
||||
: Navigator.pop(context),
|
||||
),
|
||||
title: Text(
|
||||
_dariResetLink ? 'Reset Password' : 'Ganti Password',
|
||||
style: const TextStyle(
|
||||
// --- BERUBAH: Warna Judul ---
|
||||
color: AppColors.textMain,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_dariResetLink
|
||||
? 'Masukkan password baru untuk akun kamu.'
|
||||
: 'Ubah password akun kamu.\nPastikan password baru minimal 6 karakter.',
|
||||
// --- BERUBAH: Warna Sub-judul ---
|
||||
style: const TextStyle(color: AppColors.textSecondary, fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
// --- BERUBAH: Latar Belakang Kotak Form ---
|
||||
color: AppColors.cardBg,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
// --- BERUBAH: Garis Pinggir Halus ---
|
||||
border: Border.all(color: AppColors.textSecondary.withValues(alpha: 0.15)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Field password lama — hanya tampil kalau bukan dari reset link
|
||||
if (!_dariResetLink) ...[
|
||||
_buildPasswordField(
|
||||
controller: _passLamaController,
|
||||
label: 'Password Lama',
|
||||
obscure: _obscurePassLama,
|
||||
onToggle: () => setState(
|
||||
() => _obscurePassLama = !_obscurePassLama),
|
||||
),
|
||||
// --- BERUBAH: Warna Divider ---
|
||||
Divider(color: AppColors.textSecondary.withValues(alpha: 0.3)),
|
||||
],
|
||||
|
||||
_buildPasswordField(
|
||||
controller: _passBaruController,
|
||||
label: 'Password Baru',
|
||||
obscure: _obscurePassBaru,
|
||||
onToggle: () => setState(
|
||||
() => _obscurePassBaru = !_obscurePassBaru),
|
||||
),
|
||||
// --- BERUBAH: Warna Divider ---
|
||||
Divider(color: AppColors.textSecondary.withValues(alpha: 0.3)),
|
||||
|
||||
_buildPasswordField(
|
||||
controller: _konfirmasiController,
|
||||
label: 'Konfirmasi Password Baru',
|
||||
obscure: _obscureKonfirmasi,
|
||||
onToggle: () => setState(
|
||||
() => _obscureKonfirmasi = !_obscureKonfirmasi),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 30),
|
||||
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 55,
|
||||
child: ElevatedButton(
|
||||
onPressed: _isLoading ? null : _handleGantiPassword,
|
||||
style: ElevatedButton.styleFrom(
|
||||
// --- BERUBAH: Warna Tombol ---
|
||||
backgroundColor: AppColors.primary,
|
||||
disabledBackgroundColor: AppColors.primary.withValues(alpha: 0.3),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
),
|
||||
child: _isLoading
|
||||
// Tetap putih agar kontras dengan tombol primer
|
||||
? const CircularProgressIndicator(color: Colors.white)
|
||||
: const Text(
|
||||
'Simpan Password Baru',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white), // Tetap putih
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPasswordField({
|
||||
required TextEditingController controller,
|
||||
required String label,
|
||||
required bool obscure,
|
||||
required VoidCallback onToggle,
|
||||
}) {
|
||||
return TextField(
|
||||
controller: controller,
|
||||
obscureText: obscure,
|
||||
// --- BERUBAH: Warna Teks Inputan ---
|
||||
style: const TextStyle(color: AppColors.textMain),
|
||||
decoration: InputDecoration(
|
||||
// --- BERUBAH: Warna Ikon Kunci ---
|
||||
prefixIcon: const Icon(Icons.lock, color: AppColors.primary),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(obscure ? Icons.visibility : Icons.visibility_off,
|
||||
// --- BERUBAH: Warna Ikon Mata ---
|
||||
color: AppColors.textSecondary),
|
||||
onPressed: onToggle,
|
||||
),
|
||||
hintText: label,
|
||||
// --- BERUBAH: Warna Placeholder/Hint ---
|
||||
hintStyle: const TextStyle(color: AppColors.textSecondary),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,338 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../services/mqtt_service.dart';
|
||||
import '../services/supabase_service.dart';
|
||||
import '../constants/app_colors.dart'; // <-- IMPORT APP COLORS DITAMBAHKAN
|
||||
|
||||
class ControlSettingsScreen extends StatefulWidget {
|
||||
const ControlSettingsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ControlSettingsScreen> createState() => _ControlSettingsScreenState();
|
||||
}
|
||||
|
||||
class _ControlSettingsScreenState extends State<ControlSettingsScreen> {
|
||||
int _operatingMode = 0; // 0 = otomatis, 1 = manual
|
||||
final TextEditingController _tempMinController = TextEditingController();
|
||||
final TextEditingController _tempMaxController = TextEditingController();
|
||||
final TextEditingController _humMinController = TextEditingController();
|
||||
final TextEditingController _humMaxController = TextEditingController();
|
||||
bool _isLoading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadSettings();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tempMinController.dispose();
|
||||
_tempMaxController.dispose();
|
||||
_humMinController.dispose();
|
||||
_humMaxController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadSettings() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final data = await SupabaseService().getBatasSensor();
|
||||
if (data != null && mounted) {
|
||||
setState(() {
|
||||
_tempMinController.text = data['suhu_min'].toString();
|
||||
_tempMaxController.text = data['suhu_max'].toString();
|
||||
_humMinController.text = data['rh_min'].toString();
|
||||
_humMaxController.text = data['rh_max'].toString();
|
||||
_operatingMode = prefs.getInt('op_mode') ?? 1;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error loading settings: $e');
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveSettings() async {
|
||||
if (_tempMinController.text.isEmpty ||
|
||||
_tempMaxController.text.isEmpty ||
|
||||
_humMinController.text.isEmpty ||
|
||||
_humMaxController.text.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text('Semua kolom batas harus diisi!'),
|
||||
// --- BERUBAH: Warna Notifikasi Peringatan ---
|
||||
backgroundColor: AppColors.warning,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final suhuMin = double.tryParse(_tempMinController.text) ?? 30;
|
||||
final suhuMax = double.tryParse(_tempMaxController.text) ?? 45;
|
||||
final rhMin = double.tryParse(_humMinController.text) ?? 60;
|
||||
final rhMax = double.tryParse(_humMaxController.text) ?? 70;
|
||||
|
||||
// Validasi logika
|
||||
if (suhuMin >= suhuMax) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text('Suhu min harus lebih kecil dari suhu max!'),
|
||||
// --- BERUBAH: Warna Notifikasi Peringatan ---
|
||||
backgroundColor: AppColors.warning,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (rhMin >= rhMax) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text('RH min harus lebih kecil dari RH max!'),
|
||||
// --- BERUBAH: Warna Notifikasi Peringatan ---
|
||||
backgroundColor: AppColors.warning,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final mqtt = Provider.of<MqttService>(context, listen: false);
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
try {
|
||||
// Simpan mode ke local storage
|
||||
await prefs.setInt('op_mode', _operatingMode);
|
||||
|
||||
// Kirim batas sensor ke ESP32 via MQTT → Flow 5 simpan ke Supabase
|
||||
mqtt.setBatasSensor(suhuMin, suhuMax, rhMin, rhMax);
|
||||
|
||||
// Kirim mode ke ESP32
|
||||
mqtt.gantiMode(_operatingMode == 0 ? "otomatis" : "manual");
|
||||
|
||||
// Update Supabase langsung juga sebagai backup
|
||||
await SupabaseService().updateBatasSensor(suhuMin, suhuMax, rhMin, rhMax);
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text('✅ Pengaturan Tersimpan & Terkirim ke ESP32'),
|
||||
// --- BERUBAH: Warna Notifikasi Sukses ---
|
||||
backgroundColor: AppColors.success,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('❌ Gagal: $e'),
|
||||
// --- BERUBAH: Warna Notifikasi Error ---
|
||||
backgroundColor: AppColors.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
// --- BERUBAH: Background Scaffold Utama ---
|
||||
backgroundColor: AppColors.background,
|
||||
body: SafeArea(
|
||||
child: _isLoading
|
||||
? const Center(
|
||||
// --- BERUBAH: Warna Loading Indicator ---
|
||||
child: CircularProgressIndicator(color: AppColors.primary))
|
||||
: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Pengaturan',
|
||||
style: TextStyle(
|
||||
// --- BERUBAH: Warna Judul Utama ---
|
||||
color: AppColors.textMain,
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
const Text(
|
||||
'Batas Sensor & Mode Operasi',
|
||||
// --- BERUBAH: Warna Sub-judul ---
|
||||
style: TextStyle(color: AppColors.textSecondary, fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
|
||||
// ── Batas Suhu ──
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildInputCard(
|
||||
'Suhu Min (°C)', '30', _tempMinController)),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(child: _buildInputCard(
|
||||
'Suhu Max (°C)', '45', _tempMaxController)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
|
||||
// ── Batas RH ──
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildInputCard(
|
||||
'RH Min (%)', '60', _humMinController)),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(child: _buildInputCard(
|
||||
'RH Max (%)', '70', _humMaxController)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// ── Mode Operasi ──
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
// --- BERUBAH: Background Kartu Mode Operasi ---
|
||||
color: AppColors.cardBg,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
// --- BERUBAH: Menambahkan border halus agar serasi ---
|
||||
border: Border.all(color: AppColors.textSecondary.withValues(alpha: 0.15)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Mode Operasi',
|
||||
style: TextStyle(
|
||||
// --- BERUBAH: Warna Judul Kartu Mode ---
|
||||
color: AppColors.textMain,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16)),
|
||||
const SizedBox(height: 10),
|
||||
RadioListTile<int>(
|
||||
value: 0,
|
||||
groupValue: _operatingMode,
|
||||
onChanged: (val) =>
|
||||
setState(() => _operatingMode = val!),
|
||||
title: const Text('Mode Otomatis',
|
||||
// --- BERUBAH: Warna Teks Radio Button ---
|
||||
style: TextStyle(color: AppColors.textMain)),
|
||||
// --- BERUBAH: Warna Lingkaran Radio Button (Aktif) ---
|
||||
activeColor: AppColors.primary,
|
||||
subtitle: const Text(
|
||||
'Kipas bekerja otomatis berdasarkan sensor',
|
||||
// --- BERUBAH: Warna Sub-teks Radio Button ---
|
||||
style: TextStyle(
|
||||
color: AppColors.textSecondary, fontSize: 10),
|
||||
),
|
||||
),
|
||||
RadioListTile<int>(
|
||||
value: 1,
|
||||
groupValue: _operatingMode,
|
||||
onChanged: (val) =>
|
||||
setState(() => _operatingMode = val!),
|
||||
title: const Text('Mode Manual',
|
||||
// --- BERUBAH: Warna Teks Radio Button ---
|
||||
style: TextStyle(color: AppColors.textMain)),
|
||||
// --- BERUBAH: Warna Lingkaran Radio Button (Aktif) ---
|
||||
activeColor: AppColors.primary,
|
||||
subtitle: const Text(
|
||||
'Kontrol penuh kipas melalui dashboard HP',
|
||||
// --- BERUBAH: Warna Sub-teks Radio Button ---
|
||||
style: TextStyle(
|
||||
color: AppColors.textSecondary, fontSize: 10),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// ── Tombol Simpan ──
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 55,
|
||||
child: ElevatedButton(
|
||||
onPressed: _saveSettings,
|
||||
style: ElevatedButton.styleFrom(
|
||||
// --- BERUBAH: Warna Latar Tombol Simpan ---
|
||||
backgroundColor: AppColors.primary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'Simpan & Kirim ke ESP32',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
// Tetap putih agar tulisan dalam tombol kontras
|
||||
color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 100),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInputCard(
|
||||
String title, String hint, TextEditingController controller) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(15),
|
||||
decoration: BoxDecoration(
|
||||
// --- BERUBAH: Latar Belakang Kartu Input Sensor ---
|
||||
color: AppColors.cardBg,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
// --- BERUBAH: Menambahkan border halus agar serasi ---
|
||||
border: Border.all(color: AppColors.textSecondary.withValues(alpha: 0.15)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title,
|
||||
style: const TextStyle(
|
||||
// --- BERUBAH: Warna Judul Input Sensor ---
|
||||
color: AppColors.textMain,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 10),
|
||||
TextField(
|
||||
controller: controller,
|
||||
keyboardType: TextInputType.number,
|
||||
// --- BERUBAH: Warna Angka/Nilai Input Sensor ---
|
||||
style: const TextStyle(color: AppColors.textMain),
|
||||
decoration: InputDecoration(
|
||||
hintText: hint,
|
||||
// --- BERUBAH: Warna Teks Placeholder/Hint ---
|
||||
hintStyle: const TextStyle(color: AppColors.textSecondary),
|
||||
filled: true,
|
||||
// --- BERUBAH: Warna Kolom Input Sensor ---
|
||||
fillColor: AppColors.background,
|
||||
isDense: true,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
// --- BERUBAH: Warna Garis Pinggir Input Sensor ---
|
||||
borderSide: BorderSide(color: AppColors.textSecondary.withValues(alpha: 0.3)),
|
||||
),
|
||||
// Menambahkan fokus border agar saat diklik terlihat jelas warnanya
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
borderSide: const BorderSide(color: AppColors.primary),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,339 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import '../services/supabase_service.dart';
|
||||
import '../constants/app_colors.dart'; // <-- IMPORT APP COLORS DITAMBAHKAN
|
||||
|
||||
class ForgotPasswordScreen extends StatefulWidget {
|
||||
const ForgotPasswordScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ForgotPasswordScreen> createState() => _ForgotPasswordScreenState();
|
||||
}
|
||||
|
||||
class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
|
||||
final _emailController = TextEditingController();
|
||||
final _otpController = TextEditingController();
|
||||
bool _isLoading = false;
|
||||
bool _otpSent = false; // step 2: input OTP
|
||||
String _email = '';
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_emailController.dispose();
|
||||
_otpController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// Step 1 — kirim OTP ke email
|
||||
Future<void> _handleKirimOTP() async {
|
||||
final email = _emailController.text.trim();
|
||||
|
||||
if (email.isEmpty) {
|
||||
_showSnackBar('Email tidak boleh kosong!', isError: true);
|
||||
return;
|
||||
}
|
||||
if (!email.contains('@') || !email.contains('.')) {
|
||||
_showSnackBar('Format email tidak valid!', isError: true);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
await SupabaseService().resetPassword(email);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_otpSent = true;
|
||||
_email = email;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
final err = e.toString().toLowerCase();
|
||||
String pesan = 'Gagal mengirim kode, coba lagi.';
|
||||
if (err.contains('network') || err.contains('socket')) {
|
||||
pesan = 'Tidak ada koneksi internet.';
|
||||
} else if (err.contains('rate limit') || err.contains('too many')) {
|
||||
pesan = 'Terlalu banyak percobaan, tunggu beberapa menit.';
|
||||
} else if (err.contains('user not found') || err.contains('invalid')) {
|
||||
pesan = 'Email tidak terdaftar di sistem.';
|
||||
}
|
||||
_showSnackBar(pesan, isError: true);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2 — verifikasi OTP dan masuk ke halaman ganti password
|
||||
Future<void> _handleVerifikasiOTP() async {
|
||||
final otp = _otpController.text.trim();
|
||||
|
||||
if (otp.isEmpty || otp.length != 6) {
|
||||
_showSnackBar('Masukkan kode 6 angka dari email!', isError: true);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
await SupabaseService().verifyOTP(_email, otp);
|
||||
if (mounted) {
|
||||
// OTP valid — langsung ke halaman ganti password
|
||||
Navigator.pushReplacementNamed(
|
||||
context,
|
||||
'/change_password',
|
||||
arguments: {'dari_reset': true},
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
final err = e.toString().toLowerCase();
|
||||
String pesan = 'Kode tidak valid atau sudah kadaluarsa.';
|
||||
if (err.contains('expired')) {
|
||||
pesan = 'Kode sudah kadaluarsa. Kirim ulang kode baru.';
|
||||
} else if (err.contains('invalid')) {
|
||||
pesan = 'Kode salah. Periksa kembali kode di email.';
|
||||
}
|
||||
_showSnackBar(pesan, isError: true);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
void _showSnackBar(String pesan, {required bool isError}) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(children: [
|
||||
Icon(
|
||||
isError ? Icons.error_outline : Icons.check_circle_outline,
|
||||
color: Colors.white, // Tetap putih untuk kontras
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(pesan,
|
||||
style: const TextStyle(
|
||||
color: Colors.white, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
]),
|
||||
// --- BERUBAH: Menggunakan warna notifikasi dari AppColors ---
|
||||
backgroundColor:
|
||||
isError ? AppColors.error : AppColors.success,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
margin: const EdgeInsets.all(16),
|
||||
duration: const Duration(seconds: 4),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
// --- BERUBAH: Background Utama ---
|
||||
backgroundColor: AppColors.background,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
// --- BERUBAH: Warna Ikon Back ---
|
||||
icon: const Icon(Icons.arrow_back, color: AppColors.primary),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 30.0),
|
||||
child: _otpSent ? _buildStepOTP() : _buildStepEmail(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ── Step 1: Input Email ──
|
||||
Widget _buildStepEmail() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Lupa Password?',
|
||||
style: TextStyle(
|
||||
// --- BERUBAH: Warna Judul ---
|
||||
color: AppColors.primary, fontSize: 32, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Masukkan email akun kamu. Kami akan kirim kode verifikasi 6 angka ke inbox kamu.',
|
||||
// --- BERUBAH: Warna Sub-judul ---
|
||||
style: TextStyle(color: AppColors.textSecondary, fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 50),
|
||||
|
||||
// Field email
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
// --- BERUBAH: Latar Belakang Form ---
|
||||
color: AppColors.cardBg,
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
// --- BERUBAH: Garis Pinggir Form ---
|
||||
border: Border.all(color: AppColors.textSecondary.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: TextField(
|
||||
controller: _emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
// --- BERUBAH: Warna Teks Inputan ---
|
||||
style: const TextStyle(color: AppColors.textMain),
|
||||
decoration: const InputDecoration(
|
||||
// --- BERUBAH: Warna Ikon ---
|
||||
prefixIcon: Icon(Icons.email, color: AppColors.primary),
|
||||
hintText: 'Email',
|
||||
// --- BERUBAH: Warna Placeholder/Hint ---
|
||||
hintStyle: TextStyle(color: AppColors.textSecondary),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
|
||||
// Tombol kirim
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 55,
|
||||
child: ElevatedButton(
|
||||
onPressed: _isLoading ? null : _handleKirimOTP,
|
||||
style: ElevatedButton.styleFrom(
|
||||
// --- BERUBAH: Warna Tombol ---
|
||||
backgroundColor: AppColors.primary,
|
||||
// --- BERUBAH: Warna Tombol saat Loading ---
|
||||
disabledBackgroundColor: AppColors.primary.withValues(alpha: 0.3),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15)),
|
||||
),
|
||||
child: _isLoading
|
||||
? const CircularProgressIndicator(color: Colors.white)
|
||||
: const Text(
|
||||
'Kirim Kode Verifikasi',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Center(
|
||||
child: TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Kembali ke Login',
|
||||
// --- BERUBAH: Warna Teks Bawah ---
|
||||
style: TextStyle(color: AppColors.primary)),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ── Step 2: Input OTP ──
|
||||
Widget _buildStepOTP() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const CircleAvatar(
|
||||
radius: 40,
|
||||
// --- BERUBAH: Latar Belakang Ikon ---
|
||||
backgroundColor: AppColors.cardBg,
|
||||
// --- BERUBAH: Warna Ikon ---
|
||||
child: Icon(Icons.mark_email_read, size: 40, color: AppColors.primary),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Text(
|
||||
'Cek Email Kamu',
|
||||
style: TextStyle(
|
||||
// --- BERUBAH: Warna Judul ---
|
||||
color: AppColors.primary, fontSize: 28, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Kode verifikasi 6 angka sudah dikirim ke:\n$_email\n\nMasukkan kode tersebut di bawah ini.',
|
||||
// --- BERUBAH: Warna Subteks ---
|
||||
style: const TextStyle(color: AppColors.textSecondary, fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
|
||||
// Field OTP
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
// --- BERUBAH: Latar Belakang Kotak OTP ---
|
||||
color: AppColors.cardBg,
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
// --- BERUBAH: Garis Pinggir Kotak OTP ---
|
||||
border: Border.all(color: AppColors.primary.withValues(alpha: 0.5)),
|
||||
),
|
||||
child: TextField(
|
||||
controller: _otpController,
|
||||
keyboardType: TextInputType.number,
|
||||
maxLength: 6,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
// --- BERUBAH: Warna Teks Inputan ---
|
||||
color: AppColors.textMain, fontSize: 28, letterSpacing: 12),
|
||||
decoration: const InputDecoration(
|
||||
// --- BERUBAH: Warna Ikon ---
|
||||
prefixIcon: Icon(Icons.lock_open, color: AppColors.primary),
|
||||
hintText: '000000',
|
||||
// --- BERUBAH: Warna Placeholder/Hint ---
|
||||
hintStyle: TextStyle(color: AppColors.textSecondary, letterSpacing: 8),
|
||||
border: InputBorder.none,
|
||||
counterText: '', // sembunyikan counter maxLength
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
|
||||
// Tombol verifikasi
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 55,
|
||||
child: ElevatedButton(
|
||||
onPressed: _isLoading ? null : _handleVerifikasiOTP,
|
||||
style: ElevatedButton.styleFrom(
|
||||
// --- BERUBAH: Warna Tombol ---
|
||||
backgroundColor: AppColors.primary,
|
||||
// --- BERUBAH: Warna Tombol saat Loading ---
|
||||
disabledBackgroundColor: AppColors.primary.withValues(alpha: 0.3),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15)),
|
||||
),
|
||||
child: _isLoading
|
||||
? const CircularProgressIndicator(color: Colors.white)
|
||||
: const Text(
|
||||
'Verifikasi Kode',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Kirim ulang kode
|
||||
Center(
|
||||
child: TextButton(
|
||||
onPressed: _isLoading
|
||||
? null
|
||||
: () => setState(() {
|
||||
_otpSent = false;
|
||||
_otpController.clear();
|
||||
}),
|
||||
child: const Text(
|
||||
'Kirim ulang kode',
|
||||
// --- BERUBAH: Warna Teks Bawah ---
|
||||
style: TextStyle(color: AppColors.primary),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../services/mqtt_service.dart';
|
||||
import '../constants/app_colors.dart'; // <-- IMPORT APP COLORS DITAMBAHKAN
|
||||
import 'monitoring_screen.dart';
|
||||
import 'control_settings_screen.dart';
|
||||
import 'recording_screen.dart';
|
||||
import 'settings_screen.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
|
||||
@override
|
||||
State<HomeScreen> createState() => _HomeScreenState();
|
||||
}
|
||||
|
||||
class _HomeScreenState extends State<HomeScreen> {
|
||||
int _currentIndex = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Listen for MQTT connection results to show notifications
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final mqtt = Provider.of<MqttService>(context, listen: false);
|
||||
mqtt.onConnectionResult = (message, isSuccess) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
message,
|
||||
// Opsional: Teks putih agar selalu kontras dengan warna notifikasi
|
||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)
|
||||
),
|
||||
// --- BERUBAH: Warna Notifikasi Berhasil / Gagal ---
|
||||
backgroundColor: isSuccess
|
||||
? AppColors.success.withValues(alpha: 0.9)
|
||||
: AppColors.error,
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
List<Widget> get _screens => [
|
||||
MonitoringScreen(onProfileTap: () => setState(() => _currentIndex = 3)),
|
||||
const ControlSettingsScreen(),
|
||||
const RecordingScreen(),
|
||||
const SettingsScreen(),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: _screens[_currentIndex],
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
currentIndex: _currentIndex,
|
||||
onTap: (index) => setState(() => _currentIndex = index),
|
||||
type: BottomNavigationBarType.fixed,
|
||||
// --- BERUBAH: Latar Belakang Menu Navigasi Bawah ---
|
||||
backgroundColor: AppColors.cardBg,
|
||||
// --- BERUBAH: Warna Ikon yang Sedang Dipilih ---
|
||||
selectedItemColor: AppColors.primary,
|
||||
// --- BERUBAH: Warna Ikon yang Tidak Dipilih ---
|
||||
unselectedItemColor: AppColors.textSecondary,
|
||||
showSelectedLabels: false,
|
||||
showUnselectedLabels: false,
|
||||
elevation: 10, // Menambahkan sedikit bayangan agar terpisah dari body
|
||||
items: const [
|
||||
BottomNavigationBarItem(icon: Icon(Icons.grid_view), label: 'Monitoring'),
|
||||
BottomNavigationBarItem(icon: Icon(Icons.tune), label: 'Control'),
|
||||
BottomNavigationBarItem(icon: Icon(Icons.access_time), label: 'Recording'),
|
||||
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,404 +0,0 @@
|
|||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pdf/pdf.dart';
|
||||
import 'package:pdf/widgets.dart' as pw;
|
||||
import 'package:printing/printing.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../constants/app_colors.dart';
|
||||
|
||||
class ImageResultScreen extends StatelessWidget {
|
||||
const ImageResultScreen({super.key});
|
||||
|
||||
// ============================================================
|
||||
// GENERATE PDF
|
||||
// ============================================================
|
||||
|
||||
Future<void> _exportPDF(
|
||||
BuildContext context, Map<String, dynamic> args) async {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => const Center(
|
||||
child: CircularProgressIndicator(color: AppColors.primary),
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
final pdf = pw.Document();
|
||||
|
||||
pw.MemoryImage? fotoImage;
|
||||
final urlFoto = args['url_foto']?.toString() ?? '';
|
||||
if (urlFoto.isNotEmpty) {
|
||||
try {
|
||||
final response = await http.get(Uri.parse(urlFoto));
|
||||
if (response.statusCode == 200) {
|
||||
fotoImage = pw.MemoryImage(response.bodyBytes);
|
||||
}
|
||||
} catch (_) {
|
||||
// Foto gagal didownload, lanjut tanpa foto
|
||||
}
|
||||
}
|
||||
|
||||
pdf.addPage(
|
||||
pw.Page(
|
||||
pageFormat: PdfPageFormat.a4,
|
||||
margin: const pw.EdgeInsets.all(32),
|
||||
build: (pw.Context context) {
|
||||
return pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.Container(
|
||||
width: double.infinity,
|
||||
padding: const pw.EdgeInsets.all(16),
|
||||
decoration: pw.BoxDecoration(
|
||||
color: PdfColors.green800,
|
||||
borderRadius: pw.BorderRadius.circular(8),
|
||||
),
|
||||
child: pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.Text(
|
||||
'Dataset Pengeringan Biji Kopi',
|
||||
style: pw.TextStyle(
|
||||
color: PdfColors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: pw.FontWeight.bold,
|
||||
),
|
||||
),
|
||||
pw.SizedBox(height: 4),
|
||||
pw.Text(
|
||||
'Sistem Monitoring Otomatis',
|
||||
style: const pw.TextStyle(
|
||||
color: PdfColors.white,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
pw.SizedBox(height: 20),
|
||||
|
||||
pw.Text(
|
||||
'Tanggal & Waktu: ${args['date']} - ${args['time']}',
|
||||
style: pw.TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: pw.FontWeight.bold,
|
||||
),
|
||||
),
|
||||
|
||||
pw.SizedBox(height: 16),
|
||||
pw.Divider(),
|
||||
pw.SizedBox(height: 16),
|
||||
|
||||
if (fotoImage != null) ...[
|
||||
pw.Center(
|
||||
child: pw.Container(
|
||||
height: 180,
|
||||
width: double.infinity,
|
||||
decoration: pw.BoxDecoration(
|
||||
borderRadius: pw.BorderRadius.circular(8),
|
||||
border: pw.Border.all(color: PdfColors.grey300),
|
||||
),
|
||||
child: pw.ClipRRect(
|
||||
horizontalRadius: 8,
|
||||
verticalRadius: 8,
|
||||
child: pw.Image(
|
||||
fotoImage,
|
||||
fit: pw.BoxFit.contain,
|
||||
height: 180,
|
||||
width: double.infinity,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
pw.SizedBox(height: 16),
|
||||
pw.Divider(),
|
||||
pw.SizedBox(height: 16),
|
||||
],
|
||||
|
||||
pw.Text(
|
||||
'Data Sensor',
|
||||
style: pw.TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: pw.FontWeight.bold,
|
||||
),
|
||||
),
|
||||
pw.SizedBox(height: 10),
|
||||
|
||||
pw.Table(
|
||||
border: pw.TableBorder.all(color: PdfColors.grey300),
|
||||
columnWidths: {
|
||||
0: const pw.FlexColumnWidth(1),
|
||||
1: const pw.FlexColumnWidth(2),
|
||||
},
|
||||
children: [
|
||||
_pdfRow('Suhu', args['temp']),
|
||||
_pdfRow('Kelembapan', args['humidity']),
|
||||
_pdfRow('Intensitas Cahaya', args['light']),
|
||||
_pdfRow('Fan Intake (Kipas 1)', args['intake']),
|
||||
_pdfRow('Fan Exhaust (Kipas 2)', args['exhaust']),
|
||||
],
|
||||
),
|
||||
|
||||
pw.SizedBox(height: 24),
|
||||
pw.Divider(),
|
||||
pw.SizedBox(height: 8),
|
||||
|
||||
pw.Text(
|
||||
'Diekspor dari Aplikasi Monitoring Kopi',
|
||||
style: const pw.TextStyle(
|
||||
color: PdfColors.grey,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (context.mounted) Navigator.pop(context);
|
||||
|
||||
await Printing.layoutPdf(
|
||||
onLayout: (PdfPageFormat format) async => pdf.save(),
|
||||
name:
|
||||
'dataset_kopi_${args['date']?.replaceAll('/', '-')}_${args['time']?.replaceAll(':', '-')}.pdf',
|
||||
);
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Gagal membuat PDF: $e'),
|
||||
backgroundColor: AppColors.error,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
margin: const EdgeInsets.all(16),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pw.TableRow _pdfRow(String label, String? value) {
|
||||
return pw.TableRow(
|
||||
children: [
|
||||
pw.Padding(
|
||||
padding: const pw.EdgeInsets.all(8),
|
||||
child: pw.Text(
|
||||
label,
|
||||
style: pw.TextStyle(fontWeight: pw.FontWeight.bold, fontSize: 11),
|
||||
),
|
||||
),
|
||||
pw.Padding(
|
||||
padding: const pw.EdgeInsets.all(8),
|
||||
child: pw.Text(value ?? '-', style: const pw.TextStyle(fontSize: 11)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// BUILD UI
|
||||
// ============================================================
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final args = ModalRoute.of(context)?.settings.arguments
|
||||
as Map<String, dynamic>? ??
|
||||
{
|
||||
'id': '1',
|
||||
'date': '2025/11/18',
|
||||
'time': '14:35:00',
|
||||
'temp': '45.5 °C',
|
||||
'humidity': '52 %',
|
||||
'light': '200 Lux',
|
||||
'intake': 'ON',
|
||||
'exhaust': 'ON',
|
||||
'url_foto': '',
|
||||
};
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
title: const Text(
|
||||
'Detail Data Citra',
|
||||
style: TextStyle(color: AppColors.textMain),
|
||||
),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: AppColors.primary),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
children: [
|
||||
// ── Foto ──
|
||||
Container(
|
||||
height: 250,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.cardBg,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: AppColors.textSecondary.withValues(alpha: 0.15)),
|
||||
),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: args['url_foto'] != null &&
|
||||
args['url_foto'].toString().isNotEmpty
|
||||
? Image.network(
|
||||
args['url_foto'],
|
||||
fit: BoxFit.cover,
|
||||
loadingBuilder: (context, child, progress) {
|
||||
if (progress == null) return child;
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(color: AppColors.primary),
|
||||
);
|
||||
},
|
||||
errorBuilder: (context, error, stack) => Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.broken_image,
|
||||
size: 80, color: AppColors.textSecondary),
|
||||
Text('Citra #${args['id']}',
|
||||
style: const TextStyle(color: AppColors.textSecondary)),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.image,
|
||||
size: 80, color: AppColors.textSecondary),
|
||||
Text('Citra #${args['id']}',
|
||||
style: const TextStyle(color: AppColors.textSecondary)),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// ── Data Card ──
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.cardBg,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: AppColors.textSecondary.withValues(alpha: 0.15)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.calendar_today,
|
||||
color: AppColors.primary, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'${args['date']} - ${args['time']}',
|
||||
style: const TextStyle(
|
||||
color: AppColors.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
Divider(color: AppColors.textSecondary.withValues(alpha: 0.3), height: 30),
|
||||
|
||||
_buildDataRow(
|
||||
'Suhu', args['temp'], 'Kelembaban', args['humidity']),
|
||||
const SizedBox(height: 15),
|
||||
|
||||
// --- BERUBAH: Mengirimkan string kosong ('') untuk menghapus kolom Mode ---
|
||||
_buildDataRow(
|
||||
'Cahaya', args['light'], '', ''),
|
||||
|
||||
Divider(color: AppColors.textSecondary.withValues(alpha: 0.3), height: 30),
|
||||
|
||||
_buildDataRow(
|
||||
'Kipas 1 (Intake)', args['intake'],
|
||||
'Kipas 2 (Exhaust)', args['exhaust'],
|
||||
valueColor: AppColors.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
|
||||
// ── Tombol Export PDF ──
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 55,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => _exportPDF(context, args),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
),
|
||||
icon: const Icon(Icons.picture_as_pdf, color: Colors.white),
|
||||
label: const Text(
|
||||
'Export Data Lengkap (.PDF)',
|
||||
style: TextStyle(
|
||||
color: Colors.white, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- BERUBAH: Menambahkan logika untuk menyembunyikan kolom kanan jika string kosong ---
|
||||
Widget _buildDataRow(
|
||||
String label1,
|
||||
String value1,
|
||||
String label2,
|
||||
String value2, {
|
||||
Color? valueColor,
|
||||
}) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label1,
|
||||
style: const TextStyle(color: AppColors.textSecondary, fontSize: 12)),
|
||||
const SizedBox(height: 4),
|
||||
Text(': $value1',
|
||||
style: TextStyle(
|
||||
color: valueColor ?? AppColors.textMain,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: label2.isEmpty
|
||||
? const SizedBox.shrink() // Jika kosong, sembunyikan sepenuhnya
|
||||
: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label2,
|
||||
style: const TextStyle(color: AppColors.textSecondary, fontSize: 12)),
|
||||
const SizedBox(height: 4),
|
||||
Text(': $value2',
|
||||
style: TextStyle(
|
||||
color: valueColor ?? AppColors.textMain,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,259 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import '../services/supabase_service.dart';
|
||||
import '../constants/app_colors.dart'; // <-- IMPORT APP COLORS DITAMBAHKAN
|
||||
|
||||
class LoginScreen extends StatefulWidget {
|
||||
const LoginScreen({super.key});
|
||||
|
||||
@override
|
||||
State<LoginScreen> createState() => _LoginScreenState();
|
||||
}
|
||||
|
||||
class _LoginScreenState extends State<LoginScreen> {
|
||||
final _emailController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
bool _isLoading = false;
|
||||
bool _obscurePassword = true;
|
||||
|
||||
Future<void> _handleLogin() async {
|
||||
if (_emailController.text.trim().isEmpty ||
|
||||
_passwordController.text.trim().isEmpty) {
|
||||
_showError('Email dan password tidak boleh kosong!');
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
await SupabaseService().signIn(
|
||||
_emailController.text.trim(),
|
||||
_passwordController.text.trim(),
|
||||
);
|
||||
if (mounted) {
|
||||
Navigator.pushReplacementNamed(context, '/home');
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
final errorStr = e.toString().toLowerCase();
|
||||
String pesanError = 'Login gagal, coba lagi.';
|
||||
|
||||
if (errorStr.contains('invalid_credentials') ||
|
||||
errorStr.contains('invalid login credentials') ||
|
||||
errorStr.contains('email or password')) {
|
||||
pesanError = 'Email atau password salah!';
|
||||
} else if (errorStr.contains('not confirmed')) {
|
||||
pesanError = 'Email belum dikonfirmasi, cek inbox kamu.';
|
||||
} else if (errorStr.contains('network') ||
|
||||
errorStr.contains('socket') ||
|
||||
errorStr.contains('connection')) {
|
||||
pesanError = 'Tidak ada koneksi internet.';
|
||||
} else if (errorStr.contains('too many') ||
|
||||
errorStr.contains('rate limit')) {
|
||||
pesanError = 'Terlalu banyak percobaan, tunggu sebentar.';
|
||||
} else if (errorStr.contains('user not found')) {
|
||||
pesanError = 'Email atau password salah!';
|
||||
}
|
||||
|
||||
_showError(pesanError);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
void _showError(String pesan) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
const Icon(Icons.error_outline, color: Colors.white), // Tetap putih agar kontras di atas merah
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
pesan,
|
||||
style: const TextStyle(
|
||||
color: Colors.white, // Tetap putih
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// --- BERUBAH: Background SnackBar Error ---
|
||||
backgroundColor: AppColors.error,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
margin: const EdgeInsets.all(16),
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
// --- BERUBAH: Background Scaffold Utama ---
|
||||
backgroundColor: AppColors.background,
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 30.0, vertical: 80.0),
|
||||
child: Column(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 60,
|
||||
// --- BERUBAH: Latar belakang logo ---
|
||||
backgroundColor: AppColors.primary,
|
||||
child: ClipOval(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Image.asset(
|
||||
'assets/icon/kopibaru_removebg.png',
|
||||
width: 120,
|
||||
height: 120,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
const Text(
|
||||
'Selamat Datang',
|
||||
style: TextStyle(
|
||||
// --- BERUBAH: Warna Judul ---
|
||||
color: AppColors.primary,
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
const Text(
|
||||
'Sistem Kendali Pengering Kopi',
|
||||
// --- BERUBAH: Warna Sub-judul ---
|
||||
style: TextStyle(color: AppColors.textSecondary, fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 50),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
// --- BERUBAH: Warna Kotak Form ---
|
||||
color: AppColors.cardBg,
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
// --- BERUBAH: Warna Garis Pinggir Form ---
|
||||
border: Border.all(color: AppColors.textSecondary.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
TextField(
|
||||
controller: _emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
// --- BERUBAH: Warna Teks Inputan ---
|
||||
style: const TextStyle(color: AppColors.textMain),
|
||||
decoration: const InputDecoration(
|
||||
// --- BERUBAH: Warna Ikon ---
|
||||
prefixIcon: Icon(Icons.email, color: AppColors.primary),
|
||||
hintText: 'Email',
|
||||
// --- BERUBAH: Warna Placeholder/Hint ---
|
||||
hintStyle: TextStyle(color: AppColors.textSecondary),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
),
|
||||
// --- BERUBAH: Garis Pemisah (Divider) ---
|
||||
Divider(color: AppColors.textSecondary.withValues(alpha: 0.3)),
|
||||
TextField(
|
||||
controller: _passwordController,
|
||||
obscureText: _obscurePassword,
|
||||
// --- BERUBAH: Warna Teks Inputan ---
|
||||
style: const TextStyle(color: AppColors.textMain),
|
||||
decoration: InputDecoration(
|
||||
// --- BERUBAH: Warna Ikon ---
|
||||
prefixIcon: const Icon(Icons.lock, color: AppColors.primary),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_obscurePassword
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
// --- BERUBAH: Warna Ikon Mata ---
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
onPressed: () => setState(
|
||||
() => _obscurePassword = !_obscurePassword),
|
||||
),
|
||||
hintText: 'Password',
|
||||
// --- BERUBAH: Warna Placeholder/Hint ---
|
||||
hintStyle: const TextStyle(color: AppColors.textSecondary),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 55,
|
||||
child: ElevatedButton(
|
||||
onPressed: _isLoading ? null : _handleLogin,
|
||||
style: ElevatedButton.styleFrom(
|
||||
// --- BERUBAH: Latar Belakang Tombol ---
|
||||
backgroundColor: AppColors.primary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
),
|
||||
child: _isLoading
|
||||
? const CircularProgressIndicator(
|
||||
color: Colors.white) // Tetap putih untuk kontras
|
||||
: const Text(
|
||||
'Masuk ke Akun',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white), // Tetap putih
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// ── 1. TOMBOL LUPA PASSWORD ──
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pushNamed(context, '/forgot_password'),
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
minimumSize: Size.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
child: const Text(
|
||||
'Lupa Password?',
|
||||
style: TextStyle(
|
||||
// --- BERUBAH: Warna Teks ---
|
||||
color: AppColors.primary,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// ── 2. TOMBOL DAFTAR AKUN ──
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pushNamed(context, '/register'),
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
minimumSize: Size.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
child: const Text(
|
||||
'Belum punya akun? Daftar Sekarang',
|
||||
style: TextStyle(
|
||||
// --- BERUBAH: Warna Teks ---
|
||||
color: AppColors.primary,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,477 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:mqtt_client/mqtt_client.dart';
|
||||
import '../services/mqtt_service.dart';
|
||||
import '../services/supabase_service.dart';
|
||||
import '../constants/app_colors.dart'; // <-- IMPORT APP COLORS DITAMBAHKAN
|
||||
|
||||
class MonitoringScreen extends StatefulWidget {
|
||||
final VoidCallback? onProfileTap;
|
||||
const MonitoringScreen({super.key, this.onProfileTap});
|
||||
|
||||
@override
|
||||
State<MonitoringScreen> createState() => _MonitoringScreenState();
|
||||
}
|
||||
|
||||
class _MonitoringScreenState extends State<MonitoringScreen> {
|
||||
bool _isInitialLoading = true;
|
||||
double _prevSuhu = 0;
|
||||
double _prevKelembapan = 0;
|
||||
double _prevIntensitas = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initData();
|
||||
}
|
||||
|
||||
Future<void> _initData() async {
|
||||
setState(() => _isInitialLoading = true);
|
||||
await _loadLimits();
|
||||
if (mounted) setState(() => _isInitialLoading = false);
|
||||
}
|
||||
|
||||
Future<void> _loadLimits() async {
|
||||
try {
|
||||
final data = await SupabaseService().getBatasSensor();
|
||||
if (data != null && mounted) {
|
||||
Provider.of<MqttService>(context, listen: false).setLimits(
|
||||
double.parse(data['suhu_max'].toString()),
|
||||
double.parse(data['rh_max'].toString()),
|
||||
double.parse(data['suhu_min'].toString()),
|
||||
double.parse(data['rh_min'].toString()),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error loading limits: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final mqtt = Provider.of<MqttService>(context);
|
||||
final curSuhu = double.tryParse(mqtt.suhu) ?? 0;
|
||||
final curHum = double.tryParse(mqtt.kelembapan) ?? 0;
|
||||
final curLux = double.tryParse(mqtt.intensitas) ?? 0;
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_prevSuhu = curSuhu;
|
||||
_prevKelembapan = curHum;
|
||||
_prevIntensitas = curLux;
|
||||
});
|
||||
|
||||
return Scaffold(
|
||||
// --- BERUBAH: Background Utama ---
|
||||
backgroundColor: AppColors.background,
|
||||
body: SafeArea(
|
||||
child: _isInitialLoading
|
||||
// --- BERUBAH: Warna Loading Indicator ---
|
||||
? const Center(child: CircularProgressIndicator(color: AppColors.primary))
|
||||
: RefreshIndicator(
|
||||
onRefresh: _initData,
|
||||
// --- BERUBAH: Warna Refresh Indicator ---
|
||||
color: AppColors.primary,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
const headerHeight = 175.0;
|
||||
const gaps = 15.0 * 2;
|
||||
const padding = 20.0 * 2;
|
||||
final available = constraints.maxHeight - headerHeight - gaps - padding;
|
||||
final cardH = (available / 3).clamp(100.0, 180.0);
|
||||
|
||||
return SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minHeight: constraints.maxHeight),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// ── Header ──
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Monitoring',
|
||||
style: TextStyle(
|
||||
// --- BERUBAH: Warna Judul ---
|
||||
color: AppColors.textMain,
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold)),
|
||||
const Text('Sistem Pengeringan Kopi',
|
||||
// --- BERUBAH: Warna Sub-judul ---
|
||||
style: TextStyle(color: AppColors.textSecondary, fontSize: 16)),
|
||||
const SizedBox(height: 5),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 8, height: 8,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
// --- BERUBAH: Indikator Titik Koneksi ---
|
||||
color: mqtt.client?.connectionStatus?.state ==
|
||||
MqttConnectionState.connected
|
||||
? AppColors.success
|
||||
: AppColors.error,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
mqtt.client?.connectionStatus?.state ==
|
||||
MqttConnectionState.connected
|
||||
? 'Connected'
|
||||
: 'Disconnected',
|
||||
style: TextStyle(
|
||||
// --- BERUBAH: Warna Teks Koneksi ---
|
||||
color: mqtt.client?.connectionStatus?.state ==
|
||||
MqttConnectionState.connected
|
||||
? AppColors.success
|
||||
: AppColors.error,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: widget.onProfileTap,
|
||||
child: const CircleAvatar(
|
||||
// --- BERUBAH: Latar Belakang Avatar Profil ---
|
||||
backgroundColor: AppColors.cardBg,
|
||||
radius: 25,
|
||||
// --- BERUBAH: Warna Ikon Avatar ---
|
||||
child: Icon(Icons.person, color: AppColors.primary),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
|
||||
// ── Timestamp ──
|
||||
Text('Update: ${mqtt.timestamp}',
|
||||
// --- BERUBAH: Warna Teks Timestamp ---
|
||||
style: const TextStyle(color: AppColors.textSecondary, fontSize: 11)),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// ── Mode Badge ──
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
// --- BERUBAH: Latar Belakang Transparan Badge ---
|
||||
color: mqtt.mode == 'manual'
|
||||
? AppColors.warning.withValues(alpha: 0.2)
|
||||
: AppColors.success.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
// --- BERUBAH: Warna Garis Pinggir Badge ---
|
||||
color: mqtt.mode == 'manual' ? AppColors.warning : AppColors.success,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Mode: ${mqtt.mode.toUpperCase()}',
|
||||
style: TextStyle(
|
||||
// --- BERUBAH: Warna Teks Badge ---
|
||||
color: mqtt.mode == 'manual' ? AppColors.warning : AppColors.success,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
|
||||
// ── Baris 1: Suhu & Kelembaban ──
|
||||
SizedBox(
|
||||
height: cardH,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildSensorCard(
|
||||
title: 'Suhu',
|
||||
prev: _prevSuhu,
|
||||
cur: curSuhu,
|
||||
suffix: '°C',
|
||||
dec: 1,
|
||||
icon: Icons.thermostat,
|
||||
// --- BERUBAH: Warna Tema Kartu Suhu ---
|
||||
color: curSuhu > mqtt.maxSuhu ? AppColors.error : AppColors.warning,
|
||||
isAlert: curSuhu > mqtt.maxSuhu,
|
||||
isLow: curSuhu < mqtt.minSuhu,
|
||||
onTap: () => Navigator.pushNamed(context, '/analytics',
|
||||
arguments: {'title': 'Grafik Suhu (°C)', 'color': AppColors.error, 'value': '${mqtt.suhu}°C'}),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
Expanded(
|
||||
child: _buildSensorCard(
|
||||
title: 'Kelembaban',
|
||||
prev: _prevKelembapan,
|
||||
cur: curHum,
|
||||
suffix: '%',
|
||||
dec: 1,
|
||||
icon: Icons.water_drop,
|
||||
// --- BERUBAH: Warna Tema Kartu Kelembaban ---
|
||||
color: curHum > mqtt.maxRh ? AppColors.error : AppColors.info,
|
||||
isAlert: curHum > mqtt.maxRh,
|
||||
isLow: curHum < mqtt.minRh,
|
||||
onTap: () => Navigator.pushNamed(context, '/analytics',
|
||||
arguments: {'title': 'Grafik Kelembaban (%)', 'color': AppColors.info, 'value': '${mqtt.kelembapan}%'}),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
|
||||
// ── Baris 2: Cahaya full width ──
|
||||
SizedBox(
|
||||
height: cardH,
|
||||
width: double.infinity,
|
||||
child: _buildSensorCard(
|
||||
title: 'Cahaya',
|
||||
prev: _prevIntensitas,
|
||||
cur: curLux,
|
||||
suffix: ' Lux',
|
||||
dec: 1,
|
||||
icon: Icons.wb_sunny,
|
||||
// --- BERUBAH: Warna Tema Kartu Cahaya ---
|
||||
color: AppColors.warning,
|
||||
onTap: () => Navigator.pushNamed(context, '/analytics',
|
||||
arguments: {'title': 'Grafik Cahaya (Lux)', 'color': AppColors.light, 'value': '${mqtt.intensitas} Lux'}),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
|
||||
// ── Baris 3: Kipas 1 & Kipas 2 ──
|
||||
SizedBox(
|
||||
height: cardH,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildControlCard(
|
||||
'Kipas 1\n(Intake)',
|
||||
mqtt.isKipas1On,
|
||||
(val) => mqtt.perintahKipas("1", val),
|
||||
mqtt,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
Expanded(
|
||||
child: _buildControlCard(
|
||||
'Kipas 2\n(Exhaust)',
|
||||
mqtt.isKipas2On,
|
||||
(val) => mqtt.perintahKipas("2", val),
|
||||
mqtt,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// ── Alert Banner ──
|
||||
if (curSuhu > mqtt.maxSuhu || curHum > mqtt.maxRh)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 15),
|
||||
padding: const EdgeInsets.all(15),
|
||||
decoration: BoxDecoration(
|
||||
// --- BERUBAH: Background Peringatan ---
|
||||
color: AppColors.error.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
// --- BERUBAH: Garis Pinggir Peringatan ---
|
||||
border: Border.all(color: AppColors.error),
|
||||
),
|
||||
child: const Row(
|
||||
children: [
|
||||
// --- BERUBAH: Ikon Peringatan ---
|
||||
Icon(Icons.warning_amber_rounded, color: AppColors.error),
|
||||
SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Peringatan: Parameter pengeringan melewati batas aman!',
|
||||
style: TextStyle(
|
||||
// --- BERUBAH: Teks Peringatan ---
|
||||
color: AppColors.error,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (curSuhu < mqtt.minSuhu || curHum < mqtt.minRh)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 15),
|
||||
padding: const EdgeInsets.all(15),
|
||||
decoration: BoxDecoration(
|
||||
// --- BERUBAH: Background Info/Low ---
|
||||
color: AppColors.info.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
// --- BERUBAH: Garis Pinggir Info/Low ---
|
||||
border: Border.all(color: AppColors.info),
|
||||
),
|
||||
child: const Row(
|
||||
children: [
|
||||
// --- BERUBAH: Ikon Info/Low ---
|
||||
Icon(Icons.info_outline, color: AppColors.info),
|
||||
SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Info: Parameter pengeringan di bawah batas minimum!',
|
||||
style: TextStyle(
|
||||
// --- BERUBAH: Teks Info/Low ---
|
||||
color: AppColors.info,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSensorCard({
|
||||
required String title,
|
||||
required double prev,
|
||||
required double cur,
|
||||
required String suffix,
|
||||
required int dec,
|
||||
required IconData icon,
|
||||
required Color color,
|
||||
bool isAlert = false,
|
||||
bool isLow = false,
|
||||
VoidCallback? onTap,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
// --- BERUBAH: Kartu Latar Putih Bersih atau Transparan Merah/Biru ---
|
||||
color: isAlert ? AppColors.error.withValues(alpha: 0.1) : isLow ? AppColors.info.withValues(alpha: 0.1) : AppColors.cardBg,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
// --- BERUBAH: Memberi garis halus agar kartu terlihat pop up di latar terang ---
|
||||
border: isAlert
|
||||
? Border.all(color: AppColors.error, width: 2)
|
||||
: isLow
|
||||
? Border.all(color: AppColors.info, width: 2)
|
||||
: Border.all(color: AppColors.textSecondary.withValues(alpha: 0.15)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, color: color, size: 32),
|
||||
const SizedBox(height: 6),
|
||||
// --- BERUBAH: Warna Judul Sensor ---
|
||||
Text(title, style: const TextStyle(color: AppColors.textSecondary, fontSize: 12)),
|
||||
const SizedBox(height: 4),
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween<double>(begin: prev, end: cur),
|
||||
duration: const Duration(milliseconds: 800),
|
||||
curve: Curves.easeOutCubic,
|
||||
builder: (context, value, _) => FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
'${value.toStringAsFixed(dec)}$suffix',
|
||||
style: const TextStyle(
|
||||
// --- BERUBAH: Warna Angka/Nilai Sensor ---
|
||||
color: AppColors.textMain,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildControlCard(
|
||||
String title,
|
||||
bool isOn,
|
||||
Function(bool) onChanged,
|
||||
MqttService mqtt,
|
||||
) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
// --- BERUBAH: Kartu Latar Putih Bersih & Garis Halus ---
|
||||
color: AppColors.cardBg,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: AppColors.textSecondary.withValues(alpha: 0.15)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
// --- BERUBAH: Warna Ikon Kontroler ---
|
||||
Icon(Icons.settings_input_component,
|
||||
color: isOn ? AppColors.primary : AppColors.textSecondary, size: 16),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
// --- BERUBAH: Warna Judul Kartu Kontrol ---
|
||||
child: Text(title,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textMain,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold)),
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// --- BERUBAH: Warna Status Mode ---
|
||||
Text('Mode: ${mqtt.mode.toUpperCase()}',
|
||||
style: const TextStyle(
|
||||
color: AppColors.primary, fontSize: 9, fontWeight: FontWeight.bold)),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
isOn ? 'ON' : 'OFF',
|
||||
style: TextStyle(
|
||||
// --- BERUBAH: Warna Status ON/OFF ---
|
||||
color: isOn ? AppColors.primary : AppColors.textSecondary,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
SizedBox(
|
||||
height: 28,
|
||||
child: Switch(
|
||||
value: isOn,
|
||||
onChanged: mqtt.mode == 'manual' ? onChanged : null,
|
||||
// --- BERUBAH: Warna Switch ---
|
||||
activeColor: AppColors.primary,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,683 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../services/mqtt_service.dart';
|
||||
import '../services/supabase_service.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import '../constants/app_colors.dart';
|
||||
import '../services/recording_service.dart'; // Memori Abadi Timer Tetap Dipakai
|
||||
import 'dart:io';
|
||||
import 'dart:convert';
|
||||
import 'package:archive/archive_io.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class RecordingScreen extends StatefulWidget {
|
||||
const RecordingScreen({super.key});
|
||||
|
||||
@override
|
||||
State<RecordingScreen> createState() => _RecordingScreenState();
|
||||
}
|
||||
|
||||
class _RecordingScreenState extends State<RecordingScreen> {
|
||||
final TextEditingController _intervalController = TextEditingController();
|
||||
List<Map<String, dynamic>> _gallery = [];
|
||||
bool _isLoading = true;
|
||||
bool _isSelectionMode = false;
|
||||
final Set<int> _selectedIndices = {}; // Menyimpan index foto yang dicentang
|
||||
|
||||
final recordingService = RecordingService();
|
||||
late final RealtimeChannel _channel;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadData();
|
||||
|
||||
_channel = Supabase.instance.client
|
||||
.channel('realtime-foto')
|
||||
.onPostgresChanges(
|
||||
event: PostgresChangeEvent.insert,
|
||||
schema: 'public',
|
||||
table: 'foto_dataset',
|
||||
callback: (payload) {
|
||||
_loadData();
|
||||
},
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_intervalController.dispose();
|
||||
Supabase.instance.client.removeChannel(_channel);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadData() async {
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
final interval = await SupabaseService().getIntervalSetting();
|
||||
final images = await SupabaseService().getFotoDataset();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_intervalController.text = interval.toString();
|
||||
_gallery = images.reversed.toList();
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error loading recording data: $e');
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _controlRecording(bool start) async {
|
||||
final mqtt = Provider.of<MqttService>(context, listen: false);
|
||||
final intervalVal = int.tryParse(_intervalController.text) ?? 30;
|
||||
|
||||
if (intervalVal <= 0) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Interval harus lebih dari 0 menit!'),
|
||||
backgroundColor: AppColors.warning,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (start) {
|
||||
await SupabaseService().updateIntervalSetting(intervalVal);
|
||||
mqtt.setIntervalFoto(intervalVal);
|
||||
recordingService.setRecordingStatus(true);
|
||||
} else {
|
||||
mqtt.setIntervalFoto(0);
|
||||
recordingService.setRecordingStatus(false);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
start
|
||||
? 'Perekaman dimulai — interval $intervalVal menit'
|
||||
: 'Perekaman dihentikan',
|
||||
style: const TextStyle(
|
||||
color: Colors.white, fontWeight: FontWeight.bold),
|
||||
),
|
||||
backgroundColor: start ? AppColors.success : AppColors.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
floatingActionButton: _selectedIndices.isNotEmpty
|
||||
? FloatingActionButton.extended(
|
||||
onPressed: () => _downloadDatasetZip(),
|
||||
backgroundColor: AppColors.primary,
|
||||
icon: const Icon(Icons.download, color: Colors.white),
|
||||
label: Text(
|
||||
'Download ZIP (${_selectedIndices.length})',
|
||||
style: const TextStyle(
|
||||
color: Colors.white, fontWeight: FontWeight.bold),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
backgroundColor: AppColors.background,
|
||||
body: SafeArea(
|
||||
child: _isLoading
|
||||
? const Center(
|
||||
child: CircularProgressIndicator(color: AppColors.primary))
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Perekaman',
|
||||
style: TextStyle(
|
||||
color: AppColors.textMain,
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
const Text(
|
||||
'Dataset Citra Kopi',
|
||||
style: TextStyle(
|
||||
color: AppColors.textSecondary, fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// LISTENABLE BUILDER: Membuat timer kebal pindah halaman
|
||||
ListenableBuilder(
|
||||
listenable: recordingService,
|
||||
builder: (context, child) {
|
||||
final isRecording = recordingService.isRecording;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.cardBg,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: AppColors.textSecondary
|
||||
.withValues(alpha: 0.15)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Interval Waktu (menit)',
|
||||
style: TextStyle(
|
||||
color: AppColors.textMain,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextField(
|
||||
controller: _intervalController,
|
||||
keyboardType: TextInputType.number,
|
||||
style:
|
||||
const TextStyle(color: AppColors.textMain),
|
||||
decoration: InputDecoration(
|
||||
hintText: '30',
|
||||
hintStyle: const TextStyle(
|
||||
color: AppColors.textSecondary),
|
||||
filled: true,
|
||||
fillColor: AppColors.background,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
borderSide: BorderSide(
|
||||
color: AppColors.textSecondary
|
||||
.withValues(alpha: 0.3)),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
borderSide: const BorderSide(
|
||||
color: AppColors.primary),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
if (isRecording)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.success
|
||||
.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border:
|
||||
Border.all(color: AppColors.success),
|
||||
),
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.circle,
|
||||
color: AppColors.success, size: 10),
|
||||
SizedBox(width: 8),
|
||||
Text('Perekaman berjalan...',
|
||||
style: TextStyle(
|
||||
color: AppColors.success,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: isRecording
|
||||
? null
|
||||
: () => _controlRecording(true),
|
||||
icon: const Icon(Icons.play_arrow,
|
||||
color: Colors.white),
|
||||
label: const Text('Mulai',
|
||||
style:
|
||||
TextStyle(color: Colors.white)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primary,
|
||||
disabledBackgroundColor: AppColors
|
||||
.primary
|
||||
.withValues(alpha: 0.3),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(12)),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: !isRecording
|
||||
? null
|
||||
: () => _controlRecording(false),
|
||||
icon: const Icon(Icons.stop,
|
||||
color: Colors.white),
|
||||
label: const Text('Berhenti',
|
||||
style:
|
||||
TextStyle(color: Colors.white)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.error,
|
||||
disabledBackgroundColor: AppColors.error
|
||||
.withValues(alpha: 0.3),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(12)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// KODE BARU (Sudah Termasuk Tombol Checklist & Select All):
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Galeri Dataset (${_gallery.length} foto)',
|
||||
style: const TextStyle(
|
||||
color: AppColors.textMain,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
if (_isSelectionMode) ...[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.select_all,
|
||||
color: AppColors.primary),
|
||||
tooltip: 'Pilih Semua',
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
if (_selectedIndices.length ==
|
||||
_gallery.length) {
|
||||
_selectedIndices
|
||||
.clear(); // Batal pilih semua
|
||||
} else {
|
||||
_selectedIndices.addAll(List.generate(
|
||||
_gallery.length,
|
||||
(i) => i)); // Pilih semua
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close,
|
||||
color: AppColors.error),
|
||||
tooltip: 'Batal Mode Seleksi',
|
||||
onPressed: () => setState(() {
|
||||
_isSelectionMode = false;
|
||||
_selectedIndices.clear();
|
||||
}),
|
||||
),
|
||||
] else ...[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.checklist,
|
||||
color: AppColors.primary),
|
||||
tooltip: 'Pilih Foto',
|
||||
onPressed: () =>
|
||||
setState(() => _isSelectionMode = true),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh,
|
||||
color: AppColors.primary),
|
||||
onPressed: _loadData,
|
||||
),
|
||||
],
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
Expanded(
|
||||
child: _gallery.isEmpty
|
||||
? const Center(
|
||||
child: Text('Belum ada data citra',
|
||||
style: TextStyle(
|
||||
color: AppColors.textSecondary)),
|
||||
)
|
||||
: GridView.builder(
|
||||
gridDelegate:
|
||||
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 10,
|
||||
mainAxisSpacing: 10,
|
||||
),
|
||||
itemCount: _gallery.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = _gallery[index];
|
||||
final nomorFoto = _gallery.length - index;
|
||||
|
||||
// PENYARING WAKTU: Ubah 'T' jadi Spasi agar kodingan lamamu aman
|
||||
String rawTime = item['timestamp'] ??
|
||||
item['created_at'] ??
|
||||
'';
|
||||
String cleanTime = rawTime.replaceAll('T', ' ');
|
||||
|
||||
final tanggal = cleanTime.contains(' ')
|
||||
? cleanTime.split(' ')[0]
|
||||
: cleanTime;
|
||||
final jam = cleanTime.contains(' ')
|
||||
? cleanTime.split(' ')[1]
|
||||
: '';
|
||||
final jamSingkat =
|
||||
jam.length >= 5 ? jam.substring(0, 5) : jam;
|
||||
|
||||
return GestureDetector(
|
||||
// --- FITUR BARU: Logika Tahan & Klik ---
|
||||
onLongPress: () {
|
||||
setState(() {
|
||||
_isSelectionMode = true;
|
||||
_selectedIndices.add(index);
|
||||
});
|
||||
},
|
||||
onTap: () {
|
||||
if (_isSelectionMode) {
|
||||
setState(() {
|
||||
if (_selectedIndices.contains(index)) {
|
||||
_selectedIndices.remove(index);
|
||||
} else {
|
||||
_selectedIndices.add(index);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// KODE LAMAMU TETAP AMAN DI SINI (Navigasi ke Detail)
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
'/image_result',
|
||||
arguments: {
|
||||
'id': item['id'].toString(),
|
||||
'date': tanggal,
|
||||
'time': jamSingkat,
|
||||
'temp': (item['suhu'] == 0 ||
|
||||
item['suhu'] == null)
|
||||
? '-'
|
||||
: '${item['suhu']} °C',
|
||||
'humidity': (item['kelembapan'] ==
|
||||
0 ||
|
||||
item['kelembapan'] == null)
|
||||
? '-'
|
||||
: '${item['kelembapan']} %',
|
||||
'light': (item['intensitas'] == 0 ||
|
||||
item['intensitas'] == null)
|
||||
? '-'
|
||||
: '${item['intensitas']} Lux',
|
||||
'location': 'Gudang Pengering',
|
||||
'intake': item['kipas1'] ?? 'OFF',
|
||||
'exhaust': item['kipas2'] ?? 'OFF',
|
||||
'url_foto': item['url_foto'] ?? '',
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
// KODE WADAH FOTO LAMAMU (Hanya ditambah opacity saat dicentang)
|
||||
Opacity(
|
||||
opacity:
|
||||
_selectedIndices.contains(index)
|
||||
? 0.6
|
||||
: 1.0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.cardBg,
|
||||
borderRadius:
|
||||
BorderRadius.circular(15),
|
||||
border: Border.all(
|
||||
color: _selectedIndices
|
||||
.contains(index)
|
||||
? AppColors.primary
|
||||
: AppColors.textSecondary
|
||||
.withValues(alpha: 0.15),
|
||||
width: _selectedIndices
|
||||
.contains(index)
|
||||
? 2.0
|
||||
: 1.0,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: ClipRRect(
|
||||
borderRadius:
|
||||
const BorderRadius
|
||||
.vertical(
|
||||
top: Radius.circular(
|
||||
14)),
|
||||
child: item['url_foto'] !=
|
||||
null
|
||||
? Image.network(
|
||||
item['url_foto'],
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder:
|
||||
(_, __, ___) =>
|
||||
const Center(
|
||||
child: Icon(
|
||||
Icons
|
||||
.broken_image,
|
||||
color: AppColors
|
||||
.textSecondary,
|
||||
size: 40),
|
||||
),
|
||||
)
|
||||
: const Center(
|
||||
child: Icon(
|
||||
Icons.image,
|
||||
color: AppColors
|
||||
.textSecondary,
|
||||
size: 40),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 6),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
borderRadius:
|
||||
BorderRadius.vertical(
|
||||
bottom:
|
||||
Radius.circular(
|
||||
15)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment
|
||||
.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets
|
||||
.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary
|
||||
.withValues(
|
||||
alpha: 0.15),
|
||||
borderRadius:
|
||||
BorderRadius
|
||||
.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
'#$nomorFoto',
|
||||
style: const TextStyle(
|
||||
color: AppColors
|
||||
.primary,
|
||||
fontSize: 10,
|
||||
fontWeight:
|
||||
FontWeight
|
||||
.bold),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment
|
||||
.end,
|
||||
children: [
|
||||
Text(tanggal,
|
||||
style: const TextStyle(
|
||||
color: AppColors
|
||||
.textSecondary,
|
||||
fontSize: 8)),
|
||||
Text(jamSingkat,
|
||||
style: const TextStyle(
|
||||
color: AppColors
|
||||
.textMain,
|
||||
fontSize: 10,
|
||||
fontWeight:
|
||||
FontWeight
|
||||
.bold)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// --- FITUR BARU: Ikon Centang ---
|
||||
if (_isSelectionMode)
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
child: Icon(
|
||||
_selectedIndices.contains(index)
|
||||
? Icons.check_circle
|
||||
: Icons.radio_button_unchecked,
|
||||
color:
|
||||
_selectedIndices.contains(index)
|
||||
? AppColors.primary
|
||||
: Colors.white,
|
||||
shadows: const [
|
||||
Shadow(
|
||||
blurRadius: 2.0,
|
||||
color: Colors.black45)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// --- FITUR BARU: Fungsi Membuat ZIP dan Ekspor ---
|
||||
// =====================================================================
|
||||
Future<void> _downloadDatasetZip() async {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => AlertDialog(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const CircularProgressIndicator(color: AppColors.primary),
|
||||
const SizedBox(height: 20),
|
||||
Text('Mengunduh & Membungkus ${_selectedIndices.length} Data...',
|
||||
textAlign: TextAlign.center),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
final archive = Archive();
|
||||
// Membuat header (judul kolom) untuk file Excel/CSV
|
||||
String csvContent =
|
||||
"Nama_File,Tanggal,Jam,Suhu(C),Kelembapan(%),Intensitas_Cahaya(Lux),Kipas_1,Kipas_2\n";
|
||||
|
||||
for (int index in _selectedIndices) {
|
||||
final item = _gallery[index];
|
||||
|
||||
// Membersihkan format waktu
|
||||
String rawTime = item['timestamp'] ?? item['created_at'] ?? '';
|
||||
String cleanTime = rawTime.replaceAll('T', ' ');
|
||||
final tanggal =
|
||||
cleanTime.contains(' ') ? cleanTime.split(' ')[0] : cleanTime;
|
||||
final jam = cleanTime.contains(' ') ? cleanTime.split(' ')[1] : '';
|
||||
final jamSingkat = jam.length >= 5 ? jam.substring(0, 5) : jam;
|
||||
|
||||
// Nama file gambar
|
||||
String fileName =
|
||||
'IMG_${tanggal}_${jamSingkat.replaceAll(':', '-')}.jpg';
|
||||
|
||||
// Unduh gambar dari Supabase
|
||||
if (item['url_foto'] != null &&
|
||||
item['url_foto'].toString().isNotEmpty) {
|
||||
final response = await http.get(Uri.parse(item['url_foto']));
|
||||
if (response.statusCode == 200) {
|
||||
archive.addFile(ArchiveFile(
|
||||
fileName, response.bodyBytes.length, response.bodyBytes));
|
||||
}
|
||||
}
|
||||
|
||||
// Tambah baris data ke Excel/CSV
|
||||
csvContent +=
|
||||
"$fileName,$tanggal,$jamSingkat,${item['suhu']},${item['kelembapan']},${item['intensitas']},${item['kipas1'] ?? 'OFF'},${item['kipas2'] ?? 'OFF'}\n";
|
||||
}
|
||||
|
||||
// Masukkan file CSV ke dalam ZIP
|
||||
final csvBytes = utf8.encode(csvContent);
|
||||
archive.addFile(
|
||||
ArchiveFile("Data_Sensor_Dataset.csv", csvBytes.length, csvBytes));
|
||||
|
||||
// Proses kompresi file ZIP
|
||||
final zipBytes = ZipEncoder().encode(archive);
|
||||
|
||||
// Simpan sementara di memori HP
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final zipPath = '${tempDir.path}/Dataset_Kopi_IoT.zip';
|
||||
final zipFile = File(zipPath);
|
||||
await zipFile.writeAsBytes(zipBytes!);
|
||||
|
||||
if (mounted) Navigator.pop(context); // Tutup loading dialog
|
||||
|
||||
// Buka popup Share/Save untuk disimpan ke HP pengguna
|
||||
await Share.shareXFiles(
|
||||
[XFile(zipPath)],
|
||||
text:
|
||||
'Ini adalah Dataset Kopi berformat ZIP. Ekstrak untuk melihat foto dan tabel Excel.',
|
||||
);
|
||||
|
||||
// Reset mode seleksi
|
||||
setState(() {
|
||||
_isSelectionMode = false;
|
||||
_selectedIndices.clear();
|
||||
});
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Gagal membuat ZIP: $e'),
|
||||
backgroundColor: AppColors.error),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,279 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import '../services/supabase_service.dart';
|
||||
import '../constants/app_colors.dart'; // <-- IMPORT APP COLORS DITAMBAHKAN
|
||||
|
||||
class RegisterScreen extends StatefulWidget {
|
||||
const RegisterScreen({super.key});
|
||||
|
||||
@override
|
||||
State<RegisterScreen> createState() => _RegisterScreenState();
|
||||
}
|
||||
|
||||
class _RegisterScreenState extends State<RegisterScreen> {
|
||||
final _nameController = TextEditingController();
|
||||
final _emailController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
bool _isLoading = false;
|
||||
bool _obscurePassword = true;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_emailController.dispose();
|
||||
_passwordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _handleRegister() async {
|
||||
final nama = _nameController.text.trim();
|
||||
final email = _emailController.text.trim();
|
||||
final password = _passwordController.text.trim();
|
||||
|
||||
// ── Validasi kosong ──
|
||||
if (nama.isEmpty || email.isEmpty || password.isEmpty) {
|
||||
_showSnackBar('Semua field harus diisi!', isError: true);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Validasi format email ──
|
||||
if (!email.contains('@') || !email.contains('.')) {
|
||||
_showSnackBar('Format email tidak valid!', isError: true);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Validasi panjang password ──
|
||||
if (password.length < 6) {
|
||||
_showSnackBar('Password minimal 6 karakter!', isError: true);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
await SupabaseService().signUp(email, password, nama);
|
||||
if (mounted) {
|
||||
_showSnackBar('Akun berhasil dibuat! Silakan login.', isError: false);
|
||||
Future.delayed(const Duration(milliseconds: 1500), () {
|
||||
if (mounted) Navigator.pop(context);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
final errorStr = e.toString().toLowerCase();
|
||||
String pesan = 'Pendaftaran gagal, coba lagi.';
|
||||
|
||||
if (errorStr.contains('user_already_exists') ||
|
||||
errorStr.contains('already registered') ||
|
||||
errorStr.contains('already been registered')) {
|
||||
pesan = 'Email sudah terdaftar, silakan login.';
|
||||
} else if (errorStr.contains('invalid') &&
|
||||
errorStr.contains('email')) {
|
||||
pesan = 'Format email tidak valid!';
|
||||
} else if (errorStr.contains('password') &&
|
||||
errorStr.contains('weak')) {
|
||||
pesan = 'Password terlalu lemah, gunakan kombinasi huruf dan angka.';
|
||||
} else if (errorStr.contains('network') ||
|
||||
errorStr.contains('socket')) {
|
||||
pesan = 'Tidak ada koneksi internet.';
|
||||
}
|
||||
|
||||
_showSnackBar(pesan, isError: true);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
void _showSnackBar(String pesan, {required bool isError}) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
Icon(
|
||||
isError ? Icons.error_outline : Icons.check_circle_outline,
|
||||
color: Colors.white, // Tetap putih agar kontras dengan background
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
pesan,
|
||||
style: const TextStyle(
|
||||
color: Colors.white, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// --- BERUBAH: Menggunakan warna notifikasi dari AppColors ---
|
||||
backgroundColor:
|
||||
isError ? AppColors.error : AppColors.success,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
margin: const EdgeInsets.all(16),
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
// --- BERUBAH: Background Scaffold Utama ---
|
||||
backgroundColor: AppColors.background,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
// --- BERUBAH: Warna Ikon Back ---
|
||||
icon: const Icon(Icons.arrow_back, color: AppColors.primary),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 30.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Buat Akun Baru',
|
||||
style: TextStyle(
|
||||
// --- BERUBAH: Warna Judul ---
|
||||
color: AppColors.primary,
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
const Text(
|
||||
'Lengkapi data diri Anda untuk memulai',
|
||||
// --- BERUBAH: Warna Sub-judul ---
|
||||
style: TextStyle(color: AppColors.textSecondary, fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 50),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
// --- BERUBAH: Latar Belakang Form ---
|
||||
color: AppColors.cardBg,
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
// --- BERUBAH: Garis Pinggir Form ---
|
||||
border:
|
||||
Border.all(color: AppColors.textSecondary.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Nama Lengkap
|
||||
TextField(
|
||||
controller: _nameController,
|
||||
// --- BERUBAH: Warna Teks Inputan ---
|
||||
style: const TextStyle(color: AppColors.textMain),
|
||||
textCapitalization: TextCapitalization.words,
|
||||
decoration: const InputDecoration(
|
||||
// --- BERUBAH: Warna Ikon ---
|
||||
prefixIcon:
|
||||
Icon(Icons.person, color: AppColors.primary),
|
||||
hintText: 'Nama Lengkap',
|
||||
// --- BERUBAH: Warna Placeholder/Hint ---
|
||||
hintStyle: TextStyle(color: AppColors.textSecondary),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
),
|
||||
// --- BERUBAH: Garis Pemisah (Divider) ---
|
||||
Divider(color: AppColors.textSecondary.withValues(alpha: 0.3)),
|
||||
|
||||
// Email
|
||||
TextField(
|
||||
controller: _emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
// --- BERUBAH: Warna Teks Inputan ---
|
||||
style: const TextStyle(color: AppColors.textMain),
|
||||
decoration: const InputDecoration(
|
||||
// --- BERUBAH: Warna Ikon ---
|
||||
prefixIcon:
|
||||
Icon(Icons.email, color: AppColors.primary),
|
||||
hintText: 'Email',
|
||||
// --- BERUBAH: Warna Placeholder/Hint ---
|
||||
hintStyle: TextStyle(color: AppColors.textSecondary),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
),
|
||||
// --- BERUBAH: Garis Pemisah (Divider) ---
|
||||
Divider(color: AppColors.textSecondary.withValues(alpha: 0.3)),
|
||||
|
||||
// Password
|
||||
TextField(
|
||||
controller: _passwordController,
|
||||
obscureText: _obscurePassword,
|
||||
// --- BERUBAH: Warna Teks Inputan ---
|
||||
style: const TextStyle(color: AppColors.textMain),
|
||||
decoration: InputDecoration(
|
||||
// --- BERUBAH: Warna Ikon Kunci ---
|
||||
prefixIcon:
|
||||
const Icon(Icons.lock, color: AppColors.primary),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_obscurePassword
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
// --- BERUBAH: Warna Ikon Mata ---
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
onPressed: () => setState(
|
||||
() => _obscurePassword = !_obscurePassword),
|
||||
),
|
||||
hintText: 'Password (min. 6 karakter)',
|
||||
// --- BERUBAH: Warna Placeholder/Hint ---
|
||||
hintStyle: const TextStyle(color: AppColors.textSecondary),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
|
||||
// Tombol Daftar
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 55,
|
||||
child: ElevatedButton(
|
||||
onPressed: _isLoading ? null : _handleRegister,
|
||||
style: ElevatedButton.styleFrom(
|
||||
// --- BERUBAH: Warna Tombol ---
|
||||
backgroundColor: AppColors.primary,
|
||||
// --- BERUBAH: Warna Tombol saat Nonaktif (Loading) ---
|
||||
disabledBackgroundColor:
|
||||
AppColors.primary.withValues(alpha: 0.3),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
),
|
||||
child: _isLoading
|
||||
? const CircularProgressIndicator(
|
||||
color: Colors.white)
|
||||
: const Text(
|
||||
'Daftar Sekarang',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
Center(
|
||||
child: TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text(
|
||||
'Sudah punya akun? Masuk',
|
||||
// --- BERUBAH: Warna Teks Bawah ---
|
||||
style: TextStyle(color: AppColors.primary),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,477 +0,0 @@
|
|||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:excel/excel.dart' hide Border;
|
||||
import 'package:open_file/open_file.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import '../services/supabase_service.dart';
|
||||
import '../services/mqtt_service.dart';
|
||||
import 'package:mqtt_client/mqtt_client.dart';
|
||||
import '../constants/app_colors.dart'; // <-- IMPORT APP COLORS DITAMBAHKAN
|
||||
|
||||
class SettingsScreen extends StatefulWidget {
|
||||
const SettingsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<SettingsScreen> createState() => _SettingsScreenState();
|
||||
}
|
||||
|
||||
class _SettingsScreenState extends State<SettingsScreen> {
|
||||
String _namaLengkap = '';
|
||||
bool _isLoadingProfile = true;
|
||||
bool _isExporting = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadProfile();
|
||||
}
|
||||
|
||||
Future<void> _loadProfile() async {
|
||||
try {
|
||||
final profile = await SupabaseService().getProfile();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_namaLengkap = profile?['nama_lengkap'] ?? '';
|
||||
_isLoadingProfile = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) setState(() => _isLoadingProfile = false);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Export foto_dataset ke Excel (Warna Excel Tetap Dipertahankan) ──
|
||||
Future<void> _exportExcel() async {
|
||||
setState(() => _isExporting = true);
|
||||
|
||||
try {
|
||||
// Ambil semua data dari foto_dataset
|
||||
final data = await SupabaseService().getFotoDataset();
|
||||
|
||||
if (data.isEmpty) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text('Belum ada data dataset untuk diexport!'),
|
||||
// --- BERUBAH: Warna Notifikasi Peringatan ---
|
||||
backgroundColor: AppColors.warning,
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Buat file Excel
|
||||
final excel = Excel.createExcel();
|
||||
final sheet = excel['Dataset Kopi'];
|
||||
|
||||
// Header kolom
|
||||
final headers = [
|
||||
'No',
|
||||
'Timestamp',
|
||||
'Suhu (°C)',
|
||||
'Kelembapan (%)',
|
||||
'Intensitas Cahaya (Lux)',
|
||||
'Kipas 1 (Intake)',
|
||||
'Kipas 2 (Exhaust)',
|
||||
];
|
||||
|
||||
// Style header (Dibiarkan tetap hijau standar Excel)
|
||||
for (int i = 0; i < headers.length; i++) {
|
||||
final cell = sheet.cell(
|
||||
CellIndex.indexByColumnRow(columnIndex: i, rowIndex: 0));
|
||||
cell.value = TextCellValue(headers[i]);
|
||||
cell.cellStyle = CellStyle(
|
||||
bold: true,
|
||||
backgroundColorHex: ExcelColor.fromHexString('#16A34A'),
|
||||
fontColorHex: ExcelColor.fromHexString('#FFFFFF'),
|
||||
);
|
||||
}
|
||||
|
||||
// Isi data — tanpa URL Foto
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
final row = data[i];
|
||||
final rowIndex = i + 1;
|
||||
|
||||
sheet.cell(CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: rowIndex))
|
||||
.value = IntCellValue(i + 1);
|
||||
|
||||
sheet.cell(CellIndex.indexByColumnRow(columnIndex: 1, rowIndex: rowIndex))
|
||||
.value = TextCellValue(row['timestamp']?.toString() ?? '-');
|
||||
|
||||
sheet.cell(CellIndex.indexByColumnRow(columnIndex: 2, rowIndex: rowIndex))
|
||||
.value = DoubleCellValue(double.tryParse(row['suhu'].toString()) ?? 0);
|
||||
|
||||
sheet.cell(CellIndex.indexByColumnRow(columnIndex: 3, rowIndex: rowIndex))
|
||||
.value = DoubleCellValue(double.tryParse(row['kelembapan'].toString()) ?? 0);
|
||||
|
||||
sheet.cell(CellIndex.indexByColumnRow(columnIndex: 4, rowIndex: rowIndex))
|
||||
.value = DoubleCellValue(double.tryParse(row['intensitas'].toString()) ?? 0);
|
||||
|
||||
sheet.cell(CellIndex.indexByColumnRow(columnIndex: 5, rowIndex: rowIndex))
|
||||
.value = TextCellValue(row['kipas1']?.toString() ?? '-');
|
||||
|
||||
sheet.cell(CellIndex.indexByColumnRow(columnIndex: 6, rowIndex: rowIndex))
|
||||
.value = TextCellValue(row['kipas2']?.toString() ?? '-');
|
||||
}
|
||||
|
||||
// Set lebar kolom — tanpa kolom URL
|
||||
sheet.setColumnWidth(0, 5);
|
||||
sheet.setColumnWidth(1, 22);
|
||||
sheet.setColumnWidth(2, 12);
|
||||
sheet.setColumnWidth(3, 16);
|
||||
sheet.setColumnWidth(4, 24);
|
||||
sheet.setColumnWidth(5, 18);
|
||||
sheet.setColumnWidth(6, 18);
|
||||
|
||||
// Simpan file
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
final tanggal = DateTime.now()
|
||||
.toString()
|
||||
.substring(0, 10)
|
||||
.replaceAll('-', '');
|
||||
final fileName = 'dataset_kopi_$tanggal.xlsx';
|
||||
final filePath = '${dir.path}/$fileName';
|
||||
final fileBytes = excel.save();
|
||||
|
||||
if (fileBytes == null) throw Exception('Gagal generate file Excel');
|
||||
|
||||
final file = File(filePath);
|
||||
await file.writeAsBytes(fileBytes);
|
||||
|
||||
// Share file
|
||||
await OpenFile.open(filePath);
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'✓ Export berhasil — ${data.length} data tersimpan'),
|
||||
// --- BERUBAH: Warna Notifikasi Sukses ---
|
||||
backgroundColor: AppColors.success,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Gagal export: $e'),
|
||||
// --- BERUBAH: Warna Notifikasi Error ---
|
||||
backgroundColor: AppColors.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _isExporting = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final mqtt = Provider.of<MqttService>(context);
|
||||
final user = SupabaseService().currentUser;
|
||||
|
||||
return Scaffold(
|
||||
// --- BERUBAH: Background UI Utama ---
|
||||
backgroundColor: AppColors.background,
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// ── Profile Section ──
|
||||
Container(
|
||||
padding: const EdgeInsets.all(15),
|
||||
decoration: BoxDecoration(
|
||||
// --- BERUBAH: Background Kartu Profil ---
|
||||
color: AppColors.cardBg,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
// --- BERUBAH: Garis pinggir halus ---
|
||||
border: Border.all(color: AppColors.textSecondary.withValues(alpha: 0.15)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const CircleAvatar(
|
||||
radius: 30,
|
||||
// --- BERUBAH: Warna Avatar ---
|
||||
backgroundColor: AppColors.primary,
|
||||
child: Icon(Icons.person,
|
||||
color: Colors.white, size: 40),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_isLoadingProfile
|
||||
? const SizedBox(
|
||||
width: 100,
|
||||
height: 16,
|
||||
child: LinearProgressIndicator(
|
||||
// --- BERUBAH: Warna Loading ---
|
||||
color: AppColors.primary,
|
||||
backgroundColor: AppColors.background,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
_namaLengkap.isNotEmpty
|
||||
? _namaLengkap
|
||||
: 'Admin Kopi',
|
||||
style: const TextStyle(
|
||||
// --- BERUBAH: Warna Nama Profil ---
|
||||
color: AppColors.textMain,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
user?.email ?? 'admin@coffee.io',
|
||||
style: const TextStyle(
|
||||
// --- BERUBAH: Warna Email Profil ---
|
||||
color: AppColors.textSecondary,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Text(
|
||||
'Status: Active User',
|
||||
style: TextStyle(
|
||||
// --- BERUBAH: Warna Status Profil ---
|
||||
color: AppColors.success,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 30),
|
||||
|
||||
// ── Sistem & Koneksi ──
|
||||
_buildSectionTitle('Sistem & Koneksi'),
|
||||
_buildSettingsCard([
|
||||
_buildConnectionStatus(
|
||||
'Database Supabase', true, Icons.storage),
|
||||
// --- BERUBAH: Warna Garis Pemisah Menu ---
|
||||
Divider(color: AppColors.textSecondary.withValues(alpha: 0.2), height: 1),
|
||||
_buildConnectionStatus(
|
||||
'MQTT Broker (EMQX)',
|
||||
mqtt.client?.connectionStatus?.state ==
|
||||
MqttConnectionState.connected,
|
||||
Icons.cloud_sync,
|
||||
),
|
||||
]),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// ── Manajemen Data ──
|
||||
_buildSectionTitle('Manajemen Data'),
|
||||
_buildSettingsCard([
|
||||
_isExporting
|
||||
? const ListTile(
|
||||
leading: SizedBox(
|
||||
width: 22,
|
||||
height: 22,
|
||||
child: CircularProgressIndicator(
|
||||
// --- BERUBAH: Warna Loading ---
|
||||
color: AppColors.primary, strokeWidth: 2),
|
||||
),
|
||||
title: Text(
|
||||
'Menyiapkan file Excel...',
|
||||
style: TextStyle(
|
||||
// --- BERUBAH: Warna Teks Loading ---
|
||||
color: AppColors.textSecondary, fontSize: 14),
|
||||
),
|
||||
)
|
||||
: _buildSettingItem(
|
||||
Icons.table_chart,
|
||||
'Export Data Dataset (.xlsx)',
|
||||
_exportExcel,
|
||||
),
|
||||
]),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// ── Keamanan ──
|
||||
_buildSectionTitle('Keamanan'),
|
||||
_buildSettingsCard([
|
||||
_buildSettingItem(
|
||||
Icons.lock_reset, 'Ganti Password Akun', () {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
'/change_password',
|
||||
arguments: {'dari_reset': false},
|
||||
);
|
||||
}),
|
||||
]),
|
||||
|
||||
const SizedBox(height: 30),
|
||||
|
||||
// ── Tombol Keluar ──
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 55,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
final konfirmasi = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
// --- BERUBAH: Latar Belakang Dialog ---
|
||||
backgroundColor: AppColors.cardBg,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
title: const Row(
|
||||
children: [
|
||||
Icon(Icons.logout, color: AppColors.error),
|
||||
SizedBox(width: 10),
|
||||
Text('Keluar dari Akun',
|
||||
// --- BERUBAH: Warna Judul Dialog ---
|
||||
style: TextStyle(color: AppColors.textMain, fontSize: 18)),
|
||||
],
|
||||
),
|
||||
content: const Text(
|
||||
'Apakah kamu yakin ingin keluar dari akun ini?',
|
||||
// --- BERUBAH: Warna Teks Konten Dialog ---
|
||||
style: TextStyle(color: AppColors.textSecondary),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: const Text('Batal',
|
||||
// --- BERUBAH: Warna Tombol Batal ---
|
||||
style: TextStyle(color: AppColors.primary)),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
// --- BERUBAH: Warna Tombol Keluar di Dialog ---
|
||||
backgroundColor: AppColors.error,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
child: const Text('Keluar',
|
||||
style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (konfirmasi == true) {
|
||||
await SupabaseService().signOut();
|
||||
if (context.mounted) {
|
||||
Navigator.pushReplacementNamed(context, '/login');
|
||||
}
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
// --- BERUBAH: Warna Latar Tombol Keluar ---
|
||||
backgroundColor: AppColors.error.withValues(alpha: 0.8),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
),
|
||||
icon: const Icon(Icons.logout, color: Colors.white), // Tetap putih
|
||||
label: const Text(
|
||||
'Keluar dari Akun',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white), // Tetap putih
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 100),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionTitle(String title) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10, left: 5),
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
// --- BERUBAH: Warna Judul Seksi/Kategori ---
|
||||
color: AppColors.textSecondary,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSettingsCard(List<Widget> children) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
// --- BERUBAH: Latar Belakang Kartu Pengaturan ---
|
||||
color: AppColors.cardBg,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
// --- BERUBAH: Garis pinggir halus ---
|
||||
border: Border.all(color: AppColors.textSecondary.withValues(alpha: 0.15)),
|
||||
),
|
||||
child: Column(children: children),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSettingItem(IconData icon, String title, VoidCallback? onTap,
|
||||
{String? trailing}) {
|
||||
return ListTile(
|
||||
// --- BERUBAH: Warna Ikon Leading ---
|
||||
leading: Icon(icon, color: AppColors.primary, size: 22),
|
||||
title: Text(title,
|
||||
// --- BERUBAH: Warna Teks Menu ---
|
||||
style: const TextStyle(color: AppColors.textMain, fontSize: 14)),
|
||||
trailing: trailing != null
|
||||
? Text(trailing,
|
||||
// --- BERUBAH: Warna Teks Trailing ---
|
||||
style: const TextStyle(color: AppColors.textSecondary, fontSize: 12))
|
||||
// --- BERUBAH: Warna Ikon Panah Kanan ---
|
||||
: const Icon(Icons.chevron_right, color: AppColors.textSecondary, size: 18),
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildConnectionStatus(
|
||||
String title, bool isConnected, IconData icon) {
|
||||
return ListTile(
|
||||
// --- BERUBAH: Warna Ikon Status Database/MQTT ---
|
||||
leading: Icon(icon,
|
||||
color: isConnected ? AppColors.info : AppColors.error, size: 22),
|
||||
title: Text(title,
|
||||
// --- BERUBAH: Warna Teks Nama Layanan ---
|
||||
style: const TextStyle(color: AppColors.textMain, fontSize: 14)),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
// --- BERUBAH: Warna Titik Indikator ---
|
||||
color: isConnected ? AppColors.success : AppColors.error,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
isConnected ? 'Online' : 'Offline',
|
||||
style: TextStyle(
|
||||
// --- BERUBAH: Warna Teks Online/Offline ---
|
||||
color: isConnected ? AppColors.success : AppColors.error,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,308 +0,0 @@
|
|||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart'; // <-- IMPORT SUPABASE DITAMBAHKAN
|
||||
import '../constants/app_colors.dart';
|
||||
import 'login_screen.dart';
|
||||
|
||||
class SplashScreen extends StatefulWidget {
|
||||
const SplashScreen({super.key});
|
||||
|
||||
@override
|
||||
State<SplashScreen> createState() => _SplashScreenState();
|
||||
}
|
||||
|
||||
class _SplashScreenState extends State<SplashScreen>
|
||||
with TickerProviderStateMixin {
|
||||
// Controllers
|
||||
late AnimationController _mainController;
|
||||
late AnimationController _slideController;
|
||||
late AnimationController _textController;
|
||||
late AnimationController _revealController;
|
||||
|
||||
// Animations
|
||||
late Animation<double> _logoY;
|
||||
late Animation<double> _shadowScale;
|
||||
late Animation<double> _shadowOpacity;
|
||||
late Animation<double> _logoSlideX;
|
||||
late Animation<double> _textOpacity;
|
||||
late Animation<double> _revealProgress;
|
||||
|
||||
// Typewriter State
|
||||
final String _fullText = 'IoT Kopi';
|
||||
String _displayText = '';
|
||||
int _charIndex = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_setupAnimations();
|
||||
_mulaiAnimasi();
|
||||
}
|
||||
|
||||
void _setupAnimations() {
|
||||
// Total durasi sekuens utama = 3.6 detik
|
||||
_mainController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 3600),
|
||||
);
|
||||
|
||||
// ── KOREOGRAFI LOGO DENGAN EFEK GRAVITASI ──
|
||||
_logoY = TweenSequence<double>([
|
||||
TweenSequenceItem(tween: ConstantTween<double>(120.0), weight: 25),
|
||||
TweenSequenceItem(
|
||||
tween: Tween(begin: 120.0, end: -180.0).chain(CurveTween(curve: Curves.easeOut)),
|
||||
weight: 14,
|
||||
),
|
||||
TweenSequenceItem(
|
||||
tween: TweenSequence<double>([
|
||||
TweenSequenceItem(tween: Tween(begin: -180.0, end: -188.0).chain(CurveTween(curve: Curves.easeInOut)), weight: 50),
|
||||
TweenSequenceItem(tween: Tween(begin: -188.0, end: -180.0).chain(CurveTween(curve: Curves.easeInOut)), weight: 50),
|
||||
]),
|
||||
weight: 25,
|
||||
),
|
||||
TweenSequenceItem(
|
||||
tween: Tween(begin: -180.0, end: 0.0).chain(CurveTween(curve: Curves.easeIn)),
|
||||
weight: 11,
|
||||
),
|
||||
TweenSequenceItem(tween: Tween(begin: 0.0, end: -45.0).chain(CurveTween(curve: Curves.easeOut)), weight: 5),
|
||||
TweenSequenceItem(tween: Tween(begin: -45.0, end: 0.0).chain(CurveTween(curve: Curves.easeIn)), weight: 5),
|
||||
TweenSequenceItem(tween: Tween(begin: 0.0, end: -15.0).chain(CurveTween(curve: Curves.easeOut)), weight: 4),
|
||||
TweenSequenceItem(tween: Tween(begin: -15.0, end: 0.0).chain(CurveTween(curve: Curves.easeIn)), weight: 4),
|
||||
TweenSequenceItem(tween: ConstantTween<double>(0.0), weight: 7),
|
||||
]).animate(_mainController);
|
||||
|
||||
// ── KOREOGRAFI UKURAN BAYANGAN ──
|
||||
_shadowScale = TweenSequence<double>([
|
||||
TweenSequenceItem(tween: ConstantTween<double>(2.5), weight: 25),
|
||||
TweenSequenceItem(tween: Tween(begin: 2.5, end: 0.1).chain(CurveTween(curve: Curves.easeOut)), weight: 14),
|
||||
TweenSequenceItem(tween: ConstantTween<double>(0.1), weight: 61),
|
||||
]).animate(_mainController);
|
||||
|
||||
// ── KOREOGRAFI TRANSPARANSI BAYANGAN ──
|
||||
_shadowOpacity = TweenSequence<double>([
|
||||
TweenSequenceItem(tween: ConstantTween<double>(0.6), weight: 25),
|
||||
TweenSequenceItem(tween: Tween(begin: 0.6, end: 0.0).chain(CurveTween(curve: Curves.easeOut)), weight: 14),
|
||||
TweenSequenceItem(tween: ConstantTween<double>(0.0), weight: 61),
|
||||
]).animate(_mainController);
|
||||
|
||||
// ── SLIDE LOGO KE KIRI ──
|
||||
_slideController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
);
|
||||
_logoSlideX = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _slideController, curve: Curves.easeInOutCubic),
|
||||
);
|
||||
|
||||
// ── FADE IN OPACITY TEKS ──
|
||||
_textController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
);
|
||||
_textOpacity = Tween<double>(begin: 0.0, end: 1.0).animate(_textController);
|
||||
|
||||
// ── KONTROL TRANSISI MENGECIL ──
|
||||
_revealController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 750),
|
||||
);
|
||||
_revealProgress = Tween<double>(begin: 1.0, end: 0.0).animate(
|
||||
CurvedAnimation(parent: _revealController, curve: Curves.easeInOutQuad),
|
||||
);
|
||||
}
|
||||
|
||||
// --- FUNGSI ANIMASI YANG SUDAH DIUPDATE DENGAN PENGECEKAN LOGIN ---
|
||||
Future<void> _mulaiAnimasi() async {
|
||||
await _mainController.forward();
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
|
||||
_slideController.forward();
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
|
||||
_textController.forward();
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
// Typewriter
|
||||
_charIndex = 0;
|
||||
_displayText = '';
|
||||
for (int i = 0; i < _fullText.length; i++) {
|
||||
await Future.delayed(const Duration(milliseconds: 120));
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_charIndex++;
|
||||
_displayText = _fullText.substring(0, _charIndex);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
|
||||
if (mounted) {
|
||||
// 1. Jalankan animasi lingkaran mengecil internal sampai habis
|
||||
await _revealController.forward();
|
||||
|
||||
// 2. CEK SESI LOGIN DI SINI
|
||||
if (mounted) {
|
||||
final session = Supabase.instance.client.auth.currentSession;
|
||||
|
||||
if (session != null) {
|
||||
// JIKA SUDAH LOGIN: Lempar ke Monitoring / Home
|
||||
Navigator.of(context).pushReplacementNamed('/home');
|
||||
} else {
|
||||
// JIKA BELUM LOGIN: Lempar ke LoginScreen
|
||||
Navigator.of(context).pushReplacement(
|
||||
PageRouteBuilder(
|
||||
pageBuilder: (_, __, ___) => const LoginScreen(),
|
||||
transitionDuration: Duration.zero,
|
||||
reverseTransitionDuration: Duration.zero,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_mainController.dispose();
|
||||
_slideController.dispose();
|
||||
_textController.dispose();
|
||||
_revealController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
const logoSize = 80.0;
|
||||
|
||||
const textEstimatedWidth = 140.0;
|
||||
const gap = 16.0;
|
||||
const totalWidth = logoSize + gap + textEstimatedWidth;
|
||||
final startX = (size.width - totalWidth) / 2;
|
||||
final logoFinalX = startX;
|
||||
final logoCenterX = size.width / 2 - logoSize / 2;
|
||||
final centerY = size.height / 2;
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: _revealProgress,
|
||||
builder: (context, child) {
|
||||
return ClipPath(
|
||||
clipper: CircularRevealClipper(progress: _revealProgress.value),
|
||||
child: Scaffold(
|
||||
backgroundColor: AppColors.primary,
|
||||
body: AnimatedBuilder(
|
||||
animation: Listenable.merge([_mainController, _slideController, _textController]),
|
||||
builder: (context, child) {
|
||||
final currentLogoX = logoCenterX - (_logoSlideX.value * (logoCenterX - logoFinalX));
|
||||
final currentLogoY = centerY - logoSize / 2 + _logoY.value;
|
||||
|
||||
final shadowW = logoSize * 0.75 * _shadowScale.value;
|
||||
final shadowH = 14.0 * _shadowScale.value;
|
||||
final shadowX = currentLogoX + (logoSize - shadowW) / 2;
|
||||
final shadowY = centerY + logoSize / 2 + 6;
|
||||
|
||||
final textX = currentLogoX + logoSize + gap;
|
||||
final textY = centerY - 20;
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
// ── 1. BAYANGAN ELIPS ──
|
||||
Positioned(
|
||||
left: shadowX,
|
||||
top: shadowY,
|
||||
child: Opacity(
|
||||
opacity: _shadowOpacity.value.clamp(0.0, 1.0),
|
||||
child: Container(
|
||||
width: shadowW.clamp(0.0, logoSize * 2.5),
|
||||
height: shadowH.clamp(0.0, 20.0 * 2.5),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ── 2. LOGO ──
|
||||
Positioned(
|
||||
left: currentLogoX,
|
||||
top: currentLogoY,
|
||||
child: Opacity(
|
||||
opacity: _mainController.value > 0.25 ? 1.0 : 0.0,
|
||||
child: Container(
|
||||
width: logoSize,
|
||||
height: logoSize,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.cardBg,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.15),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipOval(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Image.asset(
|
||||
'assets/icon/kopibaru.png',
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ── 3. TEKS POPPINS ──
|
||||
Positioned(
|
||||
left: textX,
|
||||
top: textY,
|
||||
child: Opacity(
|
||||
opacity: _textOpacity.value,
|
||||
child: Text(
|
||||
_displayText,
|
||||
style: GoogleFonts.poppins(
|
||||
color: Colors.white,
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── CUSTOM CLIPPER UNTUK EFEK LINGKARAN MENGECIL TEPAT DI TENGAH LAYAR ──
|
||||
class CircularRevealClipper extends CustomClipper<Path> {
|
||||
final double progress;
|
||||
|
||||
CircularRevealClipper({required this.progress});
|
||||
|
||||
@override
|
||||
Path getClip(Size size) {
|
||||
final center = Offset(size.width / 2, size.height / 2);
|
||||
final maxRadius = sqrt(size.width * size.width + size.height * size.height);
|
||||
final radius = maxRadius * progress;
|
||||
|
||||
final path = Path();
|
||||
path.addOval(Rect.fromCircle(center: center, radius: radius));
|
||||
return path;
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldReclip(CustomClipper<Path> oldClipper) => true;
|
||||
}
|
||||
|
|
@ -1,206 +0,0 @@
|
|||
import 'dart:convert';
|
||||
import 'package:mqtt_client/mqtt_client.dart';
|
||||
import 'package:mqtt_client/mqtt_server_client.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class MqttService extends ChangeNotifier {
|
||||
MqttServerClient? client;
|
||||
|
||||
// Data sensor
|
||||
String _suhu = "0";
|
||||
String _kelembapan = "0";
|
||||
String _intensitas = "0";
|
||||
bool _isKipas1On = false;
|
||||
bool _isKipas2On = false;
|
||||
String _mode = "otomatis";
|
||||
String _timestamp = "-";
|
||||
|
||||
// Batas sensor
|
||||
double _maxSuhu = 45.0;
|
||||
double _maxRh = 70.0;
|
||||
|
||||
// Getter sensor
|
||||
String get suhu => _suhu;
|
||||
String get kelembapan => _kelembapan;
|
||||
String get intensitas => _intensitas;
|
||||
bool get isKipas1On => _isKipas1On;
|
||||
bool get isKipas2On => _isKipas2On;
|
||||
String get mode => _mode;
|
||||
String get timestamp => _timestamp;
|
||||
|
||||
// Getter batas
|
||||
double _minSuhu = 0.0;
|
||||
double _minRh = 0.0;
|
||||
|
||||
double get maxSuhu => _maxSuhu;
|
||||
double get maxRh => _maxRh;
|
||||
double get minSuhu => _minSuhu;
|
||||
double get minRh => _minRh;
|
||||
|
||||
Function(String, bool)? onConnectionResult;
|
||||
|
||||
void setLimits(double suhuMax, double rhMax, double suhuMin, double rhMin) {
|
||||
_maxSuhu = suhuMax;
|
||||
_maxRh = rhMax;
|
||||
_minSuhu = suhuMin;
|
||||
_minRh = rhMin;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<bool> connect() async {
|
||||
final String clientId =
|
||||
'flutter_kopi_${DateTime.now().millisecondsSinceEpoch % 10000}';
|
||||
|
||||
client = MqttServerClient.withPort(
|
||||
's1fd277a.ala.asia-southeast1.emqxsl.com', // EMQX host baru
|
||||
clientId,
|
||||
8883,
|
||||
);
|
||||
|
||||
client!.secure = true;
|
||||
client!.setProtocolV311();
|
||||
client!.onBadCertificate = (dynamic cert) => true;
|
||||
client!.keepAlivePeriod = 20;
|
||||
client!.connectTimeoutPeriod = 10000;
|
||||
client!.onDisconnected = onDisconnected;
|
||||
client!.onConnected = onConnected;
|
||||
client!.logging(on: kDebugMode);
|
||||
|
||||
final connMess = MqttConnectMessage()
|
||||
.withClientIdentifier(clientId)
|
||||
.authenticateAs('kopi_user', 'Kopikopi1')
|
||||
.startClean();
|
||||
client!.connectionMessage = connMess;
|
||||
|
||||
try {
|
||||
await client!.connect();
|
||||
} catch (e) {
|
||||
debugPrint('MQTT Exception: $e');
|
||||
onConnectionResult?.call('Koneksi Gagal: $e', false);
|
||||
client!.disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (client?.connectionStatus?.state == MqttConnectionState.connected) {
|
||||
debugPrint('MQTT: Terhubung!');
|
||||
onConnectionResult?.call(' Terhubung ke Server Kopi', true);
|
||||
_subscribeToTopics();
|
||||
notifyListeners();
|
||||
return true;
|
||||
} else {
|
||||
onConnectionResult?.call(' Koneksi Gagal', false);
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void _subscribeToTopics() {
|
||||
client!.subscribe("kopi/sensor", MqttQos.atMostOnce);
|
||||
client!.subscribe("kopi/relay/kipas1", MqttQos.atMostOnce);
|
||||
client!.subscribe("kopi/relay/kipas2", MqttQos.atMostOnce);
|
||||
client!.subscribe("kopi/relay/mode", MqttQos.atMostOnce);
|
||||
|
||||
client!.updates!.listen((List<MqttReceivedMessage<MqttMessage?>>? c) {
|
||||
if (c == null || c.isEmpty) return;
|
||||
|
||||
final recMess = c[0].payload as MqttPublishMessage;
|
||||
final pt = MqttPublishPayload.bytesToStringAsString(
|
||||
recMess.payload.message);
|
||||
final topic = c[0].topic;
|
||||
|
||||
if (topic == "kopi/sensor") {
|
||||
try {
|
||||
final data = jsonDecode(pt);
|
||||
_suhu = data['suhu'].toString();
|
||||
_kelembapan = data['kelembapan'].toString();
|
||||
_intensitas = data['intensitas'].toString();
|
||||
_isKipas1On = data['kipas1'] == 'ON';
|
||||
_isKipas2On = data['kipas2'] == 'ON';
|
||||
_mode = data['mode'] ?? 'otomatis';
|
||||
_timestamp = data['timestamp'] ?? '-';
|
||||
} catch (e) {
|
||||
debugPrint('MQTT parse error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
if (topic == "kopi/relay/kipas1") _isKipas1On = (pt == "ON");
|
||||
if (topic == "kopi/relay/kipas2") _isKipas2On = (pt == "ON");
|
||||
if (topic == "kopi/relay/mode") _mode = pt;
|
||||
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
|
||||
// Kirim perintah kipas manual
|
||||
void perintahKipas(String nomorKipas, bool nyala) {
|
||||
final payload = jsonEncode({
|
||||
"kipas": nomorKipas,
|
||||
"status": nyala ? "ON" : "OFF",
|
||||
});
|
||||
_publish("kopi/relay/manual", payload);
|
||||
}
|
||||
|
||||
// Ganti mode otomatis/manual
|
||||
void gantiMode(String mode) {
|
||||
_publish("kopi/relay/mode", mode);
|
||||
}
|
||||
|
||||
// Kirim interval foto dalam menit
|
||||
// Kirim 0 untuk stop
|
||||
void setIntervalFoto(int menit) {
|
||||
if (menit == 0) {
|
||||
_publish("kopi/setting/interval", "0");
|
||||
} else {
|
||||
_publish("kopi/setting/interval", menit.toString());
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger foto manual
|
||||
void triggerFoto() {
|
||||
_publish("kopi/trigger/foto", "AMBIL");
|
||||
}
|
||||
|
||||
// Kirim batas sensor ke ESP32 dan Node-RED
|
||||
void setBatasSensor(
|
||||
double suhuMin, double suhuMax, double rhMin, double rhMax) {
|
||||
final payload = jsonEncode({
|
||||
"suhu_min": suhuMin,
|
||||
"suhu_max": suhuMax,
|
||||
"rh_min": rhMin,
|
||||
"rh_max": rhMax,
|
||||
});
|
||||
_publish("kopi/setting/batas", payload);
|
||||
}
|
||||
|
||||
void _publish(String topic, String message) {
|
||||
if (client?.connectionStatus?.state != MqttConnectionState.connected) {
|
||||
debugPrint('MQTT: Tidak terhubung, tidak bisa publish');
|
||||
return;
|
||||
}
|
||||
final builder = MqttClientPayloadBuilder();
|
||||
builder.addString(message);
|
||||
client!.publishMessage(topic, MqttQos.atMostOnce, builder.payload!);
|
||||
debugPrint('MQTT publish → $topic : $message');
|
||||
}
|
||||
|
||||
void onConnected() {
|
||||
debugPrint('MQTT: onConnected');
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void onDisconnected() {
|
||||
debugPrint('MQTT: onDisconnected');
|
||||
|
||||
// 🔥 AUTO RECONNECT
|
||||
Future.delayed(const Duration(seconds: 3), () {
|
||||
debugPrint('MQTT: mencoba reconnect...');
|
||||
connect();
|
||||
});
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void disconnect() {
|
||||
client?.disconnect();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class RecordingService extends ChangeNotifier {
|
||||
// Membuat Singleton
|
||||
static final RecordingService _instance = RecordingService._internal();
|
||||
factory RecordingService() => _instance;
|
||||
|
||||
RecordingService._internal() {
|
||||
// Saat file ini pertama kali dipanggil (saat app dibuka),
|
||||
// langsung cek ke dalam memori hardisk HP!
|
||||
_loadStatusDariMemoriHP();
|
||||
}
|
||||
|
||||
bool isRecording = false; // Status bawaan
|
||||
|
||||
// FUNGSI BARU: Membaca status terakhir dari dalam HP
|
||||
Future<void> _loadStatusDariMemoriHP() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
// Jika ada catatan sebelumnya, pakai itu. Jika tidak, anggap false.
|
||||
isRecording = prefs.getBool('status_perekaman') ?? false;
|
||||
notifyListeners(); // Beritahu layar untuk update UI
|
||||
}
|
||||
|
||||
// FUNGSI DIUPDATE: Mengubah status DAN menyimpannya ke memori HP
|
||||
Future<void> setRecordingStatus(bool status) async {
|
||||
isRecording = status;
|
||||
notifyListeners(); // Beritahu layar untuk update tombol
|
||||
|
||||
// Simpan ke hardisk HP agar tahan banting walau app di-close paksa!
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool('status_perekaman', status);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,172 +0,0 @@
|
|||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
class SupabaseService {
|
||||
static final SupabaseService _instance = SupabaseService._internal();
|
||||
factory SupabaseService() => _instance;
|
||||
SupabaseService._internal();
|
||||
|
||||
final supabase = Supabase.instance.client;
|
||||
|
||||
// ============================================================
|
||||
// AUTH
|
||||
// ============================================================
|
||||
|
||||
Future<AuthResponse> signUp(
|
||||
String email, String password, String fullName) async {
|
||||
final response = await supabase.auth.signUp(
|
||||
email: email,
|
||||
password: password,
|
||||
);
|
||||
|
||||
if (response.user != null) {
|
||||
await supabase.from('profiles').insert({
|
||||
'id': response.user!.id,
|
||||
'nama_lengkap': fullName,
|
||||
'username': email.split('@')[0],
|
||||
});
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
Future<AuthResponse> signIn(String email, String password) async {
|
||||
return await supabase.auth.signInWithPassword(
|
||||
email: email,
|
||||
password: password,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> signOut() async {
|
||||
await supabase.auth.signOut();
|
||||
}
|
||||
|
||||
User? get currentUser => supabase.auth.currentUser;
|
||||
|
||||
// ============================================================
|
||||
// PROFILE
|
||||
// ============================================================
|
||||
|
||||
// Ambil profil dari tabel profiles berdasarkan user yang login
|
||||
Future<Map<String, dynamic>?> getProfile() async {
|
||||
final user = currentUser;
|
||||
if (user == null) return null;
|
||||
final response = await supabase
|
||||
.from('profiles')
|
||||
.select()
|
||||
.eq('id', user.id)
|
||||
.single();
|
||||
return response;
|
||||
}
|
||||
|
||||
// Update password akun yang sedang login
|
||||
Future<void> updatePassword(String passwordBaru) async {
|
||||
await supabase.auth.updateUser(
|
||||
UserAttributes(password: passwordBaru),
|
||||
);
|
||||
}
|
||||
|
||||
// Kirim OTP ke email
|
||||
Future<void> resetPassword(String email) async {
|
||||
await supabase.auth.signInWithOtp(
|
||||
email: email,
|
||||
shouldCreateUser: false, // jangan buat user baru
|
||||
);
|
||||
}
|
||||
|
||||
// Verifikasi OTP yang diinput user
|
||||
Future<void> verifyOTP(String email, String token) async {
|
||||
await supabase.auth.verifyOTP(
|
||||
email: email,
|
||||
token: token,
|
||||
type: OtpType.email,
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// SENSOR DATA (sensor_log)
|
||||
// ============================================================
|
||||
|
||||
Future<List<Map<String, dynamic>>> getSensorLogs() async {
|
||||
return await supabase
|
||||
.from('sensor_log')
|
||||
.select()
|
||||
.order('created_at', ascending: false)
|
||||
.limit(20);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> getLatestLog() async {
|
||||
final List<dynamic> data = await supabase
|
||||
.from('sensor_log')
|
||||
.select()
|
||||
.order('created_at', ascending: false)
|
||||
.limit(1);
|
||||
return data.isNotEmpty ? data.first as Map<String, dynamic> : null;
|
||||
}
|
||||
|
||||
Future<void> logFanAction(
|
||||
String fan1, String fan2, double suhu, double hum, double lux) async {
|
||||
await supabase.from('sensor_log').insert({
|
||||
'kipas1': fan1,
|
||||
'kipas2': fan2,
|
||||
'suhu': suhu,
|
||||
'kelembapan': hum,
|
||||
'intensitas': lux,
|
||||
'timestamp': DateTime.now().toString().substring(0, 19),
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// BATAS SENSOR (batas_sensor)
|
||||
// ============================================================
|
||||
|
||||
Future<Map<String, dynamic>?> getBatasSensor() async {
|
||||
final data = await supabase
|
||||
.from('batas_sensor')
|
||||
.select()
|
||||
.limit(1)
|
||||
.single();
|
||||
return data;
|
||||
}
|
||||
|
||||
Future<void> updateBatasSensor(
|
||||
double suhuMin, double suhuMax, double rhMin, double rhMax) async {
|
||||
await supabase.from('batas_sensor').update({
|
||||
'suhu_min': suhuMin,
|
||||
'suhu_max': suhuMax,
|
||||
'rh_min': rhMin,
|
||||
'rh_max': rhMax,
|
||||
'updated_at': DateTime.now().toIso8601String(),
|
||||
}).eq('id', 1);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// INTERVAL SETTING (interval_setting)
|
||||
// ============================================================
|
||||
|
||||
Future<int> getIntervalSetting() async {
|
||||
final data = await supabase
|
||||
.from('interval_setting')
|
||||
.select('durasi_menit')
|
||||
.limit(1)
|
||||
.single();
|
||||
return data['durasi_menit'] as int;
|
||||
}
|
||||
|
||||
Future<void> updateIntervalSetting(int menit) async {
|
||||
await supabase.from('interval_setting').update({
|
||||
'durasi_menit': menit,
|
||||
'update_at': DateTime.now().toIso8601String(),
|
||||
}).eq('id', 1);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// GALLERY (foto_dataset)
|
||||
// ============================================================
|
||||
|
||||
Future<List<Map<String, dynamic>>> getFotoDataset() async {
|
||||
final List<dynamic> data = await supabase
|
||||
.from('foto_dataset')
|
||||
.select()
|
||||
.order('created_at', ascending: true);
|
||||
return List<Map<String, dynamic>>.from(data);
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
flutter/ephemeral
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
# Project-level configuration.
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
project(runner LANGUAGES CXX)
|
||||
|
||||
# The name of the executable created for the application. Change this to change
|
||||
# the on-disk name of your application.
|
||||
set(BINARY_NAME "coffee_iot_flutter")
|
||||
# The unique GTK application identifier for this application. See:
|
||||
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
|
||||
set(APPLICATION_ID "com.example.coffee_iot_flutter")
|
||||
|
||||
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||
# versions of CMake.
|
||||
cmake_policy(SET CMP0063 NEW)
|
||||
|
||||
# Load bundled libraries from the lib/ directory relative to the binary.
|
||||
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
|
||||
|
||||
# Root filesystem for cross-building.
|
||||
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
|
||||
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
|
||||
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||
endif()
|
||||
|
||||
# Define build configuration options.
|
||||
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||
set(CMAKE_BUILD_TYPE "Debug" CACHE
|
||||
STRING "Flutter build mode" FORCE)
|
||||
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
|
||||
"Debug" "Profile" "Release")
|
||||
endif()
|
||||
|
||||
# Compilation settings that should be applied to most targets.
|
||||
#
|
||||
# Be cautious about adding new options here, as plugins use this function by
|
||||
# default. In most cases, you should add new options to specific targets instead
|
||||
# of modifying this function.
|
||||
function(APPLY_STANDARD_SETTINGS TARGET)
|
||||
target_compile_features(${TARGET} PUBLIC cxx_std_14)
|
||||
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
|
||||
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
|
||||
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
|
||||
endfunction()
|
||||
|
||||
# Flutter library and tool build rules.
|
||||
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
|
||||
add_subdirectory(${FLUTTER_MANAGED_DIR})
|
||||
|
||||
# System-level dependencies.
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
||||
|
||||
# Application build; see runner/CMakeLists.txt.
|
||||
add_subdirectory("runner")
|
||||
|
||||
# Run the Flutter tool portions of the build. This must not be removed.
|
||||
add_dependencies(${BINARY_NAME} flutter_assemble)
|
||||
|
||||
# Only the install-generated bundle's copy of the executable will launch
|
||||
# correctly, since the resources must in the right relative locations. To avoid
|
||||
# people trying to run the unbundled copy, put it in a subdirectory instead of
|
||||
# the default top-level location.
|
||||
set_target_properties(${BINARY_NAME}
|
||||
PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
|
||||
)
|
||||
|
||||
|
||||
# Generated plugin build rules, which manage building the plugins and adding
|
||||
# them to the application.
|
||||
include(flutter/generated_plugins.cmake)
|
||||
|
||||
|
||||
# === Installation ===
|
||||
# By default, "installing" just makes a relocatable bundle in the build
|
||||
# directory.
|
||||
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
|
||||
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
|
||||
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
|
||||
endif()
|
||||
|
||||
# Start with a clean build bundle directory every time.
|
||||
install(CODE "
|
||||
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
|
||||
" COMPONENT Runtime)
|
||||
|
||||
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
|
||||
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
|
||||
|
||||
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
|
||||
install(FILES "${bundled_library}"
|
||||
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
endforeach(bundled_library)
|
||||
|
||||
# Copy the native assets provided by the build.dart from all packages.
|
||||
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
|
||||
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
|
||||
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
# Fully re-copy the assets directory on each build to avoid having stale files
|
||||
# from a previous install.
|
||||
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
|
||||
install(CODE "
|
||||
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
|
||||
" COMPONENT Runtime)
|
||||
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
|
||||
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
|
||||
|
||||
# Install the AOT library on non-Debug builds only.
|
||||
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
|
||||
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
endif()
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
# This file controls Flutter-level build steps. It should not be edited.
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
|
||||
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
|
||||
|
||||
# Configuration provided via flutter tool.
|
||||
include(${EPHEMERAL_DIR}/generated_config.cmake)
|
||||
|
||||
# TODO: Move the rest of this into files in ephemeral. See
|
||||
# https://github.com/flutter/flutter/issues/57146.
|
||||
|
||||
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
|
||||
# which isn't available in 3.10.
|
||||
function(list_prepend LIST_NAME PREFIX)
|
||||
set(NEW_LIST "")
|
||||
foreach(element ${${LIST_NAME}})
|
||||
list(APPEND NEW_LIST "${PREFIX}${element}")
|
||||
endforeach(element)
|
||||
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
# === Flutter Library ===
|
||||
# System-level dependencies.
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
||||
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
|
||||
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
|
||||
|
||||
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
|
||||
|
||||
# Published to parent scope for install step.
|
||||
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
|
||||
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
|
||||
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
|
||||
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
|
||||
|
||||
list(APPEND FLUTTER_LIBRARY_HEADERS
|
||||
"fl_basic_message_channel.h"
|
||||
"fl_binary_codec.h"
|
||||
"fl_binary_messenger.h"
|
||||
"fl_dart_project.h"
|
||||
"fl_engine.h"
|
||||
"fl_json_message_codec.h"
|
||||
"fl_json_method_codec.h"
|
||||
"fl_message_codec.h"
|
||||
"fl_method_call.h"
|
||||
"fl_method_channel.h"
|
||||
"fl_method_codec.h"
|
||||
"fl_method_response.h"
|
||||
"fl_plugin_registrar.h"
|
||||
"fl_plugin_registry.h"
|
||||
"fl_standard_message_codec.h"
|
||||
"fl_standard_method_codec.h"
|
||||
"fl_string_codec.h"
|
||||
"fl_value.h"
|
||||
"fl_view.h"
|
||||
"flutter_linux.h"
|
||||
)
|
||||
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
|
||||
add_library(flutter INTERFACE)
|
||||
target_include_directories(flutter INTERFACE
|
||||
"${EPHEMERAL_DIR}"
|
||||
)
|
||||
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
|
||||
target_link_libraries(flutter INTERFACE
|
||||
PkgConfig::GTK
|
||||
PkgConfig::GLIB
|
||||
PkgConfig::GIO
|
||||
)
|
||||
add_dependencies(flutter flutter_assemble)
|
||||
|
||||
# === Flutter tool backend ===
|
||||
# _phony_ is a non-existent file to force this command to run every time,
|
||||
# since currently there's no way to get a full input/output list from the
|
||||
# flutter tool.
|
||||
add_custom_command(
|
||||
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
|
||||
${CMAKE_CURRENT_BINARY_DIR}/_phony_
|
||||
COMMAND ${CMAKE_COMMAND} -E env
|
||||
${FLUTTER_TOOL_ENVIRONMENT}
|
||||
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
|
||||
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_target(flutter_assemble DEPENDS
|
||||
"${FLUTTER_LIBRARY}"
|
||||
${FLUTTER_LIBRARY_HEADERS}
|
||||
)
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <gtk/gtk_plugin.h>
|
||||
#include <open_file_linux/open_file_linux_plugin.h>
|
||||
#include <printing/printing_plugin.h>
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) gtk_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
|
||||
gtk_plugin_register_with_registrar(gtk_registrar);
|
||||
g_autoptr(FlPluginRegistrar) open_file_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "OpenFileLinuxPlugin");
|
||||
open_file_linux_plugin_register_with_registrar(open_file_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) printing_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "PrintingPlugin");
|
||||
printing_plugin_register_with_registrar(printing_registrar);
|
||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#ifndef GENERATED_PLUGIN_REGISTRANT_
|
||||
#define GENERATED_PLUGIN_REGISTRANT_
|
||||
|
||||
#include <flutter_linux/flutter_linux.h>
|
||||
|
||||
// Registers Flutter plugins.
|
||||
void fl_register_plugins(FlPluginRegistry* registry);
|
||||
|
||||
#endif // GENERATED_PLUGIN_REGISTRANT_
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
#
|
||||
# Generated file, do not edit.
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
gtk
|
||||
open_file_linux
|
||||
printing
|
||||
url_launcher_linux
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
jni
|
||||
)
|
||||
|
||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||
|
||||
foreach(plugin ${FLUTTER_PLUGIN_LIST})
|
||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
|
||||
endforeach(plugin)
|
||||
|
||||
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
|
||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
|
||||
endforeach(ffi_plugin)
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
cmake_minimum_required(VERSION 3.13)
|
||||
project(runner LANGUAGES CXX)
|
||||
|
||||
# Define the application target. To change its name, change BINARY_NAME in the
|
||||
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
|
||||
# work.
|
||||
#
|
||||
# Any new source files that you add to the application should be added here.
|
||||
add_executable(${BINARY_NAME}
|
||||
"main.cc"
|
||||
"my_application.cc"
|
||||
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
|
||||
)
|
||||
|
||||
# Apply the standard set of build settings. This can be removed for applications
|
||||
# that need different build settings.
|
||||
apply_standard_settings(${BINARY_NAME})
|
||||
|
||||
# Add preprocessor definitions for the application ID.
|
||||
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
|
||||
|
||||
# Add dependency libraries. Add any application-specific dependencies here.
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
|
||||
|
||||
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
#include "my_application.h"
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
g_autoptr(MyApplication) app = my_application_new();
|
||||
return g_application_run(G_APPLICATION(app), argc, argv);
|
||||
}
|
||||
|
|
@ -1,148 +0,0 @@
|
|||
#include "my_application.h"
|
||||
|
||||
#include <flutter_linux/flutter_linux.h>
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
#include <gdk/gdkx.h>
|
||||
#endif
|
||||
|
||||
#include "flutter/generated_plugin_registrant.h"
|
||||
|
||||
struct _MyApplication {
|
||||
GtkApplication parent_instance;
|
||||
char** dart_entrypoint_arguments;
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
|
||||
|
||||
// Called when first Flutter frame received.
|
||||
static void first_frame_cb(MyApplication* self, FlView* view) {
|
||||
gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view)));
|
||||
}
|
||||
|
||||
// Implements GApplication::activate.
|
||||
static void my_application_activate(GApplication* application) {
|
||||
MyApplication* self = MY_APPLICATION(application);
|
||||
GtkWindow* window =
|
||||
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
|
||||
|
||||
// Use a header bar when running in GNOME as this is the common style used
|
||||
// by applications and is the setup most users will be using (e.g. Ubuntu
|
||||
// desktop).
|
||||
// If running on X and not using GNOME then just use a traditional title bar
|
||||
// in case the window manager does more exotic layout, e.g. tiling.
|
||||
// If running on Wayland assume the header bar will work (may need changing
|
||||
// if future cases occur).
|
||||
gboolean use_header_bar = TRUE;
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
GdkScreen* screen = gtk_window_get_screen(window);
|
||||
if (GDK_IS_X11_SCREEN(screen)) {
|
||||
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
|
||||
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
|
||||
use_header_bar = FALSE;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (use_header_bar) {
|
||||
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
||||
gtk_widget_show(GTK_WIDGET(header_bar));
|
||||
gtk_header_bar_set_title(header_bar, "coffee_iot_flutter");
|
||||
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
||||
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
||||
} else {
|
||||
gtk_window_set_title(window, "coffee_iot_flutter");
|
||||
}
|
||||
|
||||
gtk_window_set_default_size(window, 1280, 720);
|
||||
|
||||
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
||||
fl_dart_project_set_dart_entrypoint_arguments(
|
||||
project, self->dart_entrypoint_arguments);
|
||||
|
||||
FlView* view = fl_view_new(project);
|
||||
GdkRGBA background_color;
|
||||
// Background defaults to black, override it here if necessary, e.g. #00000000
|
||||
// for transparent.
|
||||
gdk_rgba_parse(&background_color, "#000000");
|
||||
fl_view_set_background_color(view, &background_color);
|
||||
gtk_widget_show(GTK_WIDGET(view));
|
||||
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
|
||||
|
||||
// Show the window when Flutter renders.
|
||||
// Requires the view to be realized so we can start rendering.
|
||||
g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb),
|
||||
self);
|
||||
gtk_widget_realize(GTK_WIDGET(view));
|
||||
|
||||
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
|
||||
|
||||
gtk_widget_grab_focus(GTK_WIDGET(view));
|
||||
}
|
||||
|
||||
// Implements GApplication::local_command_line.
|
||||
static gboolean my_application_local_command_line(GApplication* application,
|
||||
gchar*** arguments,
|
||||
int* exit_status) {
|
||||
MyApplication* self = MY_APPLICATION(application);
|
||||
// Strip out the first argument as it is the binary name.
|
||||
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
|
||||
|
||||
g_autoptr(GError) error = nullptr;
|
||||
if (!g_application_register(application, nullptr, &error)) {
|
||||
g_warning("Failed to register: %s", error->message);
|
||||
*exit_status = 1;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
g_application_activate(application);
|
||||
*exit_status = 0;
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Implements GApplication::startup.
|
||||
static void my_application_startup(GApplication* application) {
|
||||
// MyApplication* self = MY_APPLICATION(object);
|
||||
|
||||
// Perform any actions required at application startup.
|
||||
|
||||
G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
|
||||
}
|
||||
|
||||
// Implements GApplication::shutdown.
|
||||
static void my_application_shutdown(GApplication* application) {
|
||||
// MyApplication* self = MY_APPLICATION(object);
|
||||
|
||||
// Perform any actions required at application shutdown.
|
||||
|
||||
G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
|
||||
}
|
||||
|
||||
// Implements GObject::dispose.
|
||||
static void my_application_dispose(GObject* object) {
|
||||
MyApplication* self = MY_APPLICATION(object);
|
||||
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
|
||||
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
|
||||
}
|
||||
|
||||
static void my_application_class_init(MyApplicationClass* klass) {
|
||||
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
|
||||
G_APPLICATION_CLASS(klass)->local_command_line =
|
||||
my_application_local_command_line;
|
||||
G_APPLICATION_CLASS(klass)->startup = my_application_startup;
|
||||
G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
|
||||
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
|
||||
}
|
||||
|
||||
static void my_application_init(MyApplication* self) {}
|
||||
|
||||
MyApplication* my_application_new() {
|
||||
// Set the program name to the application ID, which helps various systems
|
||||
// like GTK and desktop environments map this running application to its
|
||||
// corresponding .desktop file. This ensures better integration by allowing
|
||||
// the application to be recognized beyond its binary name.
|
||||
g_set_prgname(APPLICATION_ID);
|
||||
|
||||
return MY_APPLICATION(g_object_new(my_application_get_type(),
|
||||
"application-id", APPLICATION_ID, "flags",
|
||||
G_APPLICATION_NON_UNIQUE, nullptr));
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
#ifndef FLUTTER_MY_APPLICATION_H_
|
||||
#define FLUTTER_MY_APPLICATION_H_
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
G_DECLARE_FINAL_TYPE(MyApplication,
|
||||
my_application,
|
||||
MY,
|
||||
APPLICATION,
|
||||
GtkApplication)
|
||||
|
||||
/**
|
||||
* my_application_new:
|
||||
*
|
||||
* Creates a new Flutter-based application.
|
||||
*
|
||||
* Returns: a new #MyApplication.
|
||||
*/
|
||||
MyApplication* my_application_new();
|
||||
|
||||
#endif // FLUTTER_MY_APPLICATION_H_
|
||||
BIN
log_flutter.txt
|
|
@ -1,7 +0,0 @@
|
|||
# Flutter-related
|
||||
**/Flutter/ephemeral/
|
||||
**/Pods/
|
||||
|
||||
# Xcode-related
|
||||
**/dgph
|
||||
**/xcuserdata/
|
||||