Initial commit

This commit is contained in:
ayunda 2026-07-28 14:30:25 +07:00
commit 449d66abbb
162 changed files with 16479 additions and 0 deletions

5
.firebaserc Normal file
View File

@ -0,0 +1,5 @@
{
"projects": {
"default": "tape-ketan-70347"
}
}

45
.gitignore vendored Normal file
View File

@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

45
.metadata Normal file
View File

@ -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: "ac4e799d237041cf905519190471f657b657155a"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: ac4e799d237041cf905519190471f657b657155a
base_revision: ac4e799d237041cf905519190471f657b657155a
- platform: android
create_revision: ac4e799d237041cf905519190471f657b657155a
base_revision: ac4e799d237041cf905519190471f657b657155a
- platform: ios
create_revision: ac4e799d237041cf905519190471f657b657155a
base_revision: ac4e799d237041cf905519190471f657b657155a
- platform: linux
create_revision: ac4e799d237041cf905519190471f657b657155a
base_revision: ac4e799d237041cf905519190471f657b657155a
- platform: macos
create_revision: ac4e799d237041cf905519190471f657b657155a
base_revision: ac4e799d237041cf905519190471f657b657155a
- platform: web
create_revision: ac4e799d237041cf905519190471f657b657155a
base_revision: ac4e799d237041cf905519190471f657b657155a
- platform: windows
create_revision: ac4e799d237041cf905519190471f657b657155a
base_revision: ac4e799d237041cf905519190471f657b657155a
# 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'

25
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,25 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "ketan",
"request": "launch",
"type": "dart"
},
{
"name": "ketan (profile mode)",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "ketan (release mode)",
"request": "launch",
"type": "dart",
"flutterMode": "release"
}
]
}

16
README.md Normal file
View File

@ -0,0 +1,16 @@
# ketan
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

28
analysis_options.yaml Normal file
View File

@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

14
android/.gitignore vendored Normal file
View File

@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

View File

@ -0,0 +1,58 @@
plugins {
id("com.android.application")
id("com.google.gms.google-services")
id("kotlin-android")
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.example.ketan"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
defaultConfig {
applicationId = "com.example.ketan"
// Paksa ke 21 untuk kestabilan Firebase
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
// Penting agar tidak crash karena library banyak
multiDexEnabled = true
}
buildTypes {
release {
signingConfig = signingConfigs.getByName("debug")
// Perbaikan penulisan untuk Kotlin DSL (.kts)
isMinifyEnabled = false
isShrinkResources = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
flutter {
source = "../.."
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
}

View File

@ -0,0 +1,30 @@
{
"project_info": {
"project_number": "968686817464",
"firebase_url": "https://tape-ketan-70347-default-rtdb.firebaseio.com",
"project_id": "tape-ketan-70347",
"storage_bucket": "tape-ketan-70347.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:968686817464:android:e30fbf296143da3f3a644d",
"android_client_info": {
"package_name": "com.example.ketan"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyDc01KmGZYUWY6A27yRSlSWlzrirRgXKUM"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@ -0,0 +1,57 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<application
android:label="GoTape"
android:name="${applicationName}"
android:icon="@mipmap/launcher_icon">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="FLUTTER_NOTIFICATION_CLICK" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="channel_id"/>
<meta-data
android:name="flutterEmbedding"
android:value="2"/>
</application>
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

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

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

24
android/build.gradle.kts Normal file
View File

@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View File

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

View File

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

View File

@ -0,0 +1,29 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.9.1" apply false
// START: FlutterFire Configuration
id("com.google.gms.google-services") version("4.3.15") apply false
// END: FlutterFire Configuration
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
}
include(":app")

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

BIN
assets/images/tape.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

3
devtools_options.yaml Normal file
View File

@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

37
firebase.json Normal file
View File

@ -0,0 +1,37 @@
{
"flutter": {
"platforms": {
"android": {
"default": {
"projectId": "tape-ketan-70347",
"appId": "1:968686817464:android:e30fbf296143da3f3a644d",
"fileOutput": "android/app/google-services.json"
}
},
"dart": {
"lib/firebase_options.dart": {
"projectId": "tape-ketan-70347",
"configurations": {
"android": "1:968686817464:android:e30fbf296143da3f3a644d",
"web": "1:968686817464:web:9ddf9e7a980280333a644d",
"windows": "1:968686817464:web:cb19922df0732e103a644d"
}
}
}
}
},
"functions": [
{
"source": "functions",
"codebase": "default",
"disallowLegacyRuntimeConfig": true,
"ignore": [
"node_modules",
".git",
"firebase-debug.log",
"firebase-debug.*.log",
"*.local"
]
}
]
}

2
functions/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules/
*.local

113
functions/index.js Normal file
View File

@ -0,0 +1,113 @@
const { setGlobalOptions } = require("firebase-functions");
const { onValueCreated } = require("firebase-functions/v2/database");
const admin = require("firebase-admin");
admin.initializeApp();
setGlobalOptions({ maxInstances: 10 });
/// 🔔 1. NOTIFIKASI TRIGGER (MONITORING & SELESAI)
exports.notifHandler = onValueCreated(
"/notifikasi/{id}",
async (event) => {
try {
const data = event.data.val();
if (!data) return null;
const tipe = data.tipe || "";
const jenisTape = data.jenisTape || "Ketan";
const waktu = data.waktu || "-";
const suhu = data.suhu || "-";
const alkohol = data.alkohol || "-";
const status = data.status || "";
const jamTotal = data.jam_total || data.waktu || "-";
/// AMBIL TOKEN
const tokenSnap = await admin.database().ref("fcm_token").once("value");
const token = tokenSnap.val();
if (!token) {
console.log("❌ Token tidak ada");
return null;
}
let message;
if (tipe === "monitoring") {
const delta = data.delta_alkohol !== undefined ? data.delta_alkohol : "-";
message = {
notification: {
title: `📊 Monitoring Tape ${jenisTape}`,
body: `Status: ${status}\nSuhu: ${suhu}°C\nAlkohol: ${alkohol}%\nDelta 3 Jam: ${delta}%`,
},
token: token,
};
} else if (tipe === "selesai") {
message = {
notification: {
title: `🍌 Fermentasi Tape ${jenisTape} Selesai!`,
body: `Waktu: ${waktu}\nTotal Waktu: ${jamTotal} Jam\nSuhu: ${suhu}°C\nAlkohol: ${alkohol}%\nStatus: ${status}`,
},
token: token,
};
} else {
console.log(`⚠️ Tipe notifikasi tidak dikenal: ${tipe}`);
return null;
}
await admin.messaging().send(message);
console.log(`✅ Notif ${tipe} terkirim`);
return null;
} catch (error) {
console.error("❌ Error sending notification:", error);
return null;
}
}
);
/// 🔔 2. PERINGATAN TRIGGER (HANYA TERLALU MATANG)
exports.peringatanHandler = onValueCreated(
"/peringatan/{id}",
async (event) => {
try {
const data = event.data.val();
if (!data) return null;
const status = data.status || "";
const jenisTape = data.jenisTape || "Ketan";
const suhu = data.suhu || "-";
const alkohol = data.alkohol || "-";
const waktu = data.waktu || "-";
// HANYA kirim jika status "Terlalu Matang"
if (status !== "Terlalu Matang") {
console.log(`⚠️ Mengabaikan peringatan dengan status: ${status}`);
return null;
}
/// AMBIL TOKEN
const tokenSnap = await admin.database().ref("fcm_token").once("value");
const token = tokenSnap.val();
if (!token) {
console.log("❌ Token tidak ada");
return null;
}
const message = {
notification: {
title: `⚠️ Peringatan: Tape ${jenisTape} Terlalu Matang!`,
body: `Tape sudah terlalu matang.\nWaktu: ${waktu}\nSuhu: ${suhu}°C\nAlkohol: ${alkohol}%`,
},
token: token,
};
await admin.messaging().send(message);
console.log("✅ Notif peringatan Terlalu Matang terkirim");
return null;
} catch (error) {
console.error("❌ Error sending warning notification:", error);
return null;
}
}
);

7412
functions/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

23
functions/package.json Normal file
View File

@ -0,0 +1,23 @@
{
"name": "functions",
"description": "Cloud Functions for Firebase",
"scripts": {
"serve": "firebase emulators:start --only functions",
"shell": "firebase functions:shell",
"start": "npm run shell",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log"
},
"engines": {
"node": "24"
},
"main": "index.js",
"dependencies": {
"firebase-admin": "^13.6.0",
"firebase-functions": "^7.0.0"
},
"devDependencies": {
"firebase-functions-test": "^3.4.1"
},
"private": true
}

34
ios/.gitignore vendored Normal file
View File

@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict>
</plist>

View File

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

View File

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

View File

@ -0,0 +1,616 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 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.ketan;
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.ketan.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.ketan.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.ketan.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 = AppIcon;
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 = AppIcon;
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.ketan;
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.ketan;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

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

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@ -0,0 +1,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>

View File

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

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

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

View File

@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 967 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View File

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

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

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>GoTape</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>GoTape</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

View File

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

View File

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

80
lib/firebase_options.dart Normal file
View File

@ -0,0 +1,80 @@
// File generated by FlutterFire CLI.
// ignore_for_file: type=lint
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, kIsWeb, TargetPlatform;
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options.dart';
/// // ...
/// await Firebase.initializeApp(
/// options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
return web;
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for ios - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.macOS:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for macos - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.windows:
return windows;
case TargetPlatform.linux:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyDc01KmGZYUWY6A27yRSlSWlzrirRgXKUM',
appId: '1:968686817464:android:e30fbf296143da3f3a644d',
messagingSenderId: '968686817464',
projectId: 'tape-ketan-70347',
databaseURL: 'https://tape-ketan-70347-default-rtdb.firebaseio.com',
storageBucket: 'tape-ketan-70347.firebasestorage.app',
);
static const FirebaseOptions web = FirebaseOptions(
apiKey: 'AIzaSyAUX0D9v6SNiFOsFppFjG7pFt0NzrqE0tg',
appId: '1:968686817464:web:9ddf9e7a980280333a644d',
messagingSenderId: '968686817464',
projectId: 'tape-ketan-70347',
authDomain: 'tape-ketan-70347.firebaseapp.com',
databaseURL: 'https://tape-ketan-70347-default-rtdb.firebaseio.com',
storageBucket: 'tape-ketan-70347.firebasestorage.app',
measurementId: 'G-9C51PKW0K6',
);
static const FirebaseOptions windows = FirebaseOptions(
apiKey: 'AIzaSyAUX0D9v6SNiFOsFppFjG7pFt0NzrqE0tg',
appId: '1:968686817464:web:cb19922df0732e103a644d',
messagingSenderId: '968686817464',
projectId: 'tape-ketan-70347',
authDomain: 'tape-ketan-70347.firebaseapp.com',
databaseURL: 'https://tape-ketan-70347-default-rtdb.firebaseio.com',
storageBucket: 'tape-ketan-70347.firebasestorage.app',
measurementId: 'G-KWH1JDHCV3',
);
}

263
lib/main.dart Normal file
View File

@ -0,0 +1,263 @@
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:in_app_notification/in_app_notification.dart';
import 'firebase_options.dart';
import 'service/notification_service.dart';
// IMPORT PAGE
import 'screen/welcome.dart';
import 'screen/login.dart';
import 'screen/register.dart';
import 'screen/dashboard.dart';
import 'screen/pesan.dart';
import 'screen/history.dart';
import 'screen/akun.dart';
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
/// 🔔 HANDLER NOTIFIKASI BACKGROUND
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
// Inisialisasi Firebase di background
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
if (message.notification != null) {
String body = message.notification!.body ?? "";
body = body
.replaceAll(RegExp(r'Proses Optimal', caseSensitive: false), 'Belum Matang')
.replaceAll(RegExp(r'Proses Awal', caseSensitive: false), 'Belum Matang')
.replaceAll(RegExp(r'Menjelang Matang', caseSensitive: false), 'Belum Matang');
await NotificationService.showNotification(
message.notification!.title ?? "Notifikasi",
body,
);
}
}
Future<void> main() async {
// 1. Pastikan binding sudah siap
WidgetsFlutterBinding.ensureInitialized();
// 2. Inisialisasi Firebase (Gunakan Try-Catch agar aman)
try {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
} catch (e) {
debugPrint("Firebase init error: $e");
}
// 3. Register Background Handler
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
// 4. Init Local Notification
await NotificationService.init();
// 5. Jalankan Aplikasi Dulu (SANGAT PENTING agar tidak layar hitam)
runApp(const GoTapeApp());
// 6. Jalankan konfigurasi tambahan setelah runApp agar tidak menghambat startup
_initNotification();
}
/// Fungsi tambahan untuk setup notifikasi tanpa menghambat UI
void _initNotification() async {
try {
FirebaseMessaging messaging = FirebaseMessaging.instance;
// Minta Izin
await messaging.requestPermission();
// Ambil & Simpan Token
String? token = await messaging.getToken();
if (token != null) {
// Simpan ke database tanpa await agar tidak nunggu lama
FirebaseDatabase.instance.ref("fcm_token").set(token);
debugPrint("🔥 FCM TOKEN: $token");
}
// Listener Foreground
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
if (message.notification != null) {
String title = message.notification!.title ?? "Notifikasi";
String body = message.notification!.body ?? "";
body = body
.replaceAll(RegExp(r'Proses Optimal', caseSensitive: false), 'Belum Matang')
.replaceAll(RegExp(r'Proses Awal', caseSensitive: false), 'Belum Matang')
.replaceAll(RegExp(r'Menjelang Matang', caseSensitive: false), 'Belum Matang');
NotificationService.showNotification(title, body);
// Tampilkan In-App Notification (Seperti WhatsApp)
final context = navigatorKey.currentContext;
if (context != null) {
InAppNotification.show(
child: _buildWhatsAppStyleNotification(title, body),
context: context,
duration: const Duration(seconds: 4),
curve: Curves.easeOutCubic,
);
}
}
});
// Listener saat notif diklik
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
debugPrint("🔔 User membuka notifikasi");
});
} catch (e) {
debugPrint("Notification Setup Error: $e");
}
}
Widget _buildWhatsAppStyleNotification(String title, String body) {
bool isAlert = title.toLowerCase().contains('berbahaya') || title.toLowerCase().contains('matang');
return SafeArea(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: isAlert ? Colors.redAccent : const Color(0xFF5B35D5),
shape: BoxShape.circle,
),
child: Icon(
isAlert ? Icons.warning_rounded : Icons.notifications_active,
color: Colors.white,
size: 24,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
title,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: isAlert ? Colors.redAccent : Colors.black87,
),
),
const SizedBox(height: 4),
Text(
body,
style: const TextStyle(
color: Colors.black54,
fontSize: 14,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
),
);
}
class GoTapeApp extends StatelessWidget {
const GoTapeApp({super.key});
@override
Widget build(BuildContext context) {
return InAppNotification(
child: MaterialApp(
navigatorKey: navigatorKey,
debugShowCheckedModeBanner: false,
title: 'GoTape',
theme: ThemeData(
useMaterial3: true,
colorSchemeSeed: const Color(0xFF4DB6AC),
scaffoldBackgroundColor: const Color(0xFFF2F6F6),
appBarTheme: const AppBarTheme(
centerTitle: true,
elevation: 0,
backgroundColor: Colors.transparent,
),
),
// Halaman Utama menggunakan AuthGate
home: const AuthGate(),
// Daftar Route
routes: {
'/welcome': (_) => const WelcomeScreen(),
'/login': (_) => const LoginPage(),
'/register': (_) => const RegisterScreen(),
'/dashboard': (_) => const DashboardPage(),
'/pesan': (_) => const NotificationPage(),
'/riwayat': (_) => const HistoryPage(),
'/akun': (_) => const AkunScreen(),
},
),
);
}
}
class AuthGate extends StatelessWidget {
const AuthGate({super.key});
@override
Widget build(BuildContext context) {
return StreamBuilder<User?>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
// Jika sedang mengecek status login
if (snapshot.connectionState == ConnectionState.waiting) {
return const Scaffold(
body: Center(
child: CircularProgressIndicator(color: Color(0xFF4DB6AC)),
),
);
}
// Jika terjadi error pada Firebase Auth
if (snapshot.hasError) {
return const Scaffold(
body: Center(
child: Text(
'Terjadi kesalahan autentikasi',
style: TextStyle(color: Colors.red),
),
),
);
}
// Jika user sudah login, lempar ke Dashboard
if (snapshot.hasData && snapshot.data != null) {
return const DashboardPage();
}
// Jika belum login, tampilkan Welcome Screen
return const WelcomeScreen();
},
);
}
}

