Compare commits

..

No commits in common. "6994e14443544ddbfa1501d110bae60001f80dd6" and "db1fdd66e47c806f269fb6b3a859a017f25f457c" have entirely different histories.

155 changed files with 0 additions and 11301 deletions

View File

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

45
.gitignore vendored
View File

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

View File

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

BIN
README.md

Binary file not shown.

View File

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

14
android/.gitignore vendored
View File

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

View File

@ -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 '../..'
}

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 B

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 B

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

@ -1,2 +0,0 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

34
ios/.gitignore vendored
View File

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

View File

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

View File

@ -1 +0,0 @@
#include "Generated.xcconfig"

View File

@ -1 +0,0 @@
#include "Generated.xcconfig"

View File

@ -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 */;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +0,0 @@
import Flutter
import UIKit
class SceneDelegate: FlutterSceneDelegate {
}

View File

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

View File

@ -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);
}

View File

@ -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(),
},
);
}
}

View File

@ -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)
),
),
],
),
);
}
}

View File

@ -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,
),
);
}
}

View File

@ -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),
),
),
),
],
),
);
}
}

View File

@ -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),
),
),
),
],
);
}
}

View File

@ -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'),
],
),
);
}
}

View File

@ -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)),
],
),
),
],
);
}
}

View File

@ -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,
),
),
),
],
),
),
),
);
}
}

View File

@ -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,
),
),
],
),
],
),
],
),
);
}
}

View File

@ -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),
);
}
}
}
}

View File

@ -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),
),
),
),
],
),
),
),
);
}
}

View File

@ -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),
),
],
),
);
}
}

View File

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

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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
linux/.gitignore vendored
View File

@ -1 +0,0 @@
flutter/ephemeral

View File

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

View File

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

View File

@ -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);
}

View File

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

View File

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

View File

@ -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}")

View File

@ -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);
}

View File

@ -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));
}

View File

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

Binary file not shown.

7
macos/.gitignore vendored
View File

@ -1,7 +0,0 @@
# Flutter-related
**/Flutter/ephemeral/
**/Pods/
# Xcode-related
**/dgph
**/xcuserdata/

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