Compare commits
9 Commits
db1fdd66e4
...
6994e14443
| Author | SHA1 | Date |
|---|---|---|
|
|
6994e14443 | |
|
|
54327cb32b | |
|
|
d5b6d8c4b2 | |
|
|
3d4741b535 | |
|
|
318f572f4a | |
|
|
3c1e9de4bf | |
|
|
1908432345 | |
|
|
1cd4bb22ee | |
|
|
02d6de8e5e |
|
|
@ -0,0 +1,21 @@
|
||||||
|
# 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\\
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.build/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
.swiftpm/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
**/doc/api/
|
||||||
|
**/ios/Flutter/.last_build_id
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
/build/
|
||||||
|
/coverage/
|
||||||
|
|
||||||
|
# Symbolication related
|
||||||
|
app.*.symbols
|
||||||
|
|
||||||
|
# Obfuscation related
|
||||||
|
app.*.map.json
|
||||||
|
|
||||||
|
# Android Studio will place build artifacts here
|
||||||
|
/android/app/debug
|
||||||
|
/android/app/profile
|
||||||
|
/android/app/release
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
# 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'
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
# This file configures the analyzer, which statically analyzes Dart code to
|
||||||
|
# check for errors, warnings, and lints.
|
||||||
|
#
|
||||||
|
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||||
|
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||||
|
# invoked from the command line by running `flutter analyze`.
|
||||||
|
|
||||||
|
# The following line activates a set of recommended lints for Flutter apps,
|
||||||
|
# packages, and plugins designed to encourage good coding practices.
|
||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
linter:
|
||||||
|
# The lint rules applied to this project can be customized in the
|
||||||
|
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||||
|
# included above or to enable additional rules. A list of all available lints
|
||||||
|
# and their documentation is published at https://dart.dev/lints.
|
||||||
|
#
|
||||||
|
# Instead of disabling a lint rule for the entire project in the
|
||||||
|
# section below, it can also be suppressed for a single line of code
|
||||||
|
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||||
|
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||||
|
# producing the lint.
|
||||||
|
rules:
|
||||||
|
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||||
|
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||||
|
|
||||||
|
# Additional information about this file can be found at
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
gradle-wrapper.jar
|
||||||
|
/.gradle
|
||||||
|
/captures/
|
||||||
|
/gradlew
|
||||||
|
/gradlew.bat
|
||||||
|
/local.properties
|
||||||
|
GeneratedPluginRegistrant.java
|
||||||
|
.cxx/
|
||||||
|
|
||||||
|
# Remember to never publicly share your keystore.
|
||||||
|
# See https://flutter.dev/to/reference-keystore
|
||||||
|
key.properties
|
||||||
|
**/*.keystore
|
||||||
|
**/*.jks
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
plugins {
|
||||||
|
id "com.android.application"
|
||||||
|
id "kotlin-android"
|
||||||
|
id "dev.flutter.flutter-gradle-plugin"
|
||||||
|
}
|
||||||
|
|
||||||
|
def localProperties = new Properties()
|
||||||
|
def localPropertiesFile = rootProject.file('local.properties')
|
||||||
|
if (localPropertiesFile.exists()) {
|
||||||
|
localPropertiesFile.withReader('UTF-8') { reader ->
|
||||||
|
localProperties.load(reader)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def flutterVersionCode = localProperties.getProperty('flutter.versionCode') ?: '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 '../..'
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
<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>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.example.coffee_iot_flutter
|
||||||
|
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
class MainActivity : FlutterActivity()
|
||||||
|
After Width: | Height: | Size: 69 B |
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?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>
|
||||||
|
After Width: | Height: | Size: 69 B |
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?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>
|
||||||
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?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>
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?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>
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?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>
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?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>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
|
android.useAndroidX=true
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
pluginManagement {
|
||||||
|
def flutterSdkPath = {
|
||||||
|
def properties = new Properties()
|
||||||
|
file("local.properties").withInputStream { properties.load(it) }
|
||||||
|
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||||
|
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||||
|
return flutterSdkPath
|
||||||
|
}()
|
||||||
|
|
||||||
|
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id "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"
|
||||||
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 186 KiB |
|
|
@ -0,0 +1,34 @@
|
||||||
|
**/dgph
|
||||||
|
*.mode1v3
|
||||||
|
*.mode2v3
|
||||||
|
*.moved-aside
|
||||||
|
*.pbxuser
|
||||||
|
*.perspectivev3
|
||||||
|
**/*sync/
|
||||||
|
.sconsign.dblite
|
||||||
|
.tags*
|
||||||
|
**/.vagrant/
|
||||||
|
**/DerivedData/
|
||||||
|
Icon?
|
||||||
|
**/Pods/
|
||||||
|
**/.symlinks/
|
||||||
|
profile
|
||||||
|
xcuserdata
|
||||||
|
**/.generated/
|
||||||
|
Flutter/App.framework
|
||||||
|
Flutter/Flutter.framework
|
||||||
|
Flutter/Flutter.podspec
|
||||||
|
Flutter/Generated.xcconfig
|
||||||
|
Flutter/ephemeral/
|
||||||
|
Flutter/app.flx
|
||||||
|
Flutter/app.zip
|
||||||
|
Flutter/flutter_assets/
|
||||||
|
Flutter/flutter_export_environment.sh
|
||||||
|
ServiceDefinitions.json
|
||||||
|
Runner/GeneratedPluginRegistrant.*
|
||||||
|
|
||||||
|
# Exceptions to above rules.
|
||||||
|
!default.mode1v3
|
||||||
|
!default.mode2v3
|
||||||
|
!default.pbxuser
|
||||||
|
!default.perspectivev3
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>en</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>App</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>io.flutter.flutter.app</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>App</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>FMWK</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
#include "Generated.xcconfig"
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
#include "Generated.xcconfig"
|
||||||
|
|
@ -0,0 +1,620 @@
|
||||||
|
// !$*UTF8*$!
|
||||||
|
{
|
||||||
|
archiveVersion = 1;
|
||||||
|
classes = {
|
||||||
|
};
|
||||||
|
objectVersion = 54;
|
||||||
|
objects = {
|
||||||
|
|
||||||
|
/* Begin PBXBuildFile section */
|
||||||
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||||
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||||
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||||
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||||
|
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
|
||||||
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||||
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||||
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||||
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
|
||||||
|
remoteInfo = Runner;
|
||||||
|
};
|
||||||
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
|
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
|
||||||
|
isa = PBXCopyFilesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
dstPath = "";
|
||||||
|
dstSubfolderSpec = 10;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
name = "Embed Frameworks";
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXFileReference section */
|
||||||
|
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||||
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||||
|
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||||
|
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||||
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||||
|
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||||
|
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||||
|
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||||
|
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
|
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
|
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXGroup section */
|
||||||
|
331C8082294A63A400263BE5 /* RunnerTests */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
331C807B294A618700263BE5 /* RunnerTests.swift */,
|
||||||
|
);
|
||||||
|
path = RunnerTests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||||
|
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||||
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||||
|
9740EEB31CF90195004384FC /* Generated.xcconfig */,
|
||||||
|
);
|
||||||
|
name = Flutter;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146E51CF9000F007C117D = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
9740EEB11CF90186004384FC /* Flutter */,
|
||||||
|
97C146F01CF9000F007C117D /* Runner */,
|
||||||
|
97C146EF1CF9000F007C117D /* Products */,
|
||||||
|
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||||
|
);
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146EF1CF9000F007C117D /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||||
|
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146F01CF9000F007C117D /* Runner */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||||
|
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||||
|
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||||
|
97C147021CF9000F007C117D /* Info.plist */,
|
||||||
|
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
||||||
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||||
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||||
|
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
|
||||||
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||||
|
);
|
||||||
|
path = Runner;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXGroup section */
|
||||||
|
|
||||||
|
/* Begin PBXNativeTarget section */
|
||||||
|
331C8080294A63A400263BE5 /* RunnerTests */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||||
|
buildPhases = (
|
||||||
|
331C807D294A63A400263BE5 /* Sources */,
|
||||||
|
331C807F294A63A400263BE5 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
331C8086294A63A400263BE5 /* PBXTargetDependency */,
|
||||||
|
);
|
||||||
|
name = RunnerTests;
|
||||||
|
productName = RunnerTests;
|
||||||
|
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
|
||||||
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
|
};
|
||||||
|
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||||
|
buildPhases = (
|
||||||
|
9740EEB61CF901F6004384FC /* Run Script */,
|
||||||
|
97C146EA1CF9000F007C117D /* Sources */,
|
||||||
|
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||||
|
97C146EC1CF9000F007C117D /* Resources */,
|
||||||
|
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||||
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = Runner;
|
||||||
|
productName = Runner;
|
||||||
|
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||||
|
productType = "com.apple.product-type.application";
|
||||||
|
};
|
||||||
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
|
/* Begin PBXProject section */
|
||||||
|
97C146E61CF9000F007C117D /* Project object */ = {
|
||||||
|
isa = PBXProject;
|
||||||
|
attributes = {
|
||||||
|
BuildIndependentTargetsInParallel = YES;
|
||||||
|
LastUpgradeCheck = 1510;
|
||||||
|
ORGANIZATIONNAME = "";
|
||||||
|
TargetAttributes = {
|
||||||
|
331C8080294A63A400263BE5 = {
|
||||||
|
CreatedOnToolsVersion = 14.0;
|
||||||
|
TestTargetID = 97C146ED1CF9000F007C117D;
|
||||||
|
};
|
||||||
|
97C146ED1CF9000F007C117D = {
|
||||||
|
CreatedOnToolsVersion = 7.3.1;
|
||||||
|
LastSwiftMigration = 1100;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
|
||||||
|
compatibilityVersion = "Xcode 9.3";
|
||||||
|
developmentRegion = en;
|
||||||
|
hasScannedForEncodings = 0;
|
||||||
|
knownRegions = (
|
||||||
|
en,
|
||||||
|
Base,
|
||||||
|
);
|
||||||
|
mainGroup = 97C146E51CF9000F007C117D;
|
||||||
|
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||||
|
projectDirPath = "";
|
||||||
|
projectRoot = "";
|
||||||
|
targets = (
|
||||||
|
97C146ED1CF9000F007C117D /* Runner */,
|
||||||
|
331C8080294A63A400263BE5 /* RunnerTests */,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
/* End PBXProject section */
|
||||||
|
|
||||||
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
331C807F294A63A400263BE5 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
97C146EC1CF9000F007C117D /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
|
||||||
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||||
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||||
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||||
|
);
|
||||||
|
name = "Thin Binary";
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||||
|
};
|
||||||
|
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
name = "Run Script";
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||||
|
};
|
||||||
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
331C807D294A63A400263BE5 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
97C146EA1CF9000F007C117D /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||||
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||||
|
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXTargetDependency section */
|
||||||
|
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = 97C146ED1CF9000F007C117D /* Runner */;
|
||||||
|
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
|
/* Begin PBXVariantGroup section */
|
||||||
|
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
97C146FB1CF9000F007C117D /* Base */,
|
||||||
|
);
|
||||||
|
name = Main.storyboard;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
97C147001CF9000F007C117D /* Base */,
|
||||||
|
);
|
||||||
|
name = LaunchScreen.storyboard;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXVariantGroup section */
|
||||||
|
|
||||||
|
/* Begin XCBuildConfiguration section */
|
||||||
|
249021D3217E4FDB00AE95B9 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
VALIDATE_PRODUCT = YES;
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
249021D4217E4FDB00AE95B9 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = 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 */;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "self:">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>PreviewsEnabled</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1510"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
<Testables>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO"
|
||||||
|
parallelizable = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "331C8080294A63A400263BE5"
|
||||||
|
BuildableName = "RunnerTests.xctest"
|
||||||
|
BlueprintName = "RunnerTests"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
enableGPUValidationMode = "1"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Profile"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "group:Runner.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>PreviewsEnabled</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
@main
|
||||||
|
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
||||||
|
override func application(
|
||||||
|
_ application: UIApplication,
|
||||||
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
|
) -> Bool {
|
||||||
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
||||||
|
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-20x20@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-20x20@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-29x29@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-29x29@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-29x29@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-40x40@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-40x40@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "60x60",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-60x60@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "60x60",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-60x60@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-20x20@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-20x20@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-29x29@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-29x29@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-40x40@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-40x40@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "76x76",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-76x76@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "76x76",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-76x76@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "83.5x83.5",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-83.5x83.5@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "1024x1024",
|
||||||
|
"idiom" : "ios-marketing",
|
||||||
|
"filename" : "Icon-App-1024x1024@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 295 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 450 B |
|
After Width: | Height: | Size: 282 B |
|
After Width: | Height: | Size: 462 B |
|
After Width: | Height: | Size: 704 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 586 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 762 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "LaunchImage.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "LaunchImage@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "LaunchImage@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Launch Screen Assets
|
||||||
|
|
||||||
|
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
|
||||||
|
|
||||||
|
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--View Controller-->
|
||||||
|
<scene sceneID="EHf-IW-A2E">
|
||||||
|
<objects>
|
||||||
|
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||||
|
<layoutGuides>
|
||||||
|
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
|
||||||
|
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
|
||||||
|
</layoutGuides>
|
||||||
|
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
||||||
|
</imageView>
|
||||||
|
</subviews>
|
||||||
|
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
||||||
|
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="53" y="375"/>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
<resources>
|
||||||
|
<image name="LaunchImage" width="168" height="185"/>
|
||||||
|
</resources>
|
||||||
|
</document>
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--Flutter View Controller-->
|
||||||
|
<scene sceneID="tne-QT-ifu">
|
||||||
|
<objects>
|
||||||
|
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
|
||||||
|
<layoutGuides>
|
||||||
|
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
||||||
|
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||||
|
</layoutGuides>
|
||||||
|
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
</document>
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
|
<true/>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>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>
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
#import "GeneratedPluginRegistrant.h"
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SceneDelegate: FlutterSceneDelegate {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
class RunnerTests: XCTestCase {
|
||||||
|
|
||||||
|
func testExample() {
|
||||||
|
// If you add code to the Runner application, consider adding tests here.
|
||||||
|
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,147 @@
|
||||||
|
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(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,177 @@
|
||||||
|
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)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,288 @@
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,338 @@
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,339 @@
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
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'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,404 @@
|
||||||
|
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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,259 @@
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,477 @@
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,683 @@
|
||||||
|
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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,279 @@
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,477 @@
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,308 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,206 @@
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,172 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
flutter/ephemeral
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
# 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()
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
# 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}
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
//
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
//
|
||||||
|
// 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_
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
#
|
||||||
|
# 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)
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
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}")
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
#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);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
#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));
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
#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_
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
# Flutter-related
|
||||||
|
**/Flutter/ephemeral/
|
||||||
|
**/Pods/
|
||||||
|
|
||||||
|
# Xcode-related
|
||||||
|
**/dgph
|
||||||
|
**/xcuserdata/
|
||||||