386
lib/screen/akun.dart Normal file
View File

@ -0,0 +1,386 @@
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_database/firebase_database.dart';
import 'dashboard.dart';
import 'history.dart';
import 'pesan.dart';
import '../widgets/fade_in_up.dart';
class AkunScreen extends StatefulWidget {
const AkunScreen({super.key});
@override
State<AkunScreen> createState() => _AkunScreenState();
}
class _AkunScreenState extends State<AkunScreen> {
final user = FirebaseAuth.instance.currentUser;
int selectedIndex = 3;
String get username {
if (user?.displayName != null && user!.displayName!.isNotEmpty) {
return user!.displayName!;
}
if (user?.email != null) return user!.email!.split("@")[0];
return "User";
}
String get email => user?.email ?? "email";
Future<void> _resetFermentasi() async {
await FirebaseDatabase.instance.ref("control").update({
"reset_request": true,
"fermentasi_aktif": false,
"waktu_mulai_ui": 0,
});
await FirebaseDatabase.instance.ref("fermentasi").update({
"waktu": 0,
"status": "Belum Mulai",
});
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text("Reset berhasil dikirim")));
}
Future<void> _logout() async {
await FirebaseAuth.instance.signOut();
if (mounted) Navigator.pushReplacementNamed(context, '/login');
}
@override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: _buildBottomNavbar(),
body: Stack(
children: [
Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
stops: [0.0, 0.4, 0.7, 1.0],
colors: [
Color(0xFFE8F5E9),
Color(0xFFFFFDF0),
Color(0xFFF1F8E9),
Color(0xFFE8F5E9),
],
),
),
),
SafeArea(
bottom: false,
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 24, 20, 110),
child: Column(
children: [
/// HEADER
FadeInUp(
delay: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: const Icon(Icons.arrow_back_ios_new, size: 20),
onPressed: () => Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const DashboardPage()),
),
),
const Text(
"My Account",
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
GestureDetector(
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const NotificationPage(),
),
),
child: Container(
width: 42,
height: 42,
decoration: BoxDecoration(
color: const Color(0xFF2E7D32),
borderRadius: BorderRadius.circular(14),
),
child: const Icon(
Icons.notifications_none_rounded,
color: Colors.white,
size: 22,
),
),
),
],
),
),
const SizedBox(height: 25),
/// USERNAME CARD
FadeInUp(
delay: 150,
child: _buildTopCard(
icon: Icons.work_outline,
text: username,
color: Colors.green,
),
),
const SizedBox(height: 20),
/// PROJECT NAME
FadeInUp(delay: 300, child: _buildProjectCard()),
const SizedBox(height: 20),
/// PERSONAL INFO
FadeInUp(delay: 450, child: _buildInfoCard()),
const SizedBox(height: 30),
/// LOGOUT
FadeInUp(
delay: 600,
child: SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: _logout,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFF9A825),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
child: const Text(
"LOGOUT",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
),
),
const SizedBox(height: 30),
],
),
),
),
],
),
);
}
/// ================= WIDGET =================
Widget _buildTopCard({
required IconData icon,
required String text,
required Color color,
}) {
return Container(
padding: const EdgeInsets.all(16),
decoration: _cardStyle(),
child: Row(
children: [
_iconBox(icon, color),
const SizedBox(width: 12),
Text(text, style: const TextStyle(fontSize: 14)),
],
),
);
}
Widget _buildProjectCard() {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(18),
decoration: _cardStyle(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
"Project Name",
style: TextStyle(fontSize: 12, color: Colors.black54),
),
SizedBox(height: 6),
Text(
"Fermentasi tape ketan dan tape singkong",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
],
),
);
}
Widget _buildInfoCard() {
return Container(
padding: const EdgeInsets.all(18),
decoration: _cardStyle(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"PERSONAL INFO",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.black45,
),
),
const SizedBox(height: 15),
_infoRow(Icons.person, Colors.blue, "Full Name", username),
const SizedBox(height: 12),
_infoRow(Icons.mail, Colors.blueGrey, "Email", email),
const SizedBox(height: 12),
GestureDetector(
onTap: _resetFermentasi,
behavior: HitTestBehavior.opaque,
child: _infoRow(
Icons.refresh,
Colors.lightBlue,
"Fermentasi",
"reset waktu baru",
),
),
],
),
);
}
Widget _infoRow(IconData icon, Color color, String title, String value) {
return Row(
children: [
_iconBox(icon, color),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(fontSize: 12, color: Colors.black54),
),
Text(
value,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
),
],
),
],
);
}
Widget _iconBox(IconData icon, Color color) {
return Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: color.withOpacity(0.2),
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, color: color),
);
}
BoxDecoration _cardStyle() {
return BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
);
}
Widget _buildBottomNavbar() {
return Container(
decoration: const BoxDecoration(
color: Color(0xFFE8F5E9),
borderRadius: BorderRadius.vertical(top: Radius.circular(30)),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 10,
offset: Offset(0, -2),
),
],
),
child: ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
child: BottomNavigationBar(
currentIndex: 2,
onTap: (index) {
if (index == 0) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const DashboardPage()),
);
} else if (index == 1) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const HistoryPage()),
);
}
},
showSelectedLabels: false,
showUnselectedLabels: false,
backgroundColor: Colors.transparent,
elevation: 0,
type: BottomNavigationBarType.fixed,
items: [
BottomNavigationBarItem(
icon: _navIcon(Icons.home_filled, 0),
label: 'Home',
),
BottomNavigationBarItem(
icon: _navIcon(Icons.history, 1),
label: 'History',
),
BottomNavigationBarItem(
icon: _navIcon(Icons.people_alt_rounded, 2),
label: 'Profile',
),
],
),
),
);
}
Widget _navIcon(IconData icon, int index) {
bool isSelected = index == 2;
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
gradient: isSelected
? const LinearGradient(
colors: [Color(0xFF2E7D32), Color(0xFF4CAF50)],
)
: null,
),
child: Icon(
icon,
color: isSelected ? Colors.white : const Color(0xFF81C784),
size: 24,
),
);
}
}

926
lib/screen/dashboard.dart Normal file
View File

@ -0,0 +1,926 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_database/firebase_database.dart';
import 'history.dart';
import 'pesan.dart';
import 'akun.dart';
import '../widgets/fade_in_up.dart';
class DashboardPage extends StatefulWidget {
const DashboardPage({super.key});
@override
State<DashboardPage> createState() => _DashboardPageState();
}
class _DashboardPageState extends State<DashboardPage> {
final user = FirebaseAuth.instance.currentUser;
final sensorRef = FirebaseDatabase.instance.ref("fermentasi");
final controlRef = FirebaseDatabase.instance.ref("control");
double suhu = 0;
double alkohol = 0;
double waktu = 0;
String status = "Belum Mulai";
bool autoMode = true;
bool ptcOn = false;
bool kipas1On = false;
bool kipas2On = false;
bool fermentasiAktif = false;
String selectedTape = "";
Timer? _timer;
int detikBerjalan = 0;
int waktuMulaiUi = 0;
double _swipeValue = 0.0;
@override
void initState() {
super.initState();
listenSensor();
listenControl();
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (fermentasiAktif && mounted) {
setState(() {
detikBerjalan++;
});
}
});
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
void updateStatusKematangan() {
if (autoMode) {
if (!fermentasiAktif) {
// Cek threshold bahkan saat belum mulai di Mode Otomatis
if (selectedTape == "Ketan") {
if (alkohol >= 0.32) {
status = "Terlalu Matang";
return;
} else if (alkohol >= 0.25) {
status = "Matang";
return;
}
} else if (selectedTape == "Singkong") {
if (alkohol > 5.8) {
status = "Terlalu Matang";
return;
} else if (alkohol >= 3.5) {
status = "Matang";
return;
}
}
status = "Belum Mulai";
return;
}
// Di mode otomatis, gunakan treshold manual sebagai pendeteksi langsung di UI
if (selectedTape == "Ketan") {
if (alkohol >= 0.32) {
status = "Terlalu Matang";
return;
} else if (alkohol >= 0.25) {
status = "Matang";
return;
}
} else if (selectedTape == "Singkong") {
if (alkohol > 5.8) {
status = "Terlalu Matang";
return;
} else if (alkohol >= 3.5) {
status = "Matang";
return;
}
}
// In autoMode, the status is determined by the ESP32 dynamic peak-detection state machine
// and uploaded to Firebase (/fermentasi/status), which is handled in listenSensor().
// If no status has been loaded from Firebase yet, we set a sensible default.
if (status == "Belum Mulai") {
status = "Belum Matang";
}
} else {
// Mode Manual (matches the ESP32 manual thresholds)
if (selectedTape == "Ketan") {
if (alkohol >= 0.32) {
status = "Terlalu Matang";
return;
} else if (alkohol >= 0.25) {
status = "Matang";
return;
} else {
status = "Belum Matang";
}
} else if (selectedTape == "Singkong") {
if (alkohol > 5.8) {
status = "Terlalu Matang";
return;
} else if (alkohol >= 3.5) {
status = "Matang";
return;
} else {
status = "Belum Matang";
}
}
if (!fermentasiAktif) {
status = "Belum Mulai";
}
}
}
void listenSensor() {
sensorRef.onValue.listen((event) {
final data = event.snapshot.value as Map?;
if (data != null) {
if (mounted) {
setState(() {
suhu = (data["suhu"] as num?)?.toDouble() ?? 0;
alkohol = (data["alkohol"] as num?)?.toDouble() ?? 0;
if (data["waktu"] != null) {
waktu = (data["waktu"] as num?)?.toDouble() ?? 0;
int espSeconds = (waktu * 3600).toInt();
if (fermentasiAktif) {
if ((detikBerjalan - espSeconds).abs() > 10) {
detikBerjalan = espSeconds;
}
} else {
detikBerjalan = espSeconds;
}
}
if (data["status"] != null) {
String rawStatus = data["status"].toString();
String lowerStatus = rawStatus.toLowerCase();
// Standardize intermediate phases to "Belum Matang" to match notifications & history page mapping
if (lowerStatus.contains("optimal") || lowerStatus.contains("awal") || lowerStatus.contains("menjelang matang")) {
status = "Belum Matang";
} else {
status = rawStatus;
}
} else {
updateStatusKematangan();
}
});
}
}
});
}
void listenControl() {
controlRef.onValue.listen((event) {
final data = event.snapshot.value as Map?;
if (data != null) {
if (mounted) {
setState(() {
autoMode = data["autoMode"] ?? true;
ptcOn = data["ptc"] ?? false;
kipas1On = data["kipas1"] ?? false; // kipas1On (pemanas) baca dari "kipas1"
kipas2On = data["kipas2"] ?? false; // kipas2On (kipas pendingin) baca dari "kipas2"
fermentasiAktif = data["fermentasi_aktif"] ?? false;
waktuMulaiUi = data["waktu_mulai_ui"] ?? 0;
if (fermentasiAktif && waktuMulaiUi > 0) {
int now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
int diff = now - waktuMulaiUi;
if (diff >= 0 && (detikBerjalan - diff).abs() > 2) {
detikBerjalan = diff;
}
}
if (data["jenisTape"] != null) {
selectedTape = data["jenisTape"];
}
bool resetReq = data["reset_request"] ?? false;
if (resetReq) {
status = "Belum Mulai";
detikBerjalan = 0;
waktu = 0;
}
// Only update status locally if in manual mode or if fermentation is stopped
if (!autoMode || !fermentasiAktif) {
updateStatusKematangan();
}
});
}
}
});
}
String formatWaktu(double jam) {
int total = (jam * 3600).toInt();
int h = total ~/ 3600;
int m = (total % 3600) ~/ 60;
int s = total % 60;
return "${h.toString().padLeft(2, '0')}:${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}";
}
String formatWaktuDetik(int totalSeconds) {
int h = totalSeconds ~/ 3600;
int m = (totalSeconds % 3600) ~/ 60;
int s = totalSeconds % 60;
return "${h.toString().padLeft(2, '0')}:${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}";
}
String getUsername() {
if (user?.displayName != null && user!.displayName!.isNotEmpty)
return user!.displayName!;
if (user?.email != null) return user!.email!.split("@")[0];
return "User";
}
@override
Widget build(BuildContext context) {
return Scaffold(
extendBody: true,
body: Stack(
children: [
Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
stops: [0.0, 0.4, 0.7, 1.0],
colors: [
Color(0xFFE8F5E9),
Color(0xFFFFFDF0),
Color(0xFFF1F8E9),
Color(0xFFE8F5E9),
],
),
),
),
SafeArea(
bottom: false,
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 24, 20, 110),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FadeInUp(delay: 0, child: _header()),
const SizedBox(height: 28),
FadeInUp(delay: 150, child: _startFermentation()),
const SizedBox(height: 28),
FadeInUp(delay: 300, child: _actuatorControl()),
const SizedBox(height: 20),
],
),
),
),
],
),
bottomNavigationBar: _buildBottomNavbar(),
);
}
Widget _buildBottomNavbar() {
return Container(
decoration: const BoxDecoration(
color: Color(0xFFE8F5E9),
borderRadius: BorderRadius.vertical(top: Radius.circular(30)),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 10,
offset: Offset(0, -2),
),
],
),
child: ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
child: BottomNavigationBar(
currentIndex: 0,
onTap: (index) {
if (index == 1) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const HistoryPage()),
);
} else if (index == 2) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const AkunScreen()),
);
}
},
showSelectedLabels: false,
showUnselectedLabels: false,
backgroundColor: Colors.transparent,
elevation: 0,
type: BottomNavigationBarType.fixed,
items: [
BottomNavigationBarItem(
icon: _navIcon(Icons.home_filled, 0),
label: 'Home',
),
BottomNavigationBarItem(
icon: _navIcon(Icons.history, 1),
label: 'History',
),
BottomNavigationBarItem(
icon: _navIcon(Icons.people_alt_rounded, 2),
label: 'Profile',
),
],
),
),
);
}
Widget _navIcon(IconData icon, int index) {
bool isSelected = index == 0;
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
gradient: isSelected
? const LinearGradient(
colors: [Color(0xFF2E7D32), Color(0xFF4CAF50)],
)
: null,
),
child: Icon(
icon,
color: isSelected ? Colors.white : const Color(0xFF81C784),
size: 24,
),
);
}
Widget _header() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Hello!",
style: TextStyle(color: Colors.black54, fontSize: 13),
),
Text(
getUsername(),
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
],
),
const Spacer(),
GestureDetector(
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const NotificationPage(),
),
),
child: Container(
width: 42,
height: 42,
decoration: BoxDecoration(
color: const Color(0xFF2E7D32),
borderRadius: BorderRadius.circular(14),
),
child: const Icon(
Icons.notifications_none_rounded,
color: Colors.white,
size: 22,
),
),
),
],
),
const SizedBox(height: 20),
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 22),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF2E7D32), Color(0xFF4CAF50)],
),
borderRadius: BorderRadius.circular(28),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Fermentasi kamu hari ini!",
style: TextStyle(
color: Colors.white70,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 6),
Text(
status,
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
_chip("Alkohol ${selectedTape == 'Singkong' ? alkohol.toStringAsFixed(1) : alkohol.toStringAsFixed(2)}%"),
const SizedBox(height: 8),
_chip("Suhu ${suhu.toStringAsFixed(1)}°C"),
const SizedBox(height: 8),
_chip(
fermentasiAktif
? formatWaktuDetik(detikBerjalan)
: (status != "Belum Mulai" ? formatWaktu(waktu) : "00:00:00"),
mono: true,
),
],
),
],
),
),
],
);
}
Widget _chip(String text, {bool mono = false}) {
return Container(
width: 130,
padding: const EdgeInsets.symmetric(vertical: 9),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
),
child: Center(
child: Text(
text,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12.5,
fontFamily: mono ? 'monospace' : null,
),
),
),
);
}
Widget _startFermentation() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
"Start Fermentation",
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
if (fermentasiAktif)
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFF2E7D32).withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Row(
children: [
Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: Color(0xFF2E7D32),
shape: BoxShape.circle,
),
),
const SizedBox(width: 6),
const Text(
"Running...",
style: TextStyle(
color: Color(0xFF2E7D32),
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
],
),
),
],
),
const SizedBox(height: 14),
_segmentedModeSelector(),
const SizedBox(height: 16),
_simpleStartButton(),
],
);
}
Widget _segmentedModeSelector() {
bool isKetan = selectedTape == "Ketan";
bool isSingkong = selectedTape == "Singkong";
bool hasSelection = selectedTape.isNotEmpty;
return Container(
height: 52,
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: const Color(0xFFE8F5E9),
borderRadius: BorderRadius.circular(26),
),
child: LayoutBuilder(
builder: (context, constraints) {
double totalWidth = constraints.maxWidth;
if (totalWidth.isInfinite || totalWidth <= 0) {
totalWidth = MediaQuery.of(context).size.width - 40;
}
double width = totalWidth / 2;
return Stack(
children: [
AnimatedAlign(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
alignment: isKetan ? Alignment.centerLeft : (isSingkong ? Alignment.centerRight : Alignment.center),
child: AnimatedOpacity(
duration: const Duration(milliseconds: 200),
opacity: hasSelection ? 1.0 : 0.0,
child: Container(
width: width,
height: double.infinity,
decoration: BoxDecoration(
color: isKetan ? const Color(0xFF2E7D32) : const Color(0xFFF9A825),
borderRadius: BorderRadius.circular(22),
boxShadow: [
BoxShadow(
color: (isKetan ? const Color(0xFF2E7D32) : const Color(0xFFF9A825))
.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
),
),
),
Row(
children: [
Expanded(
child: GestureDetector(
onTap: () {
if (!fermentasiAktif) {
setState(() => selectedTape = "Ketan");
controlRef.update({"jenisTape": "Ketan"});
}
},
behavior: HitTestBehavior.opaque,
child: Center(
child: Text(
"Tape Ketan",
style: TextStyle(
fontWeight: FontWeight.bold,
color: isKetan ? Colors.white : Colors.black54,
),
),
),
),
),
Expanded(
child: GestureDetector(
onTap: () {
if (!fermentasiAktif) {
setState(() => selectedTape = "Singkong");
controlRef.update({"jenisTape": "Singkong"});
}
},
behavior: HitTestBehavior.opaque,
child: Center(
child: Text(
"Tape Singkong",
style: TextStyle(
fontWeight: FontWeight.bold,
color: isSingkong ? Colors.white : Colors.black54,
),
),
),
),
),
],
),
],
);
},
),
);
}
Widget _simpleStartButton() {
Color btnColor = fermentasiAktif ? Colors.redAccent : const Color(0xFF2E7D32);
return Container(
width: double.infinity,
height: 54,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: btnColor.withOpacity(0.25),
blurRadius: 15,
offset: const Offset(0, 6),
),
],
),
child: ElevatedButton.icon(
onPressed: _toggleFermentation,
style: ElevatedButton.styleFrom(
backgroundColor: btnColor,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
elevation: 0,
),
icon: Icon(
fermentasiAktif ? Icons.stop_circle_rounded : Icons.play_circle_filled_rounded,
size: 24,
),
label: Text(
fermentasiAktif ? "STOP FERMENTASI" : "MULAI FERMENTASI",
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
),
),
);
}
void _toggleFermentation() {
if (!fermentasiAktif && selectedTape.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Pilih jenis tape terlebih dahulu!"),
backgroundColor: Colors.redAccent,
),
);
return;
}
int nowUnix = DateTime.now().millisecondsSinceEpoch ~/ 1000;
Map<String, dynamic> updates = {
"fermentasi_aktif": !fermentasiAktif,
"jenisTape": selectedTape,
};
if (!fermentasiAktif) {
updates["reset_request"] = true;
updates["waktu_mulai_ui"] = nowUnix;
}
controlRef.update(updates);
if (!fermentasiAktif) {
setState(() {
detikBerjalan = 0;
waktuMulaiUi = nowUnix;
});
}
}
Widget _actuatorControl() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Actuator Control",
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Color(0xFF2D3142),
),
),
const SizedBox(height: 16),
Column(
children: [
Row(
children: [
Expanded(
child: _buildActuatorCard(
title: "Kipas Pemanas",
subtitle: "Heater Fan",
isActive: kipas1On,
icon: Icons.toys_outlined,
activeGradient: const [Color(0xFFFFB74D), Color(0xFFF57C00)],
onTap: () {
if (autoMode) {
_showAutoModeWarning();
} else {
controlRef.update({"kipas1": !kipas1On});
}
},
isLocked: autoMode,
),
),
const SizedBox(width: 14),
Expanded(
child: _buildActuatorCard(
title: "PTC Heater",
subtitle: "Pemanas PTC",
isActive: ptcOn,
icon: Icons.local_fire_department_outlined,
activeGradient: const [Color(0xFFFF8A65), Color(0xFFD84315)],
onTap: () {
if (autoMode) {
_showAutoModeWarning();
} else {
controlRef.update({"ptc": !ptcOn});
}
},
isLocked: autoMode,
),
),
],
),
const SizedBox(height: 14),
Row(
children: [
Expanded(
child: _buildActuatorCard(
title: "Kipas Pendingin",
subtitle: "Cooler Fan",
isActive: kipas2On,
icon: Icons.ac_unit_rounded,
activeGradient: const [Color(0xFF0288D1), Color(0xFF03A9F4)],
onTap: () {
if (autoMode) {
_showAutoModeWarning();
} else {
controlRef.update({"kipas2": !kipas2On});
}
},
isLocked: autoMode,
),
),
const SizedBox(width: 14),
Expanded(
child: _buildActuatorCard(
title: "Mode Otomatis",
subtitle: "Smart Auto",
isActive: autoMode,
icon: Icons.settings_suggest_rounded,
activeGradient: const [Color(0xFF2E7D32), Color(0xFF4CAF50)],
onTap: () {
controlRef.update({"autoMode": !autoMode});
},
isLocked: !autoMode,
),
),
],
),
],
),
],
);
}
void _showAutoModeWarning() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Nonaktifkan Mode Otomatis terlebih dahulu!"),
backgroundColor: Colors.redAccent,
duration: Duration(seconds: 2),
),
);
}
Widget _buildActuatorCard({
required String title,
required String subtitle,
required bool isActive,
required IconData icon,
required List<Color> activeGradient,
required VoidCallback onTap,
required bool isLocked,
}) {
Color cardBg = Colors.white;
Color borderCol = isActive ? activeGradient[0] : Colors.grey.withOpacity(0.08);
double borderWidth = isActive ? 1.5 : 1.0;
Color iconBg = isActive ? activeGradient[0] : const Color(0xFFF5F7F8);
Color iconCol = isActive ? Colors.white : Colors.black38;
Color titleCol = Colors.black87;
Color subtitleCol = isActive ? activeGradient[0].withOpacity(0.8) : Colors.black38;
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: cardBg,
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: borderCol,
width: borderWidth,
),
boxShadow: [
if (isActive)
BoxShadow(
color: activeGradient[0].withOpacity(0.12),
blurRadius: 16,
offset: const Offset(0, 8),
)
else
BoxShadow(
color: Colors.black.withOpacity(0.01),
blurRadius: 6,
offset: const Offset(0, 3),
)
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Glowing Icon Wrapper
AnimatedContainer(
duration: const Duration(milliseconds: 250),
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: iconBg,
shape: BoxShape.circle,
),
child: Icon(
icon,
color: iconCol,
size: 22,
),
),
// Lock Icon or custom Switch Toggle
isLocked
? const Icon(
Icons.lock_outline_rounded,
color: Colors.black26,
size: 20,
)
: AnimatedContainer(
duration: const Duration(milliseconds: 250),
width: 36,
height: 20,
padding: const EdgeInsets.symmetric(horizontal: 2),
decoration: BoxDecoration(
color: isActive ? activeGradient[0].withOpacity(0.15) : Colors.black.withOpacity(0.08),
borderRadius: BorderRadius.circular(10),
),
child: AnimatedAlign(
duration: const Duration(milliseconds: 200),
alignment: isActive ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
width: 14,
height: 14,
decoration: BoxDecoration(
color: isActive ? activeGradient[0] : Colors.black26,
shape: BoxShape.circle,
),
),
),
),
],
),
const SizedBox(height: 18),
Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: titleCol,
),
),
const SizedBox(height: 3),
Text(
subtitle,
style: TextStyle(
fontSize: 11,
color: subtitleCol,
fontWeight: FontWeight.w500,
),
),
],
),
),
);
}
}

516
lib/screen/history.dart Normal file
View File

@ -0,0 +1,516 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:intl/intl.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'dashboard.dart';
import 'akun.dart';
import '../widgets/fade_in_up.dart';
class HistoryModel {
final String key;
final String time;
final String date;
final double suhu;
final double alkohol;
final String status;
final String jenisTape;
HistoryModel({
required this.key,
required this.time,
required this.date,
required this.suhu,
required this.alkohol,
required this.status,
required this.jenisTape,
});
factory HistoryModel.fromMap(String key, Map data) {
// Ambil waktu sekarang sebagai cadangan (fallback)
DateTime sekarang = DateTime.now();
String jamSkrg = "${sekarang.hour.toString().padLeft(2, '0')}:${sekarang.minute.toString().padLeft(2, '0')}";
String tglSkrg = "${sekarang.day.toString().padLeft(2, '0')}/${sekarang.month.toString().padLeft(2, '0')}/${sekarang.year}";
// Logika Parsing Tanggal dari Firebase
String dateFromDb = data['tanggal']?.toString() ?? tglSkrg;
// Jika format dari DB adalah YYYY-MM-DD, ubah ke DD/MM/YYYY agar filter kalender jalan
if (dateFromDb.contains("-")) {
try {
List<String> p = dateFromDb.split("-");
if (p[0].length == 4) { // YYYY-MM-DD
dateFromDb = "${p[2]}/${p[1]}/${p[0]}";
}
} catch (e) {
dateFromDb = tglSkrg;
}
}
String timeStr = data['jam']?.toString() ?? jamSkrg;
// Ubah format desimal jam (misal "27.18 Jam") menjadi "HH:MM:SS"
if (timeStr.toLowerCase().contains("jam")) {
String numStr = timeStr.replaceAll(RegExp(r'[^0-9.]'), '');
double? val = double.tryParse(numStr);
if (val != null) {
int totalDetik = (val * 3600).toInt();
int j = totalDetik ~/ 3600;
int m = (totalDetik % 3600) ~/ 60;
int s = totalDetik % 60;
timeStr = "${j.toString().padLeft(2, '0')}:${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}";
}
}
String statusStr = data['status']?.toString() ?? "Done";
String lowerStatus = statusStr.toLowerCase();
if (lowerStatus.contains("optimal") || lowerStatus.contains("awal") || lowerStatus.contains("menjelang matang")) {
statusStr = "Belum Matang";
}
return HistoryModel(
key: key,
time: timeStr,
date: dateFromDb,
suhu: (data['suhu'] as num?)?.toDouble() ?? 0.0,
alkohol: (data['alkohol'] as num?)?.toDouble() ?? 0.0,
status: statusStr,
jenisTape: data['jenisTape'] != null
? (data['jenisTape'].toString().contains("Singkong")
? "Tape Singkong"
: "Tape Ketan")
: "Tape Ketan",
);
}
}
class HistoryPage extends StatefulWidget {
const HistoryPage({super.key});
@override
State<HistoryPage> createState() => _HistoryPageState();
}
class _HistoryPageState extends State<HistoryPage> {
final DatabaseReference ref = FirebaseDatabase.instance.ref("history");
String selectedCategory = "All";
DateTime activeDate = DateTime.now();
bool _localeReady = false;
@override
void initState() {
super.initState();
initializeDateFormatting('id', null).then((_) {
if (mounted) setState(() => _localeReady = true);
});
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
if (!_localeReady)
return const Scaffold(body: Center(child: CircularProgressIndicator()));
return Scaffold(
extendBody: true,
body: Stack(
children: [
Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
stops: [0.0, 0.4, 0.7, 1.0],
colors: [
Color(0xFFE8F5E9),
Color(0xFFFFFDF0),
Color(0xFFF1F8E9),
Color(0xFFE8F5E9),
],
),
),
),
Column(
children: [
FadeInUp(delay: 0, child: _buildAppBar()),
FadeInUp(delay: 150, child: _buildHorizontalCalendar()),
FadeInUp(delay: 300, child: _buildCategoryFilter()),
Expanded(child: FadeInUp(delay: 450, child: _buildHistoryList())),
],
),
],
),
bottomNavigationBar: _buildBottomNavbar(),
);
}
Widget _buildBottomNavbar() {
return Container(
decoration: const BoxDecoration(
color: Color(0xFFE8F5E9),
borderRadius: BorderRadius.vertical(top: Radius.circular(30)),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 10,
offset: Offset(0, -2),
),
],
),
child: ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
child: BottomNavigationBar(
currentIndex: 1,
onTap: (index) {
if (index == 0) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const DashboardPage()),
);
} else if (index == 2) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const AkunScreen()),
);
}
},
showSelectedLabels: false,
showUnselectedLabels: false,
backgroundColor: Colors.transparent,
elevation: 0,
type: BottomNavigationBarType.fixed,
items: [
BottomNavigationBarItem(
icon: _navIcon(Icons.home_filled, 0),
label: 'Home',
),
BottomNavigationBarItem(
icon: _navIcon(Icons.history, 1),
label: 'History',
),
BottomNavigationBarItem(
icon: _navIcon(Icons.people_alt_rounded, 2),
label: 'Profile',
),
],
),
),
);
}
Widget _navIcon(IconData icon, int index) {
bool isSelected = index == 1;
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
gradient: isSelected
? const LinearGradient(
colors: [Color(0xFF2E7D32), Color(0xFF4CAF50)],
)
: null,
),
child: Icon(
icon,
color: isSelected ? Colors.white : const Color(0xFF81C784),
size: 24,
),
);
}
Widget _buildAppBar() {
return SafeArea(
bottom: false,
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 24, 20, 10), // Padding seperti di pesan.dart
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: const Icon(Icons.arrow_back_ios_new, size: 20),
onPressed: () => Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const DashboardPage()),
),
),
const Expanded(
child: Text(
"Fermentation History",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
IconButton(
icon: const Icon(Icons.calendar_month, color: Color(0xFF2E7D32)),
onPressed: () async {
DateTime? picked = await showDatePicker(
context: context,
initialDate: activeDate,
firstDate: DateTime(2020),
lastDate: DateTime(2100),
);
if (picked != null && picked != activeDate) {
setState(() {
activeDate = picked;
});
}
},
),
],
),
),
);
}
Widget _buildHorizontalCalendar() {
return Container(
height: 90,
margin: const EdgeInsets.symmetric(vertical: 15), // Jarak seperti di pesan.dart
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 15),
itemCount: 7,
itemBuilder: (context, index) {
DateTime date = activeDate.add(Duration(days: index - 3));
bool isSelected =
date.day == activeDate.day && date.month == activeDate.month;
return GestureDetector(
onTap: () => setState(() => activeDate = date),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
width: 70,
margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFF2E7D32) : Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: isSelected
? [
BoxShadow(
color: const Color(0xFF2E7D32).withOpacity(0.3),
blurRadius: 10,
offset: const Offset(0, 5),
),
]
: [],
border: isSelected
? null
: Border.all(color: Colors.grey.withOpacity(0.1)),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
DateFormat('MMM', 'id').format(date),
style: TextStyle(
color: isSelected ? Colors.white : Colors.black54,
fontSize: 12,
),
),
Text(
date.day.toString(),
style: TextStyle(
color: isSelected ? Colors.white : Colors.black,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
Text(
DateFormat('E', 'id').format(date),
style: TextStyle(
color: isSelected ? Colors.white : Colors.black54,
fontSize: 12,
),
),
],
),
),
);
},
),
);
}
Widget _buildCategoryFilter() {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
children: [
_filterTab("All"),
_filterTab("Tape Ketan"),
_filterTab("Tape Singkong"),
],
),
),
);
}
Widget _filterTab(String label) {
bool isSelected = selectedCategory == label;
return GestureDetector(
onTap: () => setState(() => selectedCategory = label),
child: Container(
margin: const EdgeInsets.only(right: 12),
padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 12),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFF2E7D32) : const Color(0xFFE8F5E9),
borderRadius: BorderRadius.circular(15),
),
child: Text(
label,
style: TextStyle(
color: isSelected ? Colors.white : const Color(0xFF2E7D32),
fontWeight: FontWeight.bold,
),
),
),
);
}
Widget _buildHistoryList() {
return StreamBuilder(
stream: ref.onValue,
builder: (context, AsyncSnapshot<DatabaseEvent> snapshot) {
if (!snapshot.hasData || snapshot.data!.snapshot.value == null)
return const Center(child: Text("No history available"));
Map data = snapshot.data!.snapshot.value as Map;
List<HistoryModel> list = [];
// Format pembanding sesuai dengan format di model (dd/MM/yyyy)
String filterDateString = DateFormat('dd/MM/yyyy').format(activeDate);
data.forEach((key, value) {
final item = HistoryModel.fromMap(key, value);
// Memastikan pencocokan tanggal akurat
if (item.date == filterDateString &&
(selectedCategory == "All" || item.jenisTape == selectedCategory)) {
list.add(item);
}
});
// Urutkan dari yang terbaru (berdasarkan key Firebase atau jam)
list.sort((a, b) => b.time.compareTo(a.time));
if (list.isEmpty)
return const Center(child: Text("No data for this date"));
return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 20),
itemCount: list.length,
itemBuilder: (context, index) => _buildHistoryCard(list[index]),
);
},
);
}
Widget _buildHistoryCard(HistoryModel item) {
bool isAlert =
item.status.toLowerCase().contains("matang") &&
!item.status.toLowerCase().contains("belum");
return Container(
margin: const EdgeInsets.only(bottom: 15),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.03),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
border: Border.all(color: Colors.grey.withOpacity(0.05)),
),
child: Stack(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.jenisTape,
style: const TextStyle(color: Colors.black45, fontSize: 13),
),
Text(
"Suhu: ${item.suhu}°C",
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
"Alkohol: ${item.jenisTape.contains('Singkong') ? item.alkohol.toStringAsFixed(1) : item.alkohol.toStringAsFixed(2)}%",
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 10),
Row(
children: [
const Icon(
Icons.access_time_filled,
size: 16,
color: Color(0xFF2E7D32),
),
const SizedBox(width: 5),
Text(
item.time,
style: const TextStyle(
color: Color(0xFF2E7D32),
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: 15),
const Icon(
Icons.calendar_month,
size: 16,
color: Color(0xFF2E7D32),
),
const SizedBox(width: 5),
Text(
item.date,
style: const TextStyle(
color: Color(0xFF2E7D32),
fontWeight: FontWeight.w600,
),
),
],
),
],
),
Positioned(
right: 0,
bottom: 0,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFFE8F5E9),
borderRadius: BorderRadius.circular(10),
),
child: Text(
item.status,
style: TextStyle(
color: isAlert ? Colors.orange : const Color(0xFF2E7D32),
fontSize: 11,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
);
}
}

250
lib/screen/login.dart Normal file
View File

@ -0,0 +1,250 @@
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import '../widgets/fade_in_up.dart';
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final emailController = TextEditingController();
final passwordController = TextEditingController();
bool loading = false;
bool hidePassword = true;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: Stack(
children: [
Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
stops: [0.0, 0.4, 0.7, 1.0],
colors: [
Color(0xFFE8F5E9),
Color(0xFFFFFDF0),
Color(0xFFF1F8E9),
Color(0xFFE8F5E9),
],
),
),
),
SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 25),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 20),
// --- TITLE & SUBTITLE ---
FadeInUp(
delay: 0,
child: Column(
children: const [
Text(
"Login Account",
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Color(0xFF2E7D32),
),
),
SizedBox(height: 12),
Text(
"masuk untuk memulai fermentasi.",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16, color: Colors.black54),
),
],
),
),
const SizedBox(height: 50),
// --- FORM CARD ---
FadeInUp(
delay: 150,
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 30,
offset: const Offset(0, 10),
),
],
),
child: Column(
children: [
_buildInput(
controller: emailController,
hint: "Email",
icon: Icons.email_outlined,
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: 15),
_buildInput(
controller: passwordController,
hint: "Password",
icon: Icons.lock_outline_rounded,
isPassword: true,
obscure: hidePassword,
onToggle: () =>
setState(() => hidePassword = !hidePassword),
),
],
),
),
),
const SizedBox(height: 40),
// --- LOGIN BUTTON ---
FadeInUp(
delay: 300,
child: SizedBox(
width: double.infinity,
height: 65,
child: ElevatedButton(
onPressed: loading ? null : _login,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(
0xFF2E7D32,
),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
elevation: 0,
),
child: loading
? const CircularProgressIndicator(
color: Colors.white,
)
: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Login",
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
SizedBox(width: 15),
Icon(Icons.arrow_right_alt_rounded, size: 30),
],
),
),
),
),
const SizedBox(height: 20),
// Link ke Register
FadeInUp(
delay: 450,
child: TextButton(
onPressed: () =>
Navigator.pushNamed(context, '/register'),
child: const Text(
"Belum punya akun? Register",
style: TextStyle(color: Colors.black45),
),
),
),
const SizedBox(height: 20),
],
),
),
),
),
],
),
);
}
// --- WIDGET HELPER: INPUT FIELD ---
Widget _buildInput({
required TextEditingController controller,
required String hint,
required IconData icon,
bool isPassword = false,
bool obscure = false,
VoidCallback? onToggle,
TextInputType keyboardType = TextInputType.text,
}) {
return TextField(
controller: controller,
obscureText: obscure,
keyboardType: keyboardType,
decoration: InputDecoration(
hintText: hint,
hintStyle: const TextStyle(color: Colors.black38),
prefixIcon: Icon(icon, color: Colors.black87),
suffixIcon: isPassword
? IconButton(
icon: Icon(
obscure
? Icons.visibility_off_outlined
: Icons.visibility_outlined,
color: Colors.black87,
),
onPressed: onToggle,
)
: null,
filled: true,
fillColor: const Color(
0xFFE8F5E9,
).withOpacity(0.5), // Biru sangat muda sesuai desain
contentPadding: const EdgeInsets.symmetric(
vertical: 20,
horizontal: 20,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide: BorderSide.none,
),
),
);
}
// --- LOGIC LOGIN ---
Future<void> _login() async {
if (emailController.text.isEmpty || passwordController.text.isEmpty) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text("Isi semua bidang!")));
return;
}
setState(() => loading = true);
try {
await FirebaseAuth.instance.signInWithEmailAndPassword(
email: emailController.text.trim(),
password: passwordController.text.trim(),
);
if (mounted) Navigator.pushReplacementNamed(context, '/dashboard');
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(e.toString())));
}
}
if (mounted) setState(() => loading = false);
}
}

563
lib/screen/pesan.dart Normal file
View File

@ -0,0 +1,563 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:firebase_database/firebase_database.dart';
import '../widgets/fade_in_up.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
// ================= TOP LEVEL HANDLER =================
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
print("Handling background message: ${message.messageId}");
}
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
// ================= MODEL =================
class NotificationModel {
final String id;
final String time;
final String date;
final double suhu;
final double alkohol;
final String status;
final String tipe;
final String jenisTape;
final String dbPath;
NotificationModel({
required this.id,
required this.time,
required this.date,
required this.suhu,
required this.alkohol,
required this.status,
required this.tipe,
required this.jenisTape,
required this.dbPath,
});
}
class NotificationPage extends StatefulWidget {
const NotificationPage({super.key});
@override
State<NotificationPage> createState() => _NotificationPageState();
}
class _NotificationPageState extends State<NotificationPage> {
final DatabaseReference notifRef = FirebaseDatabase.instance.ref("notifikasi");
final DatabaseReference warningRef = FirebaseDatabase.instance.ref("peringatan");
List<NotificationModel> notifications = [];
bool isLoading = true;
String selectedFilter = "Monitoring";
final Map<String, NotificationModel> _notifMap = {};
final Map<String, NotificationModel> _warningMap = {};
@override
void initState() {
super.initState();
initLocalNotif();
initFCM();
listenNotification();
}
@override
void dispose() {
super.dispose();
}
void _deleteNotification(NotificationModel n) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text("Hapus Pesan?"),
content: const Text("Apakah Anda yakin ingin menghapus pesan ini?"),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text("Batal")),
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
onPressed: () {
FirebaseDatabase.instance.ref(n.dbPath).child(n.id).remove();
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Pesan berhasil dihapus")),
);
},
child: const Text("Hapus", style: TextStyle(color: Colors.white)),
),
],
),
);
}
void initLocalNotif() async {
const AndroidInitializationSettings androidSettings =
AndroidInitializationSettings('@mipmap/launcher_icon');
const InitializationSettings settings = InitializationSettings(
android: androidSettings,
);
await flutterLocalNotificationsPlugin.initialize(settings);
}
void initFCM() async {
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
await FirebaseMessaging.instance.subscribeToTopic("fermentasi_update");
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
if (message.notification != null) {
String body = message.notification!.body ?? "";
body = body
.replaceAll(RegExp(r'Proses Optimal', caseSensitive: false), 'Belum Matang')
.replaceAll(RegExp(r'Proses Awal', caseSensitive: false), 'Belum Matang')
.replaceAll(RegExp(r'Menjelang Matang', caseSensitive: false), 'Belum Matang');
flutterLocalNotificationsPlugin.show(
message.hashCode,
message.notification!.title,
body,
const NotificationDetails(
android: AndroidNotificationDetails(
'fermentasi_channel',
'Fermentasi Notifikasi',
importance: Importance.max,
priority: Priority.high,
icon: '@mipmap/launcher_icon',
),
),
);
}
});
}
NotificationModel? _parseNotification(String key, dynamic value, String sourcePath) {
try {
if (value is! Map) return null;
String id = key.toString();
DateTime notifDate = DateTime.now();
// Ekstrak waktu dari Firebase Push ID untuk akurasi tinggi
if (id.startsWith('-') && id.length >= 8) {
const chars = '-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz';
int time = 0;
for (int i = 0; i < 8; i++) {
int index = chars.indexOf(id[i]);
if (index != -1) {
time = time * 64 + index;
}
}
if (time > 1000000000000) { // Validasi tahun (lebih dari 2001)
notifDate = DateTime.fromMillisecondsSinceEpoch(time);
}
}
String datePart = "${notifDate.day.toString().padLeft(2, '0')}-${notifDate.month.toString().padLeft(2, '0')}-${notifDate.year}";
String timePart = "${notifDate.hour.toString().padLeft(2, '0')}:${notifDate.minute.toString().padLeft(2, '0')}";
// Ekstrak tipe dengan sangat kokoh (mendukung key "tipe" / "type" dan status bahaya/warning/terlalu matang)
String rawTipe = (value["tipe"] ?? value["type"] ?? "").toString().toLowerCase().trim();
String rawStatus = (value["status"] ?? "").toString().toLowerCase().trim();
String tipeStr;
if (sourcePath == "peringatan") {
tipeStr = "Peringatan";
} else if (rawTipe == "selesai" || rawTipe == "done" || rawTipe == "finished" || rawTipe == "complete" || rawStatus == "matang" || rawStatus == "selesai") {
tipeStr = "Selesai";
} else if (rawTipe == "peringatan" || rawTipe == "warning" || rawTipe == "alert" || rawTipe == "danger" || rawTipe == "bahaya" ||
rawStatus.contains("peringatan") || rawStatus.contains("terlalu matang") || rawStatus.contains("bahaya") ||
rawStatus.contains("warning") || rawStatus.contains("alert") || rawStatus.contains("danger") || rawStatus.contains("tinggi")) {
tipeStr = "Peringatan";
} else {
tipeStr = "Monitoring";
}
if (tipeStr == "Selesai") {
String total = value["jam_total"]?.toString() ?? value["waktu"]?.toString() ?? "";
if (total.isNotEmpty && !total.contains("/")) {
if (total.toLowerCase().contains("jam")) {
timePart = total;
} else {
double? val = double.tryParse(total);
if (val != null) {
if (val == 0) {
timePart = "0 Detik";
} else if (val < 0.016) { // Kurang dari ~1 Menit
timePart = "${(val * 3600).toInt()} Detik";
} else if (val < 1.0) { // Kurang dari 1 Jam
timePart = "${(val * 60).toInt()} Menit";
} else {
timePart = "${val.toStringAsFixed(2)} Jam";
}
} else {
timePart = "$total Jam";
}
}
}
}
String statusStr = value["status"]?.toString() ?? (sourcePath == "peringatan" ? "Terlalu Matang" : "Monitoring");
String lowerStatus = statusStr.toLowerCase();
if (lowerStatus.contains("optimal") || lowerStatus.contains("awal") || lowerStatus.contains("menjelang matang")) {
statusStr = "Belum Matang";
}
if (tipeStr == "Peringatan" && !statusStr.toLowerCase().contains("terlalu matang")) {
return null;
}
return NotificationModel(
id: id,
time: timePart,
date: datePart,
suhu: (value["suhu"] as num?)?.toDouble() ?? 0.0,
alkohol: (value["alkohol"] as num?)?.toDouble() ?? 0.0,
status: statusStr,
tipe: tipeStr,
jenisTape: value["jenisTape"] ?? "Tape Ketan",
dbPath: sourcePath,
);
} catch (e) {
debugPrint("Error parsing single notification: $e");
return null;
}
}
void listenNotification() {
// 1. Listen to 'notifikasi' path
notifRef.onValue.listen((event) {
final data = event.snapshot.value as Map<dynamic, dynamic>?;
_notifMap.clear();
if (data != null) {
data.forEach((key, value) {
final model = _parseNotification(key.toString(), value, "notifikasi");
if (model != null) {
_notifMap[model.id] = model;
}
});
}
_combineAndSortNotifications();
});
// 2. Listen to 'peringatan' path
warningRef.onValue.listen((event) {
final data = event.snapshot.value as Map<dynamic, dynamic>?;
_warningMap.clear();
if (data != null) {
data.forEach((key, value) {
final model = _parseNotification(key.toString(), value, "peringatan");
if (model != null) {
_warningMap[model.id] = model;
}
});
}
_combineAndSortNotifications();
});
}
void _combineAndSortNotifications() {
List<NotificationModel> temp = [..._notifMap.values, ..._warningMap.values];
temp.sort((a, b) => b.id.compareTo(a.id)); // Reversed push-ID (newest first)
if (mounted) {
setState(() {
notifications = temp;
isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
extendBody: true,
body: Stack(
children: [
Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
stops: [0.0, 0.4, 0.7, 1.0],
colors: [
Color(0xFFE8F5E9),
Color(0xFFFFFDF0),
Color(0xFFF1F8E9),
Color(0xFFE8F5E9),
],
),
),
),
Column(
children: [
FadeInUp(delay: 0, child: _buildAppBar()),
FadeInUp(delay: 150, child: _buildFilterTabs()),
Expanded(
child: isLoading
? const Center(child: CircularProgressIndicator())
: FadeInUp(delay: 300, child: _buildMessageList()),
),
],
),
],
),
);
}
// ================= 1. APP BAR =================
Widget _buildAppBar() {
return SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 24, 20, 5),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: const Icon(Icons.arrow_back_ios_new, size: 20),
onPressed: () => Navigator.pop(context),
),
const Expanded(
child: Text(
"Fermentation Message",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const Icon(Icons.notifications_active, color: Color(0xFF2E7D32)),
],
),
),
);
}
// ================= 2. FILTER TABS =================
Widget _buildFilterTabs() {
return Padding(
padding: const EdgeInsets.only(top: 18, bottom: 18),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_filterBtn("Monitoring"),
_filterBtn("Peringatan"),
_filterBtn("Selesai"),
],
),
);
}
Widget _filterBtn(String label) {
bool isSelected = selectedFilter == label;
return GestureDetector(
onTap: () => setState(() => selectedFilter = label),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFF2E7D32) : const Color(0xFFE8F5E9),
borderRadius: BorderRadius.circular(15),
gradient: isSelected
? const LinearGradient(
colors: [Color(0xFF2E7D32), Color(0xFF4CAF50)],
)
: null,
),
child: Text(
label,
style: TextStyle(
color: isSelected ? Colors.white : const Color(0xFF2E7D32),
fontWeight: FontWeight.bold,
),
),
),
);
}
// ================= 3. LIST PESAN =================
Widget _buildMessageList() {
final filteredList = notifications
.where((n) => n.tipe.toLowerCase() == selectedFilter.toLowerCase())
.toList();
if (filteredList.isEmpty) {
return const Center(child: Text("Belum ada pesan untuk kategori ini"));
}
return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 20),
itemCount: filteredList.length,
itemBuilder: (context, index) {
final n = filteredList[index];
return _buildMessageCard(n);
},
);
}
Widget _buildMessageCard(NotificationModel n) {
bool isSelesai = n.tipe.toLowerCase() == "selesai";
bool isManual = n.tipe.toLowerCase() == "peringatan";
return Container(
margin: const EdgeInsets.only(bottom: 18),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(25),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
border: Border.all(color: Colors.grey.withOpacity(0.1)),
),
child: Stack(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${n.jenisTape} | ${n.tipe}",
style: const TextStyle(color: Colors.black45, fontSize: 13),
),
const SizedBox(height: 8),
if (isManual)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${n.jenisTape} Terlalu Matang",
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
height: 1.2,
color: Colors.redAccent,
),
),
const SizedBox(height: 4),
Text(
"Alkohol saat ini: ${n.jenisTape.contains('Singkong') ? n.alkohol.toStringAsFixed(1) : n.alkohol.toStringAsFixed(2)}%",
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
],
)
else
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Suhu: ${n.suhu}°C",
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
"Alkohol: ${n.jenisTape.contains('Singkong') ? n.alkohol.toStringAsFixed(1) : n.alkohol.toStringAsFixed(2)}%",
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
"Status: ${n.status}",
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 12),
if (!isManual)
Row(
children: [
const Icon(
Icons.access_time_filled,
size: 16,
color: Color(0xFF81C784),
),
const SizedBox(width: 5),
Text(
n.time,
style: const TextStyle(
color: Color(0xFF81C784),
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: 15),
const Icon(
Icons.calendar_month,
size: 16,
color: Color(0xFF81C784),
),
const SizedBox(width: 5),
Text(
n.date,
style: const TextStyle(
color: Color(0xFF81C784),
fontWeight: FontWeight.w600,
),
),
],
),
],
),
Positioned(
right: 0,
top: 0,
child: GestureDetector(
onTap: () => _deleteNotification(n),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
Icons.delete_outline_rounded,
color: Colors.red,
size: 18,
),
),
),
),
if (!isManual)
Positioned(
right: 0,
bottom: 0,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
),
decoration: BoxDecoration(
color: isSelesai
? const Color(0xFFE8F5E9)
: const Color(0xFFFFFDE7),
borderRadius: BorderRadius.circular(8),
),
child: Text(
isSelesai ? "Done" : "In Progress",
style: TextStyle(
color: isSelesai ? const Color(0xFF2E7D32) : Colors.orange,
fontSize: 11,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
);
}
}

270
lib/screen/register.dart Normal file
View File

@ -0,0 +1,270 @@
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import '../widgets/fade_in_up.dart';
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();
final confirmController = TextEditingController();
bool loading = false;
bool hidePassword = true;
bool hideConfirmPassword = true;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: Stack(
children: [
Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
stops: [0.0, 0.4, 0.7, 1.0],
colors: [
Color(0xFFE8F5E9),
Color(0xFFFFFDF0),
Color(0xFFF1F8E9),
Color(0xFFE8F5E9),
],
),
),
),
SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 25),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 20),
// --- TITLE & SUBTITLE ---
FadeInUp(
delay: 0,
child: Column(
children: const [
Text(
"Create Account",
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Color(0xFF2E7D32),
),
),
SizedBox(height: 12),
Text(
"daftar terlebih dahulu untuk memulai fermentasi.",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
color: Colors.black54,
height: 1.5,
),
),
],
),
),
const SizedBox(height: 40),
// --- FORM CARD ---
FadeInUp(
delay: 150,
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 30,
offset: const Offset(0, 10),
),
],
),
child: Column(
children: [
_buildInput(
controller: nameController,
hint: "Full name",
icon: Icons.person_outline_rounded,
),
const SizedBox(height: 15),
_buildInput(
controller: emailController,
hint: "Email",
icon: Icons.email_outlined,
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: 15),
_buildInput(
controller: passwordController,
hint: "Password",
icon: Icons.lock_outline_rounded,
isPassword: true,
obscure: hidePassword,
onToggle: () =>
setState(() => hidePassword = !hidePassword),
),
const SizedBox(height: 15),
_buildInput(
controller: confirmController,
hint: "Confirm password",
icon: Icons.lock_outline_rounded,
isPassword: true,
obscure: hideConfirmPassword,
onToggle: () => setState(
() => hideConfirmPassword = !hideConfirmPassword,
),
),
],
),
),
),
const SizedBox(height: 35),
// --- REGISTER BUTTON ---
FadeInUp(
delay: 300,
child: SizedBox(
width: double.infinity,
height: 65,
child: ElevatedButton(
onPressed: loading ? null : _register,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2E7D32),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
elevation: 0,
),
child: loading
? const CircularProgressIndicator(color: Colors.white)
: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Register",
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
SizedBox(width: 15),
Icon(Icons.arrow_right_alt_rounded, size: 30),
],
),
),
),
),
const SizedBox(height: 20),
// Link kembali ke login
// --- LINK KE LOGIN (DIUBAH) ---
FadeInUp(
delay: 450,
child: TextButton(
onPressed: () {
// Gunakan pushReplacementNamed agar tidak menumpuk di stack
Navigator.pushReplacementNamed(context, '/login');
},
child: const Text(
"Sudah punya akun? Login",
style: TextStyle(color: Colors.black45),
),
),
),
const SizedBox(height: 20),
],
),
),
),
),
],
),
);
}
// --- WIDGET HELPER: INPUT FIELD ---
Widget _buildInput({
required TextEditingController controller,
required String hint,
required IconData icon,
bool isPassword = false,
bool obscure = false,
VoidCallback? onToggle,
TextInputType keyboardType = TextInputType.text,
}) {
return TextField(
controller: controller,
obscureText: obscure,
keyboardType: keyboardType,
decoration: InputDecoration(
hintText: hint,
hintStyle: const TextStyle(color: Colors.black38),
prefixIcon: Icon(icon, color: Colors.black87),
suffixIcon: isPassword
? IconButton(
icon: Icon(
obscure
? Icons.visibility_off_outlined
: Icons.visibility_outlined,
color: Colors.black87,
),
onPressed: onToggle,
)
: null,
filled: true,
fillColor: const Color(
0xFFE8F5E9,
).withOpacity(0.5), // Warna biru muda di desain
contentPadding: const EdgeInsets.symmetric(
vertical: 20,
horizontal: 20,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide: BorderSide.none,
),
),
);
}
Future<void> _register() async {
if (passwordController.text != confirmController.text) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text("Password tidak sama")));
return;
}
setState(() => loading = true);
try {
await FirebaseAuth.instance.createUserWithEmailAndPassword(
email: emailController.text.trim(),
password: passwordController.text.trim(),
);
Navigator.pushReplacementNamed(context, '/login');
} catch (e) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(e.toString())));
}
setState(() => loading = false);
}
}

189
lib/screen/welcome.dart Normal file
View File

@ -0,0 +1,189 @@
import 'package:flutter/material.dart';
import '../widgets/fade_in_up.dart';
class WelcomeScreen extends StatefulWidget {
const WelcomeScreen({super.key});
@override
State<WelcomeScreen> createState() => _WelcomeScreenState();
}
class _WelcomeScreenState extends State<WelcomeScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: Stack(
children: [
/// 1. ORIGINAL SOFT GRADIENT BACKGROUND
Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
stops: [0.0, 0.4, 0.7, 1.0],
colors: [
Color(0xFFE8F5E9),
Color(0xFFFFFDF0),
Color(0xFFF1F8E9),
Color(0xFFE8F5E9),
],
),
),
),
/// DESIGNER BACKGROUND SPOTLIGHTS
Positioned(
top: -60,
right: -60,
child: Container(
width: 240,
height: 240,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xFF2E7D32).withOpacity(0.05),
),
),
),
Positioned(
bottom: 250,
left: -80,
child: Container(
width: 260,
height: 260,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.orange.withOpacity(0.04),
),
),
),
/// 2. CORE LAYOUT
SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
children: [
const Spacer(flex: 3),
/// FLOATING HERO IMAGE WITH ELEVATED SHADOW EFFECT
FadeInUp(
delay: 0,
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: const Color(0xFF2E7D32).withOpacity(0.06),
blurRadius: 45,
offset: const Offset(0, 8),
),
],
),
child: Image.asset(
"assets/images/tape.png",
height: 260,
fit: BoxFit.contain,
),
),
),
const Spacer(flex: 3),
/// MAIN TITLE (SIMPLIFIED & UPDATED)
FadeInUp(
delay: 150,
child: Center(
child: FittedBox(
fit: BoxFit.scaleDown,
child: RichText(
textAlign: TextAlign.center,
text: const TextSpan(
style: TextStyle(
fontSize: 21,
fontWeight: FontWeight.w900,
letterSpacing: 1.0,
height: 1.45,
),
children: [
TextSpan(
text: "MONITORING & KONTROL\n",
style: TextStyle(color: Color(0xFF2E7D32)),
),
TextSpan(
text: "MAKANAN BERFERMENTASI",
style: TextStyle(color: Colors.orange),
),
],
),
),
),
),
),
const Spacer(flex: 2),
/// START BUTTON WITH ORIGINAL EMERALD COLOR & ORIGINAL ACCENTS
FadeInUp(
delay: 300,
child: Container(
width: double.infinity,
height: 60,
margin: const EdgeInsets.only(bottom: 10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25),
boxShadow: [
BoxShadow(
color: const Color(0xFF2E7D32).withOpacity(0.35),
blurRadius: 18,
offset: const Offset(0, 8),
),
],
),
child: ElevatedButton(
onPressed: () {
Navigator.pushNamed(context, '/register');
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2E7D32),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(25),
),
elevation: 0,
),
child: const FittedBox(
fit: BoxFit.scaleDown,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Let's Start",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
),
SizedBox(width: 12),
Icon(
Icons.arrow_right_alt_rounded,
size: 28,
),
],
),
),
),
),
),
const Spacer(flex: 3),
],
),
),
),
],
),
);
}
}

View File

@ -0,0 +1,54 @@
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
class NotificationService {
static final FlutterLocalNotificationsPlugin _notifications =
FlutterLocalNotificationsPlugin();
static Future<void> init() async {
const AndroidInitializationSettings androidSettings =
AndroidInitializationSettings('@mipmap/ic_launcher');
const InitializationSettings settings = InitializationSettings(
android: androidSettings,
);
await _notifications.initialize(
settings,
onDidReceiveNotificationResponse: (NotificationResponse response) {
// optional
},
);
// CREATE CHANNEL (WAJIB)
const AndroidNotificationChannel channel = AndroidNotificationChannel(
'channel_id',
'channel_name',
description: 'Notifikasi fermentasi',
importance: Importance.max,
);
final androidPlugin = _notifications
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>();
await androidPlugin?.createNotificationChannel(channel);
}
static Future<void> showNotification(String title, String body) async {
const AndroidNotificationDetails androidDetails =
AndroidNotificationDetails(
'channel_id',
'channel_name',
channelDescription: 'Notifikasi fermentasi',
importance: Importance.max,
priority: Priority.high,
);
const NotificationDetails details = NotificationDetails(
android: androidDetails,
);
await _notifications.show(0, title, body, details, payload: 'default');
}
}

View File

@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
class FadeInUp extends StatefulWidget {
final Widget child;
final int delay;
const FadeInUp({super.key, required this.child, this.delay = 0});
@override
State<FadeInUp> createState() => _FadeInUpState();
}
class _FadeInUpState extends State<FadeInUp> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _opacity;
late Animation<Offset> _slide;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 600),
);
_opacity = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
);
_slide = Tween<Offset>(begin: const Offset(0, 0.2), end: Offset.zero).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic),
);
if (widget.delay > 0) {
Future.delayed(Duration(milliseconds: widget.delay), () {
if (mounted) _controller.forward();
});
} else {
_controller.forward();
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _opacity,
child: SlideTransition(
position: _slide,
child: widget.child,
),
);
}
}

1
linux/.gitignore vendored Normal file
View File

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

128
linux/CMakeLists.txt Normal file
View File

@ -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 "ketan")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.example.ketan")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(SET CMP0063 NEW)
# Load bundled libraries from the lib/ directory relative to the binary.
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Root filesystem for cross-building.
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
endif()
# Define build configuration options.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
# Compilation settings that should be applied to most targets.
#
# Be cautious about adding new options here, as plugins use this function by
# default. In most cases, you should add new options to specific targets instead
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
# Application build; see runner/CMakeLists.txt.
add_subdirectory("runner")
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)
# Only the install-generated bundle's copy of the executable will launch
# correctly, since the resources must in the right relative locations. To avoid
# people trying to run the unbundled copy, put it in a subdirectory instead of
# the default top-level location.
set_target_properties(${BINARY_NAME}
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
)
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
# directory.
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
# Start with a clean build bundle directory every time.
install(CODE "
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
" COMPONENT Runtime)
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
install(FILES "${bundled_library}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endforeach(bundled_library)
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()

View File

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

View File

@ -0,0 +1,11 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
void fl_register_plugins(FlPluginRegistry* registry) {
}

View File

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

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