Upload project

This commit is contained in:
Denipratama01 2026-08-01 10:00:52 +07:00
parent 02e04dd3f6
commit 6f33605c09
160 changed files with 12695 additions and 0 deletions

43
.gitignore vendored Normal file
View File

@ -0,0 +1,43 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
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
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# 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: "dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
- platform: android
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
- platform: ios
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
- platform: linux
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
- platform: macos
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
- platform: web
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
- platform: windows
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
# 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'

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

13
android/.gitignore vendored Normal file
View File

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

44
android/app/build.gradle Normal file
View File

@ -0,0 +1,44 @@
plugins {
id "com.android.application"
id "kotlin-android"
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id "dev.flutter.flutter-gradle-plugin"
}
android {
namespace = "com.example.kopikaocare"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.kopikaocare"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.debug
}
}
}
flutter {
source = "../.."
}

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,46 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:label="kopikaocare"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@ -0,0 +1,5 @@
package com.example.kopikaocare
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: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

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>

18
android/build.gradle Normal file
View File

@ -0,0 +1,18 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = "../build"
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

View File

@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -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.11.1-all.zip

25
android/settings.gradle Normal file
View File

@ -0,0 +1,25 @@
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.9.1" apply false
id "org.jetbrains.kotlin.android" version "2.1.21" apply false
}
include ":app"

BIN
assets/about.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

BIN
assets/coffe.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

BIN
assets/gif/about.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

BIN
assets/gif/history.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

BIN
assets/gif/sensor.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 880 KiB

BIN
assets/gif/settings.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

BIN
assets/history.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

BIN
assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
assets/profile.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

BIN
assets/sensor.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 880 KiB

BIN
assets/settings.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

BIN
assets/suhu.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

0
flutter_01.png Normal file
View File

0
flutter_02.png Normal file
View File

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>12.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 = 12.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.kopikaocare;
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.kopikaocare.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.kopikaocare.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.kopikaocare.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.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.kopikaocare;
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.kopikaocare;
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,98 @@
<?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"
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"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
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: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

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>Kopikaocare</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>kopikaocare</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.
}
}

512
lib/about.dart Normal file
View File

@ -0,0 +1,512 @@
import 'package:flutter/material.dart';
class AboutAppScreen extends StatelessWidget {
const AboutAppScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xfffef3f2), // Putih kemerahan
appBar: AppBar(
title: const Text(
'Tentang Aplikasi',
style: TextStyle(fontWeight: FontWeight.bold, letterSpacing: 1),
),
backgroundColor: Colors.red,
foregroundColor: Colors.white,
elevation: 0,
),
body: SingleChildScrollView(
child: Column(
children: [
// Header Gradasi Merah Putih
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 24),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.red.shade700,
Colors.red.shade500,
Colors.red.shade300,
Colors.red.shade100,
Colors.white,
],
stops: const [0.0, 0.3, 0.6, 0.85, 1.0],
),
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(40),
bottomRight: Radius.circular(40),
),
),
child: Column(
children: [
// Animasi Coffee Bean
TweenAnimationBuilder(
duration: const Duration(seconds: 2),
tween: Tween<double>(begin: 0, end: 1),
builder: (context, double value, child) {
return Transform.scale(
scale: value,
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: Colors.white.withOpacity(0.5),
width: 2,
),
boxShadow: [
BoxShadow(
blurRadius: 30,
color: Colors.white.withOpacity(0.3),
spreadRadius: 5,
),
],
),
child: const Icon(
Icons.coffee,
size: 60,
color: Colors.white,
),
),
);
},
),
const SizedBox(height: 20),
const Text(
'KopiCare IoT',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 2,
),
),
const SizedBox(height: 8),
Container(
width: 80,
height: 3,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(2),
color: Colors.white,
),
),
const SizedBox(height: 16),
const Text(
'Smart Coffee Drying System',
style: TextStyle(
fontSize: 14,
color: Colors.white,
letterSpacing: 1,
),
),
],
),
),
const SizedBox(height: 30),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: [
// Deskripsi Card
_buildWhiteCard(
icon: Icons.description,
title: 'Deskripsi',
content:
'Aplikasi ini dirancang untuk memonitor dan mengatur proses '
'pengeringan biji kopi secara cerdas menggunakan teknologi Internet '
'of Things (IoT). Dengan aplikasi ini, petani dapat memantau suhu, '
'kelembapan, dan berat biji kopi secara real-time, sehingga proses '
'pengeringan menjadi lebih efisien dan mengurangi resiko kerusakan '
'biji kopi.',
color: Colors.red,
),
const SizedBox(height: 20),
// Fitur Utama Card
_buildWhiteCard(
icon: Icons.rocket_launch,
title: 'Fitur Utama',
content: '',
color: Colors.red,
child: Column(
children: [
_buildFeatureItem(
Icons.thermostat,
'Monitoring Suhu & Kelembapan',
'Real-time data sensor'),
_buildFeatureItem(
Icons.fitness_center,
'Monitoring Berat Biji Kopi',
'Pantau proses pengeringan'),
_buildFeatureItem(Icons.smart_toy, 'Kontrol Otomatis',
'Heater & kipas berbasis AI'),
_buildFeatureItem(Icons.timer, 'Interval Update',
'Atur frekuensi pengambilan data'),
_buildFeatureItem(Icons.history, 'Riwayat Pengeringan',
'Data tersimpan untuk analisis'),
],
),
),
const SizedBox(height: 20),
// Manfaat Card
_buildWhiteCard(
icon: Icons.emoji_events,
title: 'Manfaat',
content: '',
color: Colors.red,
child: Column(
children: [
_buildBenefitItem(Icons.star, 'Kualitas Terjaga',
'Biji kopi kering optimal'),
_buildBenefitItem(Icons.timer, 'Efisiensi Waktu',
'Proses lebih cepat 40%'),
_buildBenefitItem(Icons.savings, 'Hemat Biaya',
'Mengurangi biaya operasional'),
_buildBenefitItem(Icons.analytics, 'Data Akurat',
'Monitoring presisi tinggi'),
],
),
),
const SizedBox(height: 20),
// Proses Pengeringan Kopi
_buildWhiteCard(
icon: Icons.timeline,
title: 'Proses Pengeringan',
content: '',
color: Colors.red,
child: Column(
children: [
_buildProcessStep('1', 'Pemanasan Awal',
'Heater menyala hingga suhu optimal'),
_buildProcessStep('2', 'Pengeringan Utama',
'Suhu dijaga stabil, berat berkurang'),
_buildProcessStep('3', 'Pendinginan',
'Kipas menyala, suhu diturunkan'),
_buildProcessStep('4', 'Pengeringan Selesai',
'Target berat tercapai, proses stop'),
],
),
),
const SizedBox(height: 20),
// Teknologi Card
_buildWhiteCard(
icon: Icons.devices,
title: 'Teknologi',
content: '',
color: Colors.red,
child: Wrap(
spacing: 10,
runSpacing: 10,
children: [
_buildTechChip('Flutter', Colors.blue),
_buildTechChip('IoT', Colors.red),
_buildTechChip('ESP32', Colors.green),
_buildTechChip('Sensor DHT22', Colors.orange),
_buildTechChip('Load Cell', Colors.purple),
_buildTechChip('Firebase', Colors.yellow.shade800),
],
),
),
const SizedBox(height: 30),
// Footer
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.red.shade700,
Colors.red.shade500,
Colors.red.shade300,
],
),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
blurRadius: 20,
color: Colors.red.withOpacity(0.3),
),
],
),
child: Column(
children: [
const Icon(
Icons.coffee,
color: Colors.white,
size: 40,
),
const SizedBox(height: 10),
const Text(
'KopiCare IoT',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 1,
),
),
const SizedBox(height: 5),
Text(
'Smart Coffee Drying System',
style: TextStyle(
fontSize: 12,
color: Colors.white.withOpacity(0.9),
),
),
const SizedBox(height: 15),
const Divider(color: Colors.white24),
const SizedBox(height: 10),
Text(
'© 2026 KopiCare IoT | Version 2.0.0',
style: TextStyle(
fontSize: 11,
color: Colors.white.withOpacity(0.7),
),
),
],
),
),
const SizedBox(height: 30),
],
),
),
],
),
),
);
}
Widget _buildWhiteCard({
required IconData icon,
required String title,
required String content,
required Color color,
Widget? child,
}) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(25),
boxShadow: [
BoxShadow(
blurRadius: 15,
color: Colors.red.withOpacity(0.1),
offset: const Offset(0, 8),
),
],
border: Border.all(
color: Colors.red.withOpacity(0.1),
width: 1,
),
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(15),
),
child: Icon(icon, color: Colors.red, size: 24),
),
const SizedBox(width: 15),
Text(
title,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Color(0xff2d2d2d),
letterSpacing: 0.5,
),
),
],
),
const SizedBox(height: 20),
if (content.isNotEmpty)
Text(
content,
style: const TextStyle(
fontSize: 14,
color: Color(0xff666666),
height: 1.5,
),
),
if (child != null) child,
],
),
),
);
}
Widget _buildFeatureItem(IconData icon, String title, String subtitle) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: Colors.red, size: 20),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Color(0xff2d2d2d),
),
),
Text(
subtitle,
style: const TextStyle(
fontSize: 12,
color: Color(0xff888888),
),
),
],
),
),
],
),
);
}
Widget _buildBenefitItem(IconData icon, String title, String subtitle) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: Colors.green, size: 20),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Color(0xff2d2d2d),
),
),
Text(
subtitle,
style: const TextStyle(
fontSize: 12,
color: Color(0xff888888),
),
),
],
),
),
],
),
);
}
Widget _buildProcessStep(String number, String title, String description) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.red.shade700, Colors.red.shade500],
),
shape: BoxShape.circle,
),
child: Center(
child: Text(
number,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Color(0xff2d2d2d),
),
),
Text(
description,
style: const TextStyle(
fontSize: 12,
color: Color(0xff888888),
),
),
],
),
),
],
),
);
}
Widget _buildTechChip(String label, Color color) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: color.withOpacity(0.3),
width: 1,
),
),
child: Text(
label,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: color,
),
),
);
}
}

0
lib/api_service.dart Normal file
View File

38
lib/app_language.dart Normal file
View File

@ -0,0 +1,38 @@
class AppLanguage {
static Map<String, Map<String, String>> translations = {
'en': {
'login': 'Login',
'register': 'Register',
'password': 'Password',
'email': 'Email',
'change_password': 'Change Password',
'language': 'Language',
// 🔥 tambahan
'remember_me': 'Remember Me',
'forgot_password': 'Forgot Password?',
'no_account': "Don't have an account? ",
'login_failed': 'Login Failed',
'wrong_credential': 'Email or password is incorrect',
},
'id': {
'login': 'Masuk',
'register': 'Daftar',
'password': 'Kata Sandi',
'email': 'Email',
'change_password': 'Ganti Password',
'language': 'Bahasa',
// 🔥 tambahan
'remember_me': 'Ingat Saya',
'forgot_password': 'Lupa Password?',
'no_account': "Belum punya akun? ",
'login_failed': 'Login Gagal',
'wrong_credential': 'Email atau password salah',
}
};
static String get(String key, String lang) {
return translations[lang]?[key] ?? key;
}
}

1403
lib/cek_sensor.dart Normal file

File diff suppressed because it is too large Load Diff

481
lib/dashboard.dart Normal file
View File

@ -0,0 +1,481 @@
import 'package:flutter/material.dart';
import 'cek_sensor.dart';
import 'history.dart';
import 'about.dart';
import 'profile.dart';
import 'login_screen.dart';
import 'pengaturan.dart';
import 'package:shared_preferences/shared_preferences.dart';
class DashboardScreen extends StatefulWidget {
const DashboardScreen({super.key});
@override
State<DashboardScreen> createState() => _DashboardScreenState();
}
class _DashboardScreenState extends State<DashboardScreen> {
String name = "User";
String email = "";
String? imageName;
int _imageVersion = 0;
final String baseUrl = "http://192.168.100.9/kopikaocare_api/uploads/";
@override
void initState() {
super.initState();
loadProfile();
}
Future<void> loadProfile() async {
final prefs = await SharedPreferences.getInstance();
setState(() {
name = prefs.getString('name') ?? 'User';
email = prefs.getString('email') ?? '';
imageName = prefs.getString('image');
_imageVersion++;
});
}
String getImageUrl() {
if (imageName == null || imageName!.isEmpty) return '';
return "$baseUrl$imageName?t=${DateTime.now().millisecondsSinceEpoch}";
}
Widget menuCard(
BuildContext context, String title, String gifPath, Widget page) {
return GestureDetector(
onTap: () async {
await Navigator.push(
context,
MaterialPageRoute(builder: (context) => page),
);
await loadProfile();
},
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
blurRadius: 15,
color: Colors.red.withOpacity(0.15),
offset: const Offset(0, 8),
),
],
border: Border.all(
color: Colors.red.withOpacity(0.1),
width: 1,
),
),
padding: const EdgeInsets.all(20),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(gifPath, height: 90),
const SizedBox(height: 15),
Text(
title,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Color(0xff2d2d2d),
),
),
// HAPUS TANDA PANAH DI SINI
],
),
),
);
}
void showLogoutDialog() {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
title: const Text("Logout"),
content: const Text("Apakah anda ingin logout?"),
actions: [
TextButton(
child: const Text("Tidak"),
onPressed: () => Navigator.pop(context),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Text("Ya"),
onPressed: () async {
final prefs = await SharedPreferences.getInstance();
await prefs.clear();
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => const LoginScreen()),
(route) => false,
);
},
),
],
);
},
);
}
Widget profileImage(double radius, {bool withGlow = false}) {
final imageUrl = getImageUrl();
Widget avatar;
if (imageUrl.isNotEmpty) {
avatar = CircleAvatar(
key: ValueKey(_imageVersion),
radius: radius - 2,
backgroundImage: NetworkImage(imageUrl),
onBackgroundImageError: (exception, stackTrace) {
print('❌ Error loading image: $exception');
},
);
} else {
avatar = CircleAvatar(
radius: radius - 2,
backgroundColor: Colors.grey[300],
child: Icon(Icons.person, size: radius, color: Colors.grey[600]),
);
}
if (withGlow) {
return Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
blurRadius: 20,
color: Colors.red.withOpacity(0.4),
offset: const Offset(0, 4),
),
],
border: Border.all(
color: Colors.white,
width: 3,
),
),
child: avatar,
);
}
return avatar;
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xffeeeeee),
appBar: AppBar(
title: const Text(
'Dashboard',
style: TextStyle(
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
),
backgroundColor: Colors.red,
foregroundColor: Colors.white,
elevation: 0,
flexibleSpace: Container(
decoration: const BoxDecoration(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
),
),
),
),
drawer: Drawer(
child: Container(
color: Colors.white,
child: SafeArea(
child: Column(
children: [
Container(
width: double.infinity,
padding:
const EdgeInsets.symmetric(vertical: 40, horizontal: 20),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.red.shade700,
Colors.red.shade500,
],
),
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(30),
bottomRight: Radius.circular(30),
),
),
child: Column(
children: [
profileImage(50, withGlow: true),
const SizedBox(height: 15),
Text(
name,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 0.5,
),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
),
child: Text(
email.isEmpty ? 'Email belum diisi' : email,
style: const TextStyle(
fontSize: 13,
color: Colors.white,
),
),
),
const SizedBox(height: 20),
ElevatedButton.icon(
onPressed: () async {
Navigator.pop(context);
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ProfileScreen()),
);
await loadProfile();
},
icon: const Icon(Icons.edit, size: 18),
label: const Text('Edit Profile'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.red,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
elevation: 0,
),
),
],
),
),
const Spacer(),
Container(
margin: const EdgeInsets.all(20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: Colors.red.withOpacity(0.2),
),
),
child: ListTile(
leading: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child:
const Icon(Icons.logout, color: Colors.red, size: 20),
),
title: const Text(
'Logout',
style: TextStyle(
fontWeight: FontWeight.w600,
color: Color(0xff2d2d2d),
),
),
onTap: () {
Navigator.pop(context);
showLogoutDialog();
},
),
),
const SizedBox(height: 20),
],
),
),
),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
// Welcome Banner Modern dengan Foto Profil
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(25),
boxShadow: [
BoxShadow(
blurRadius: 20,
color: Colors.red.withOpacity(0.1),
offset: const Offset(0, 8),
),
],
border: Border.all(
color: Colors.red.withOpacity(0.1),
width: 1,
),
),
child: Row(
children: [
// Foto Profil dengan efek glow
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
blurRadius: 15,
color: Colors.red.withOpacity(0.3),
offset: const Offset(0, 4),
),
],
),
child: profileImage(35),
),
const SizedBox(width: 15),
// Teks Welcome
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Text(
"Halo, ",
style: TextStyle(
fontSize: 18,
color: Color(0xff666666),
),
),
Text(
name,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Color(0xff2d2d2d),
),
),
const Text(
"!",
style: TextStyle(
fontSize: 18,
color: Color(0xff666666),
),
),
],
),
const SizedBox(height: 4),
const Text(
"Selamat datang di Kopikaocare",
style: TextStyle(
fontSize: 13,
color: Color(0xff888888),
),
),
],
),
),
// Icon futuristik
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(15),
),
child: const Icon(
Icons.rocket_launch_rounded,
color: Colors.red,
size: 24,
),
),
],
),
),
const SizedBox(height: 30),
// Menu Layanan dengan efek futuristik
Container(
padding: const EdgeInsets.symmetric(horizontal: 5),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(Icons.grid_view_rounded,
color: Colors.red, size: 20),
),
const SizedBox(width: 12),
const Text(
"Menu Layanan",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xff2d2d2d),
),
),
const Spacer(),
Container(
height: 2,
width: 30,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.red, Colors.red.withOpacity(0.2)],
),
borderRadius: BorderRadius.circular(2),
),
),
],
),
),
const SizedBox(height: 20),
// Menu Grid dengan efek glassmorphism
Expanded(
child: GridView.count(
crossAxisCount: 2,
crossAxisSpacing: 15,
mainAxisSpacing: 15,
children: [
menuCard(context, "Cek Sensor", "assets/sensor.gif",
const CekSensor()),
menuCard(context, "Riwayat", "assets/history.gif",
const HistoryScreen()),
menuCard(context, "Tentang Aplikasi", "assets/about.gif",
const AboutAppScreen()),
menuCard(context, "Pengaturan", "assets/settings.gif",
const PengaturanScreen()),
],
),
),
],
),
),
),
);
}
}

227
lib/early_screen.dart Normal file
View File

@ -0,0 +1,227 @@
import 'package:flutter/material.dart';
import 'login_screen.dart';
import 'register_screen.dart';
class EarlyScreen extends StatelessWidget {
const EarlyScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage("assets/coffe.jpg"),
fit: BoxFit.cover,
),
),
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withOpacity(0.6),
Colors.black.withOpacity(0.4),
Colors.transparent,
Colors.black.withOpacity(0.3),
Colors.black.withOpacity(0.7),
],
stops: const [0.0, 0.2, 0.5, 0.8, 1.0],
),
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 60),
// Logo atau Brand dengan efek
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(
color: Colors.white.withOpacity(0.3),
width: 1,
),
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.coffee,
color: Colors.white,
size: 24,
),
),
const SizedBox(width: 12),
const Text(
"Kopikaocare",
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 1,
),
),
],
),
),
const Spacer(),
// Main Title
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Welcome to",
style: TextStyle(
fontSize: 16,
color: Colors.white70,
letterSpacing: 2,
),
),
const SizedBox(height: 8),
ShaderMask(
shaderCallback: (bounds) => const LinearGradient(
colors: [Colors.white, Colors.white70],
).createShader(bounds),
child: const Text(
"Kopikaocare",
style: TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 1,
),
),
),
const SizedBox(height: 16),
Container(
width: 60,
height: 3,
color: Colors.white.withOpacity(0.5),
),
const SizedBox(height: 16),
const Text(
"Smart Care for Coffee & Cocoa",
style: TextStyle(
fontSize: 18,
color: Colors.white70,
letterSpacing: 0.5,
),
),
],
),
const SizedBox(height: 40),
// Tombol Sign In
SizedBox(
width: double.infinity,
height: 55,
child: ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const LoginScreen(),
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.red.shade700,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
),
child: const Text(
"Sign In",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
letterSpacing: 1,
),
),
),
),
const SizedBox(height: 16),
// Tombol Create Account
SizedBox(
width: double.infinity,
height: 55,
child: OutlinedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const RegisterScreen(),
),
);
},
style: OutlinedButton.styleFrom(
foregroundColor: Colors.white,
side: BorderSide(
color: Colors.white.withOpacity(0.5),
width: 1.5,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
),
child: const Text(
"Create Account",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
letterSpacing: 1,
),
),
),
),
const SizedBox(height: 30),
// Footer text
Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 30,
height: 1,
color: Colors.white.withOpacity(0.3),
),
const SizedBox(width: 10),
Text(
"Kopi Kesehatan Anda",
style: TextStyle(
fontSize: 12,
color: Colors.white.withOpacity(0.5),
letterSpacing: 1,
),
),
const SizedBox(width: 10),
Container(
width: 30,
height: 1,
color: Colors.white.withOpacity(0.3),
),
],
),
),
const SizedBox(height: 30),
],
),
),
),
),
),
);
}
}

118
lib/forgot_password.dart Normal file
View File

@ -0,0 +1,118 @@
import 'package:flutter/material.dart';
class ForgotPasswordScreen extends StatefulWidget {
const ForgotPasswordScreen({super.key});
@override
State<ForgotPasswordScreen> createState() => _ForgotPasswordScreenState();
}
class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
int step = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Reset Password"),
),
body: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
if (step == 0) ...[
const Text(
"Masukkan nama akun dan Gmail",
style: TextStyle(fontSize: 18),
),
const SizedBox(height: 20),
TextField(
decoration: InputDecoration(
hintText: "Nama Akun",
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
const SizedBox(height: 20),
TextField(
decoration: InputDecoration(
hintText: "Gmail",
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
const SizedBox(height: 30),
ElevatedButton(
onPressed: () {
setState(() {
step = 1;
});
},
child: const Text("Kirim Kode"),
),
],
if (step == 1) ...[
const Text(
"Masukkan kode verifikasi",
style: TextStyle(fontSize: 18),
),
const SizedBox(height: 20),
TextField(
decoration: InputDecoration(
hintText: "Kode Verifikasi",
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
const SizedBox(height: 30),
ElevatedButton(
onPressed: () {
setState(() {
step = 2;
});
},
child: const Text("Verifikasi"),
),
],
if (step == 2) ...[
const Text(
"Buat password baru",
style: TextStyle(fontSize: 18),
),
const SizedBox(height: 20),
TextField(
obscureText: true,
decoration: InputDecoration(
hintText: "Password Baru",
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
const SizedBox(height: 20),
TextField(
obscureText: true,
decoration: InputDecoration(
hintText: "Konfirmasi Password",
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
const SizedBox(height: 30),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text("Simpan Password"),
),
],
],
),
),
);
}
}

268
lib/global_data.dart Normal file
View File

@ -0,0 +1,268 @@
// global_data.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
const String BASE_URL = 'http://192.168.100.9/kopikaocare_api';
List<Map<String, dynamic>> historyGlobal = [];
class HistoryManager {
static const String _keyHistory = 'sensor_history';
static const String _keyIntervalValue = 'interval_value';
static const String _keyIntervalUnit = 'interval_unit';
static const String _keyLastSync = 'last_sync_time';
static const String _keyCurrentRange = 'current_range';
static int intervalValue = 5;
static String intervalUnit = 'detik';
static DateTime? lastSyncTime;
static String currentRange = 'semua';
// Load history from LOCAL cache FIRST, then sync with server
static Future<void> loadHistory(
{String range = 'semua',
int limit = 1000,
bool forceRefresh = false}) async {
currentRange = range;
// First, load from local cache
await _loadHistoryLocal();
// Then, try to sync with server (if forceRefresh or cache is old)
if (forceRefresh || _shouldSyncFromServer()) {
await _syncFromServer(range, limit);
}
}
// Check if should sync from server (every 5 minutes)
static bool _shouldSyncFromServer() {
if (lastSyncTime == null) return true;
final difference = DateTime.now().difference(lastSyncTime!);
return difference.inMinutes >= 5; // Sync every 5 minutes
}
// Load from local SharedPreferences cache
static Future<void> _loadHistoryLocal() async {
final prefs = await SharedPreferences.getInstance();
// Load interval settings
intervalValue = prefs.getInt(_keyIntervalValue) ?? 5;
intervalUnit = prefs.getString(_keyIntervalUnit) ?? 'detik';
// Load last sync time
String? lastSyncStr = prefs.getString(_keyLastSync);
if (lastSyncStr != null) {
lastSyncTime = DateTime.tryParse(lastSyncStr);
}
// Load current range
currentRange = prefs.getString(_keyCurrentRange) ?? 'semua';
// Load history from local cache
String? historyString = prefs.getString(_keyHistory);
if (historyString != null && historyString.isNotEmpty) {
try {
List<dynamic> decoded = jsonDecode(historyString);
historyGlobal = decoded.map((item) {
Map<String, dynamic> map = Map<String, dynamic>.from(item);
if (map['waktu'] is String) {
map['waktu'] = DateTime.parse(map['waktu']);
}
return map;
}).toList();
print('Local cache loaded: ${historyGlobal.length} records');
} catch (e) {
print('Error loading local cache: $e');
historyGlobal = [];
}
} else {
historyGlobal = [];
print('No local cache found');
}
}
// Sync data from server with range filter
static Future<void> _syncFromServer(String range, int limit) async {
try {
print('Syncing from server with range: $range');
final response = await http.get(
Uri.parse('$BASE_URL/get_history.php?range=$range&limit=$limit'),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
if (data['status'] == 'success') {
final serverData =
List<Map<String, dynamic>>.from(data['data']).map((item) {
return {
'suhu': double.parse(item['suhu'].toString()),
'kelembapan': double.parse(item['kelembapan'].toString()),
'berat': double.parse(item['berat'].toString()),
'waktu': DateTime.parse(item['waktu']),
};
}).toList();
// Replace with server data (server is source of truth for range)
historyGlobal = serverData;
await _saveHistoryLocal();
// Save current range
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_keyCurrentRange, range);
lastSyncTime = DateTime.now();
await _saveLastSyncTime();
print(
'Synced from server: ${historyGlobal.length} records (range: $range)');
}
}
} catch (e) {
print('Error syncing from server: $e, using cached data');
}
}
// Save to local SharedPreferences cache (PRIVATE)
static Future<void> _saveHistoryLocal() async {
final prefs = await SharedPreferences.getInstance();
List<Map<String, dynamic>> toSave = historyGlobal.map((item) {
Map<String, dynamic> copy = Map.from(item);
if (copy['waktu'] is DateTime) {
copy['waktu'] = (copy['waktu'] as DateTime).toIso8601String();
}
return copy;
}).toList();
String historyString = jsonEncode(toSave);
await prefs.setString(_keyHistory, historyString);
print('Local cache saved: ${historyGlobal.length} records');
}
// PUBLIC: Save to local SharedPreferences cache (for external use)
static Future<void> saveHistoryLocal() async {
await _saveHistoryLocal();
}
// Save last sync time
static Future<void> _saveLastSyncTime() async {
final prefs = await SharedPreferences.getInstance();
if (lastSyncTime != null) {
await prefs.setString(_keyLastSync, lastSyncTime!.toIso8601String());
}
}
// Save single data to MySQL and local cache
static Future<void> addData(Map<String, dynamic> newData) async {
try {
final response = await http.post(
Uri.parse('$BASE_URL/save_history.php'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'suhu': newData['suhu'],
'kelembapan': newData['kelembapan'],
'berat': newData['berat'],
}),
);
if (response.statusCode == 200) {
final result = jsonDecode(response.body);
if (result['status'] == 'success') {
// Add to local cache
Map<String, dynamic> dataWithTime = {
...newData,
'waktu': DateTime.now(),
};
historyGlobal.insert(0, dataWithTime);
// Limit to 1000 data
if (historyGlobal.length > 1000) {
historyGlobal.removeLast();
}
await _saveHistoryLocal();
print('Data saved to MySQL and local cache');
}
}
} catch (e) {
print('Error saving to MySQL: $e');
}
}
// Delete selected data by index from display list
static Future<void> deleteSelectedData(List<int> indices) async {
if (indices.isEmpty) return;
// Get display data (reversed from historyGlobal)
List<Map<String, dynamic>> displayData = List.from(historyGlobal.reversed);
List<Map<String, dynamic>> toDelete = [];
for (var index in indices) {
if (index < displayData.length) {
toDelete.add(displayData[index]);
}
}
if (toDelete.isEmpty) return;
// Remove from historyGlobal
historyGlobal.removeWhere((item) {
return toDelete.any((deleteItem) =>
deleteItem['waktu'].toString() == item['waktu'].toString());
});
// Save to local cache
await _saveHistoryLocal();
// Also sync deletion to server (optional - you may need to implement server-side deletion)
// For now, we'll just update local and server will sync on next refresh
print('${toDelete.length} data deleted locally');
}
// Delete single data by its id or index
static Future<void> deleteSingleData(
Map<String, dynamic> dataToDelete) async {
historyGlobal.removeWhere((item) {
return item['waktu'].toString() == dataToDelete['waktu'].toString();
});
await _saveHistoryLocal();
print('Single data deleted');
}
// Clear all history from MySQL and local cache
static Future<void> clearHistory() async {
try {
final response = await http.post(
Uri.parse('$BASE_URL/clear_history.php'),
);
if (response.statusCode == 200) {
historyGlobal.clear();
await _saveHistoryLocal();
print('History cleared from MySQL and local cache');
}
} catch (e) {
print('Error clearing history: $e');
}
}
// Update interval settings
static Future<void> updateInterval(int value, String unit) async {
intervalValue = value;
intervalUnit = unit;
final prefs = await SharedPreferences.getInstance();
await prefs.setInt(_keyIntervalValue, value);
await prefs.setString(_keyIntervalUnit, unit);
print('Interval updated: every $value $unit');
}
// Force refresh from server
static Future<void> forceRefresh(
{String range = 'semua', int limit = 1000}) async {
await _syncFromServer(range, limit);
}
}

1771
lib/history.dart Normal file

File diff suppressed because it is too large Load Diff

50
lib/lib/mqtt_service.dart Normal file
View File

@ -0,0 +1,50 @@
import 'dart:convert';
import 'package:mqtt_client/mqtt_client.dart';
import 'package:mqtt_client/mqtt_server_client.dart';
class MQTTService {
static final MQTTService instance = MQTTService._();
MQTTService._();
late MqttServerClient client;
Future connect() async {
client = MqttServerClient(
'192.168.1.27',
'flutter_client',
);
client.port = 1883;
client.keepAlivePeriod = 20;
await client.connect();
client.subscribe(
'kopika/sensor',
MqttQos.atLeastOnce,
);
}
void sendControl(
bool heater,
bool fan,
) {
final builder = MqttClientPayloadBuilder();
builder.addString(
jsonEncode({
"heater": heater ? 1 : 0,
"fan": fan ? 1 : 0,
}),
);
client.publishMessage(
"kopika/control",
MqttQos.atLeastOnce,
builder.payload!,
);
}
}

391
lib/login_screen.dart Normal file
View File

@ -0,0 +1,391 @@
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import 'register_screen.dart';
import 'reset_password_screen.dart';
import 'dashboard.dart';
import 'app_language.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
TextEditingController emailController = TextEditingController();
TextEditingController passwordController = TextEditingController();
bool rememberMe = false;
bool hidePassword = true;
bool _isLoading = false;
String apiUrl = "http://192.168.100.9/kopikaocare_api/login.php";
Future login() async {
if (emailController.text.isEmpty || passwordController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Email dan password harus diisi'),
backgroundColor: Colors.orange,
),
);
return;
}
setState(() => _isLoading = true);
String lang = Localizations.localeOf(context).languageCode;
var response = await http.post(
Uri.parse(apiUrl),
body: {
"email": emailController.text,
"password": passwordController.text
},
);
var data = jsonDecode(response.body);
if (data['status'] == "success") {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('name', data['name']);
await prefs.setString('email', data['email']);
await prefs.setString('image', data['image'] ?? '');
setState(() => _isLoading = false);
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => const DashboardScreen(),
),
);
} else {
setState(() => _isLoading = false);
showDialog(
context: context,
builder: (context) {
return AlertDialog(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
title: const Text(
'Login Gagal',
style: TextStyle(color: Colors.red),
),
content: const Text('Email atau password salah'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
);
},
);
}
}
@override
Widget build(BuildContext context) {
String lang = Localizations.localeOf(context).languageCode;
return Scaffold(
backgroundColor: const Color(0xfff8f9fa),
body: SafeArea(
child: SingleChildScrollView(
child: Column(
children: [
// Header Gradient
Container(
width: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.red.shade700,
Colors.red.shade500,
Colors.red.shade300,
],
),
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(40),
bottomRight: Radius.circular(40),
),
),
child: Column(
children: [
const SizedBox(height: 50),
// Logo Container
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white.withOpacity(0.2),
boxShadow: [
BoxShadow(
blurRadius: 30,
color: Colors.white.withOpacity(0.3),
),
],
),
child: const Icon(
Icons.coffee,
size: 60,
color: Colors.white,
),
),
const SizedBox(height: 20),
const Text(
'Welcome Back!',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 1,
),
),
const SizedBox(height: 10),
Text(
'Sign in to your account',
style: TextStyle(
fontSize: 14,
color: Colors.white.withOpacity(0.8),
),
),
const SizedBox(height: 40),
],
),
),
const SizedBox(height: 30),
// Form Container
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
children: [
// Email Field
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
blurRadius: 10,
color: Colors.black.withOpacity(0.05),
offset: const Offset(0, 5),
),
],
),
child: TextField(
controller: emailController,
style: const TextStyle(fontSize: 16),
decoration: InputDecoration(
hintText: 'Email Address',
prefixIcon: const Icon(Icons.email_outlined,
color: Colors.red),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Colors.white,
contentPadding:
const EdgeInsets.symmetric(vertical: 18),
),
),
),
const SizedBox(height: 20),
// Password Field
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
blurRadius: 10,
color: Colors.black.withOpacity(0.05),
offset: const Offset(0, 5),
),
],
),
child: TextField(
controller: passwordController,
obscureText: hidePassword,
style: const TextStyle(fontSize: 16),
decoration: InputDecoration(
hintText: 'Password',
prefixIcon:
const Icon(Icons.lock_outline, color: Colors.red),
suffixIcon: IconButton(
icon: Icon(
hidePassword
? Icons.visibility_off
: Icons.visibility,
color: Colors.grey,
),
onPressed: () {
setState(() {
hidePassword = !hidePassword;
});
},
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Colors.white,
contentPadding:
const EdgeInsets.symmetric(vertical: 18),
),
),
),
const SizedBox(height: 15),
// Remember & Forgot
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Checkbox(
value: rememberMe,
activeColor: Colors.red,
onChanged: (value) {
setState(() {
rememberMe = value!;
});
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5),
),
),
const Text(
'Remember Me',
style: TextStyle(color: Color(0xff666666)),
),
],
),
TextButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
const ResetPasswordScreen(),
),
);
},
child: const Text(
'Forgot Password?',
style: TextStyle(color: Colors.red),
),
),
],
),
const SizedBox(height: 30),
// Login Button
SizedBox(
width: double.infinity,
height: 55,
child: ElevatedButton(
onPressed: _isLoading ? null : login,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
),
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Text(
'Sign In',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
letterSpacing: 1,
),
),
),
),
const SizedBox(height: 20),
// Divider
Row(
children: [
Expanded(
child: Container(
height: 1,
color: Colors.grey.shade300,
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: Text(
'OR',
style: TextStyle(
color: Colors.grey.shade400,
fontSize: 12,
),
),
),
Expanded(
child: Container(
height: 1,
color: Colors.grey.shade300,
),
),
],
),
const SizedBox(height: 20),
// Register Link
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Don't have an account? ",
style: TextStyle(color: Colors.grey.shade600),
),
GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const RegisterScreen(),
),
);
},
child: const Text(
'Sign Up',
style: TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
),
],
),
],
),
),
const SizedBox(height: 30),
],
),
),
),
);
}
}

18
lib/main.dart Normal file
View File

@ -0,0 +1,18 @@
import 'package:flutter/material.dart';
import 'splash_screen.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: SplashScreen(),
);
}
}

380
lib/pengaturan.dart Normal file
View File

@ -0,0 +1,380 @@
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import 'ver.dart';
import 'privacy_policy.dart';
class PengaturanScreen extends StatefulWidget {
const PengaturanScreen({super.key});
@override
State<PengaturanScreen> createState() => _PengaturanScreenState();
}
class _PengaturanScreenState extends State<PengaturanScreen> {
String apiUrl = "http://192.168.100.9/kopikaocare_api/change_password.php";
void showChangePasswordDialog() {
final oldPassword = TextEditingController();
final newPassword = TextEditingController();
final confirmPassword = TextEditingController();
bool hide1 = true;
bool hide2 = true;
bool hide3 = true;
bool isLoading = false;
showDialog(
context: context,
barrierDismissible: false,
builder: (context) {
return StatefulBuilder(
builder: (context, setStateDialog) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(25),
),
child: Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.white,
Colors.red.shade50,
],
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
shape: BoxShape.circle,
),
child: const Icon(
Icons.lock_reset,
size: 40,
color: Colors.red,
),
),
const SizedBox(height: 16),
const Text(
'Ganti Password',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Color(0xff2d2d2d),
),
),
const SizedBox(height: 8),
const Text(
'Masukkan password lama dan baru',
style: TextStyle(
fontSize: 13,
color: Color(0xff666666),
),
),
const SizedBox(height: 24),
TextField(
controller: oldPassword,
obscureText: hide1,
style: const TextStyle(fontSize: 14),
decoration: InputDecoration(
labelText: 'Password Lama',
prefixIcon:
const Icon(Icons.lock_outline, color: Colors.red),
suffixIcon: IconButton(
icon: Icon(
hide1 ? Icons.visibility_off : Icons.visibility,
color: Colors.grey,
),
onPressed: () => setStateDialog(() => hide1 = !hide1),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Colors.grey.shade50,
),
),
const SizedBox(height: 16),
TextField(
controller: newPassword,
obscureText: hide2,
style: const TextStyle(fontSize: 14),
decoration: InputDecoration(
labelText: 'Password Baru',
prefixIcon:
const Icon(Icons.lock_outline, color: Colors.red),
suffixIcon: IconButton(
icon: Icon(
hide2 ? Icons.visibility_off : Icons.visibility,
color: Colors.grey,
),
onPressed: () => setStateDialog(() => hide2 = !hide2),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Colors.grey.shade50,
),
),
const SizedBox(height: 16),
TextField(
controller: confirmPassword,
obscureText: hide3,
style: const TextStyle(fontSize: 14),
decoration: InputDecoration(
labelText: 'Konfirmasi Password',
prefixIcon:
const Icon(Icons.lock_outline, color: Colors.red),
suffixIcon: IconButton(
icon: Icon(
hide3 ? Icons.visibility_off : Icons.visibility,
color: Colors.grey,
),
onPressed: () => setStateDialog(() => hide3 = !hide3),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Colors.grey.shade50,
),
),
const SizedBox(height: 24),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => Navigator.pop(context),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.grey,
side: BorderSide(color: Colors.grey.shade300),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Text('Batal'),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: isLoading
? null
: () async {
if (newPassword.text !=
confirmPassword.text) {
ScaffoldMessenger.of(context)
.showSnackBar(
const SnackBar(
content:
Text('Password baru tidak cocok'),
backgroundColor: Colors.orange,
),
);
return;
}
setStateDialog(() => isLoading = true);
final prefs =
await SharedPreferences.getInstance();
String email =
prefs.getString('email') ?? '';
var response = await http.post(
Uri.parse(apiUrl),
body: {
"email": email,
"old_password": oldPassword.text,
"new_password": newPassword.text,
},
);
setStateDialog(() => isLoading = false);
if (response.body.trim() == "success") {
Navigator.pop(context);
ScaffoldMessenger.of(context)
.showSnackBar(
const SnackBar(
content:
Text('Password berhasil diubah'),
backgroundColor: Colors.green,
),
);
} else {
ScaffoldMessenger.of(context)
.showSnackBar(
const SnackBar(
content: Text('Password lama salah'),
backgroundColor: Colors.red,
),
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Text('Simpan'),
),
),
],
),
],
),
),
);
},
);
},
);
}
Widget menuItem(String title, IconData icon, VoidCallback onTap,
{Color color = Colors.red}) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
blurRadius: 10,
color: Colors.red.withOpacity(0.05),
offset: const Offset(0, 4),
),
],
),
child: ListTile(
leading: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(15),
),
child: Icon(icon, color: color, size: 24),
),
title: Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Color(0xff2d2d2d),
),
),
trailing: Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child:
const Icon(Icons.arrow_forward_ios, size: 14, color: Colors.red),
),
onTap: onTap,
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xfff8f9fa),
appBar: AppBar(
title: const Text(
'Pengaturan',
style: TextStyle(fontWeight: FontWeight.bold),
),
backgroundColor: Colors.red,
foregroundColor: Colors.white,
elevation: 0,
),
body: Column(
children: [
const SizedBox(height: 20),
Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.red.shade50, Colors.white],
),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.red.withOpacity(0.1)),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(15),
),
child:
const Icon(Icons.settings, color: Colors.white, size: 24),
),
const SizedBox(width: 12),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Pengaturan Aplikasi',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xff2d2d2d),
),
),
Text(
'Atur preferensi aplikasi Anda',
style:
TextStyle(fontSize: 12, color: Color(0xff888888)),
),
],
),
),
],
),
),
const SizedBox(height: 10),
menuItem("Ganti Password", Icons.lock, showChangePasswordDialog),
menuItem("Versi Aplikasi", Icons.info_outline, () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const VerScreen()),
);
}),
menuItem("Privacy Policy", Icons.privacy_tip, () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const PrivacyPolicyScreen()),
);
}),
],
),
);
}
}

196
lib/privacy_policy.dart Normal file
View File

@ -0,0 +1,196 @@
import 'package:flutter/material.dart';
class PrivacyPolicyScreen extends StatelessWidget {
const PrivacyPolicyScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xfff8f9fa),
appBar: AppBar(
title: const Text(
'Privacy Policy',
style: TextStyle(fontWeight: FontWeight.bold),
),
backgroundColor: Colors.red,
foregroundColor: Colors.white,
elevation: 0,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
children: [
// Header
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Colors.red.shade50, Colors.white],
),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.red.withOpacity(0.1)),
),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
shape: BoxShape.circle,
),
child: const Icon(
Icons.privacy_tip,
size: 40,
color: Colors.red,
),
),
const SizedBox(height: 12),
const Text(
'Kebijakan Privasi',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Color(0xff2d2d2d),
),
),
const SizedBox(height: 4),
Text(
'Terakhir diperbarui: 2026',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade500,
),
),
],
),
),
const SizedBox(height: 20),
// Content Cards
_buildPolicyCard(
icon: Icons.data_usage,
title: 'Pengumpulan Data',
description:
'Aplikasi ini mengumpulkan data seperti email, nama, dan gambar profil '
'untuk keperluan autentikasi dan personalisasi pengguna.',
),
_buildPolicyCard(
icon: Icons.shield,
title: 'Keamanan Data',
description:
'Data Anda aman dan tidak akan dibagikan kepada pihak ketiga '
'tanpa izin pengguna. Kami menggunakan enkripsi untuk melindungi data Anda.',
),
_buildPolicyCard(
icon: Icons.visibility,
title: 'Penggunaan Data',
description: 'Data yang dikumpulkan hanya digunakan untuk: '
'\n• Autentikasi pengguna\n• Personalisasi profil\n• Peningkatan layanan',
),
_buildPolicyCard(
icon: Icons.check_circle,
title: 'Persetujuan Pengguna',
description:
'Dengan menggunakan aplikasi ini, pengguna menyetujui '
'kebijakan privasi yang telah ditetapkan.',
),
_buildPolicyCard(
icon: Icons.contact_support,
title: 'Kontak',
description: 'Jika ada pertanyaan tentang kebijakan privasi, '
'silakan hubungi kami di support@kopicare.com',
),
const SizedBox(height: 20),
// Footer
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(15),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.security, color: Colors.red, size: 16),
const SizedBox(width: 8),
Text(
'Data Anda aman bersama kami',
style: TextStyle(
fontSize: 12,
color: Colors.red.shade700,
),
),
],
),
),
],
),
),
);
}
Widget _buildPolicyCard({
required IconData icon,
required String title,
required String description,
}) {
return Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
blurRadius: 10,
color: Colors.red.withOpacity(0.05),
offset: const Offset(0, 4),
),
],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(15),
),
child: Icon(icon, color: Colors.red, size: 22),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xff2d2d2d),
),
),
const SizedBox(height: 6),
Text(
description,
style: const TextStyle(
fontSize: 13,
color: Color(0xff666666),
height: 1.4,
),
),
],
),
),
],
),
);
}
}

659
lib/profile.dart Normal file
View File

@ -0,0 +1,659 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:http/http.dart' as http;
class ProfileScreen extends StatefulWidget {
const ProfileScreen({super.key});
@override
State<ProfileScreen> createState() => _ProfileScreenState();
}
class _ProfileScreenState extends State<ProfileScreen> {
final TextEditingController nameController = TextEditingController();
final TextEditingController emailController = TextEditingController();
String? imageName;
bool _isLoading = false;
int _refreshKey = 0;
final String baseUrl = "http://192.168.100.9/kopikaocare_api/uploads/";
final String apiUpload =
"http://192.168.100.9/kopikaocare_api/upload_image.php";
final String apiUpdate =
"http://192.168.100.9/kopikaocare_api/update_profile.php";
@override
void initState() {
super.initState();
loadProfile();
}
Future<void> loadProfile() async {
final prefs = await SharedPreferences.getInstance();
setState(() {
nameController.text = prefs.getString('name') ?? '';
emailController.text = prefs.getString('email') ?? '';
imageName = prefs.getString('image');
_refreshKey++;
});
}
String getImageUrl() {
if (imageName == null || imageName!.isEmpty) return '';
return "$baseUrl$imageName?t=${DateTime.now().millisecondsSinceEpoch}";
}
Future<void> pickImage() async {
final picker = ImagePicker();
final pickedFile = await picker.pickImage(source: ImageSource.gallery);
if (pickedFile != null) {
setState(() => _isLoading = true);
try {
final bytes = await pickedFile.readAsBytes();
final base64Image = base64Encode(bytes);
final prefs = await SharedPreferences.getInstance();
final email = prefs.getString('email') ?? '';
final response = await http.post(
Uri.parse(apiUpload),
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: {
'email': email,
'image': base64Image,
},
);
final data = jsonDecode(response.body);
if (data['status'] == 'success') {
await prefs.setString('image', data['image']);
setState(() {
imageName = data['image'];
_isLoading = false;
_refreshKey++;
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Foto berhasil diupload'),
backgroundColor: Colors.green,
),
);
}
} else {
setState(() => _isLoading = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Gagal: ${data['message']}'),
backgroundColor: Colors.red,
),
);
}
} catch (e) {
setState(() => _isLoading = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red),
);
}
}
}
Future<void> removeImage() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('image');
setState(() {
imageName = null;
_refreshKey++;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Foto dihapus'),
backgroundColor: Colors.orange,
),
);
}
Future<void> saveChanges(String newName, String newEmail) async {
final prefs = await SharedPreferences.getInstance();
final currentName = prefs.getString('name') ?? '';
final currentEmail = prefs.getString('email') ?? '';
bool nameChanged = currentName != newName;
bool emailChanged = currentEmail != newEmail;
if (!nameChanged && !emailChanged) {
return;
}
setState(() => _isLoading = true);
try {
final response = await http.post(
Uri.parse(apiUpdate),
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: {
'old_email': currentEmail,
'name': newName,
'email': newEmail,
},
);
final data = jsonDecode(response.body);
if (data['status'] == 'success') {
if (nameChanged) await prefs.setString('name', newName);
if (emailChanged) await prefs.setString('email', newEmail);
setState(() {
_isLoading = false;
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Data berhasil diupdate'),
backgroundColor: Colors.green,
),
);
}
} else {
setState(() => _isLoading = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Gagal: ${data['message']}'),
backgroundColor: Colors.red,
),
);
}
} catch (e) {
setState(() => _isLoading = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red),
);
}
}
void showEditDialog() {
final TextEditingController tempNameCtrl =
TextEditingController(text: nameController.text);
final TextEditingController tempEmailCtrl =
TextEditingController(text: emailController.text);
showDialog(
context: context,
barrierDismissible: false,
builder: (context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(25),
),
child: Container(
padding: const EdgeInsets.all(25),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.white,
Colors.red.shade50,
],
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
shape: BoxShape.circle,
),
child:
const Icon(Icons.edit_note, color: Colors.red, size: 32),
),
const SizedBox(height: 20),
const Text(
'Edit Profile',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Color(0xff2d2d2d),
),
),
const SizedBox(height: 10),
const Text(
'Ubah informasi profil Anda',
style: TextStyle(
fontSize: 14,
color: Color(0xff666666),
),
),
const SizedBox(height: 25),
TextField(
controller: tempNameCtrl,
decoration: InputDecoration(
labelText: 'Nama Lengkap',
prefixIcon:
const Icon(Icons.person_outline, color: Colors.red),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: const BorderSide(color: Colors.red, width: 2),
),
),
),
const SizedBox(height: 15),
TextField(
controller: tempEmailCtrl,
decoration: InputDecoration(
labelText: 'Email',
prefixIcon:
const Icon(Icons.email_outlined, color: Colors.red),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: const BorderSide(color: Colors.red, width: 2),
),
),
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: 25),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => Navigator.pop(context),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.grey,
side: BorderSide(color: Colors.grey.shade300),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Text('Batal'),
),
),
const SizedBox(width: 15),
Expanded(
child: ElevatedButton(
onPressed: () async {
Navigator.pop(context);
await saveChanges(tempNameCtrl.text.trim(),
tempEmailCtrl.text.trim());
await loadProfile();
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Text('Simpan'),
),
),
],
),
],
),
),
);
},
);
}
@override
Widget build(BuildContext context) {
final imageUrl = getImageUrl();
return Scaffold(
backgroundColor: const Color(0xfff8f9fa),
appBar: AppBar(
title: const Text(
'Profile',
style: TextStyle(
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
),
backgroundColor: Colors.red,
foregroundColor: Colors.white,
elevation: 0,
actions: [
Container(
margin: const EdgeInsets.only(right: 10),
child: IconButton(
icon: const Icon(Icons.edit_note),
onPressed: showEditDialog,
tooltip: 'Edit Nama & Email',
style: IconButton.styleFrom(
backgroundColor: Colors.white.withOpacity(0.2),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
],
),
body: SingleChildScrollView(
child: Column(
children: [
// Header Gradient
Container(
width: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.red.shade700,
Colors.red.shade500,
Colors.red.shade300,
],
),
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(40),
bottomRight: Radius.circular(40),
),
),
child: Column(
children: [
const SizedBox(height: 30),
// Foto Profil dengan efek glow
GestureDetector(
onTap: pickImage,
child: Stack(
children: [
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
blurRadius: 30,
color: Colors.white.withOpacity(0.5),
spreadRadius: 5,
),
],
),
child: CircleAvatar(
key: ValueKey(_refreshKey),
radius: 70,
backgroundColor: Colors.white,
backgroundImage: imageUrl.isNotEmpty
? NetworkImage(imageUrl)
: null,
child: imageUrl.isEmpty
? const Icon(Icons.person,
size: 70, color: Colors.red)
: null,
),
),
if (_isLoading)
const Positioned.fill(
child: CircleAvatar(
radius: 70,
backgroundColor: Colors.black54,
child: CircularProgressIndicator(
color: Colors.white),
),
),
Positioned(
bottom: 5,
right: 5,
child: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
blurRadius: 10,
color: Colors.black.withOpacity(0.2),
),
],
),
child: const Icon(Icons.camera_alt,
size: 22, color: Colors.red),
),
),
],
),
),
const SizedBox(height: 20),
// Nama
Text(
nameController.text.isEmpty
? 'Belum diisi'
: nameController.text,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 1,
),
),
const SizedBox(height: 8),
// Email
Container(
padding:
const EdgeInsets.symmetric(horizontal: 15, vertical: 6),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
),
child: Text(
emailController.text.isEmpty
? 'Email belum diisi'
: emailController.text,
style: const TextStyle(
fontSize: 14,
color: Colors.white,
),
),
),
const SizedBox(height: 30),
// Tombol Hapus Foto
if (imageName != null)
TextButton.icon(
onPressed: removeImage,
icon: const Icon(Icons.delete_outline, size: 18),
label: const Text('Hapus Foto'),
style: TextButton.styleFrom(
foregroundColor: Colors.white,
),
),
const SizedBox(height: 10),
],
),
),
const SizedBox(height: 30),
// Info Card
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Informasi Akun',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xff2d2d2d),
),
),
const SizedBox(height: 15),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
blurRadius: 15,
color: Colors.black.withOpacity(0.05),
offset: const Offset(0, 5),
),
],
),
child: Column(
children: [
// Nama Item
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
border: Border(
bottom: BorderSide(
color: Colors.grey.shade200,
width: 1,
),
),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(15),
),
child: const Icon(Icons.person,
color: Colors.red, size: 24),
),
const SizedBox(width: 15),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Nama Lengkap',
style: TextStyle(
fontSize: 12,
color: Color(0xff888888),
),
),
const SizedBox(height: 4),
Text(
nameController.text.isEmpty
? 'Belum diisi'
: nameController.text,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xff2d2d2d),
),
),
],
),
),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(Icons.verified,
color: Colors.red, size: 16),
),
],
),
),
// Email Item
Container(
padding: const EdgeInsets.all(20),
decoration: const BoxDecoration(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(15),
),
child: const Icon(Icons.email,
color: Colors.red, size: 24),
),
const SizedBox(width: 15),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Alamat Email',
style: TextStyle(
fontSize: 12,
color: Color(0xff888888),
),
),
const SizedBox(height: 4),
Text(
emailController.text.isEmpty
? 'Belum diisi'
: emailController.text,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xff2d2d2d),
),
),
],
),
),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(Icons.check_circle,
color: Colors.green, size: 16),
),
],
),
),
],
),
),
],
),
),
const SizedBox(height: 30),
],
),
),
);
}
}

377
lib/register_screen.dart Normal file
View File

@ -0,0 +1,377 @@
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import 'login_screen.dart';
class RegisterScreen extends StatefulWidget {
const RegisterScreen({super.key});
@override
State<RegisterScreen> createState() => _RegisterScreenState();
}
class _RegisterScreenState extends State<RegisterScreen> {
TextEditingController nameController = TextEditingController();
TextEditingController emailController = TextEditingController();
TextEditingController passwordController = TextEditingController();
TextEditingController confirmPasswordController = TextEditingController();
bool _isLoading = false;
bool _hidePassword = true;
bool _hideConfirmPassword = true;
String apiUrl = "http://192.168.100.9/kopikaocare_api/register.php";
Future register() async {
if (nameController.text.isEmpty ||
emailController.text.isEmpty ||
passwordController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Semua field harus diisi'),
backgroundColor: Colors.orange,
),
);
return;
}
if (passwordController.text != confirmPasswordController.text) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Password tidak sama'),
backgroundColor: Colors.red,
),
);
return;
}
setState(() => _isLoading = true);
var response = await http.post(
Uri.parse(apiUrl),
body: {
"name": nameController.text,
"email": emailController.text,
"password": passwordController.text
},
);
var data = response.body.trim();
setState(() => _isLoading = false);
if (data == "success") {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('name', nameController.text);
await prefs.setString('email', emailController.text);
showDialog(
context: context,
barrierDismissible: false,
builder: (context) {
return AlertDialog(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
title: const Text(
'Berhasil!',
style: TextStyle(color: Colors.green),
),
content: const Text('Selamat anda berhasil mendaftar'),
actions: [
TextButton(
onPressed: () {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => const LoginScreen(),
),
);
},
child: const Text('OK'),
),
],
);
},
);
} else {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
title: const Text(
'Gagal',
style: TextStyle(color: Colors.red),
),
content: Text(
'Register gagal: ${data == "error" ? "Email sudah terdaftar" : "Coba lagi"}'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
);
},
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xfff8f9fa),
appBar: AppBar(
title: const Text(
'Create Account',
style: TextStyle(fontWeight: FontWeight.bold),
),
backgroundColor: Colors.transparent,
foregroundColor: Colors.red,
elevation: 0,
),
body: SafeArea(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
children: [
// Header Icon
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [Colors.red.shade100, Colors.red.shade50],
),
),
child: const Icon(
Icons.person_add_alt_rounded,
size: 60,
color: Colors.red,
),
),
const SizedBox(height: 20),
const Text(
'Join Us Now!',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Color(0xff2d2d2d),
),
),
const SizedBox(height: 8),
Text(
'Create your account to get started',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
),
const SizedBox(height: 40),
// Form Fields
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
blurRadius: 10,
color: Colors.black.withOpacity(0.05),
offset: const Offset(0, 5),
),
],
),
child: Column(
children: [
// Name Field
TextField(
controller: nameController,
style: const TextStyle(fontSize: 16),
decoration: InputDecoration(
hintText: 'Full Name',
prefixIcon: const Icon(Icons.person_outline,
color: Colors.red),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Colors.white,
contentPadding:
const EdgeInsets.symmetric(vertical: 18),
),
),
Divider(
height: 1,
color: Colors.grey.shade200,
),
// Email Field
TextField(
controller: emailController,
style: const TextStyle(fontSize: 16),
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
hintText: 'Email Address',
prefixIcon: const Icon(Icons.email_outlined,
color: Colors.red),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Colors.white,
contentPadding:
const EdgeInsets.symmetric(vertical: 18),
),
),
Divider(
height: 1,
color: Colors.grey.shade200,
),
// Password Field
TextField(
controller: passwordController,
obscureText: _hidePassword,
style: const TextStyle(fontSize: 16),
decoration: InputDecoration(
hintText: 'Password',
prefixIcon:
const Icon(Icons.lock_outline, color: Colors.red),
suffixIcon: IconButton(
icon: Icon(
_hidePassword
? Icons.visibility_off
: Icons.visibility,
color: Colors.grey,
),
onPressed: () {
setState(() {
_hidePassword = !_hidePassword;
});
},
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Colors.white,
contentPadding:
const EdgeInsets.symmetric(vertical: 18),
),
),
Divider(
height: 1,
color: Colors.grey.shade200,
),
// Confirm Password Field
TextField(
controller: confirmPasswordController,
obscureText: _hideConfirmPassword,
style: const TextStyle(fontSize: 16),
decoration: InputDecoration(
hintText: 'Confirm Password',
prefixIcon:
const Icon(Icons.lock_outline, color: Colors.red),
suffixIcon: IconButton(
icon: Icon(
_hideConfirmPassword
? Icons.visibility_off
: Icons.visibility,
color: Colors.grey,
),
onPressed: () {
setState(() {
_hideConfirmPassword = !_hideConfirmPassword;
});
},
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Colors.white,
contentPadding:
const EdgeInsets.symmetric(vertical: 18),
),
),
],
),
),
const SizedBox(height: 30),
// Register Button
SizedBox(
width: double.infinity,
height: 55,
child: ElevatedButton(
onPressed: _isLoading ? null : register,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
),
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Text(
'Sign Up',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
letterSpacing: 1,
),
),
),
),
const SizedBox(height: 20),
// Login Link
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Already have an account? ",
style: TextStyle(color: Colors.grey.shade600),
),
GestureDetector(
onTap: () {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => const LoginScreen(),
),
);
},
child: const Text(
'Sign In',
style: TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
),
],
),
],
),
),
),
),
);
}
}

View File

@ -0,0 +1,409 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
class ResetPasswordScreen extends StatefulWidget {
const ResetPasswordScreen({super.key});
@override
State<ResetPasswordScreen> createState() => _ResetPasswordScreenState();
}
class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
final TextEditingController nameController = TextEditingController();
final TextEditingController emailController = TextEditingController();
final TextEditingController passwordController = TextEditingController();
bool showNewPassword = false;
bool _isLoading = false;
bool _hidePassword = true;
String apiUrl = "http://192.168.100.9/kopikaocare_api/reset_password.php";
Future<void> resetPassword() async {
if (passwordController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Password baru tidak boleh kosong'),
backgroundColor: Colors.orange,
),
);
return;
}
setState(() => _isLoading = true);
var response = await http.post(
Uri.parse(apiUrl),
body: {
"name": nameController.text,
"email": emailController.text,
"password": passwordController.text,
},
);
var data = jsonDecode(response.body);
setState(() => _isLoading = false);
if (data['status'] == "success") {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) {
return AlertDialog(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
title: const Row(
children: [
Icon(Icons.check_circle, color: Colors.green, size: 28),
SizedBox(width: 10),
Text(
'Berhasil!',
style: TextStyle(color: Colors.green),
),
],
),
content: const Text('Password berhasil diubah'),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context); // close dialog
Navigator.pop(context); // balik ke login
},
child: const Text('OK'),
),
],
);
},
);
} else {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
title: const Row(
children: [
Icon(Icons.error, color: Colors.red, size: 28),
SizedBox(width: 10),
Text(
'Gagal!',
style: TextStyle(color: Colors.red),
),
],
),
content: const Text('Nama atau email tidak cocok'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
);
},
);
}
}
void verifyUser() {
if (nameController.text.isEmpty || emailController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Nama dan email harus diisi'),
backgroundColor: Colors.orange,
),
);
return;
}
setState(() {
showNewPassword = true;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xfff8f9fa),
appBar: AppBar(
title: const Text(
'Reset Password',
style: TextStyle(fontWeight: FontWeight.bold),
),
backgroundColor: Colors.transparent,
foregroundColor: Colors.red,
elevation: 0,
),
body: SafeArea(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
children: [
// Header Icon
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [Colors.red.shade100, Colors.red.shade50],
),
),
child: Icon(
showNewPassword ? Icons.lock_reset : Icons.lock_outline,
size: 60,
color: Colors.red,
),
),
const SizedBox(height: 20),
Text(
showNewPassword
? 'Create New Password'
: 'Verify Your Identity',
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Color(0xff2d2d2d),
),
),
const SizedBox(height: 8),
Text(
showNewPassword
? 'Enter your new password below'
: 'Enter your name and email to reset password',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 40),
// Form Container
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(25),
boxShadow: [
BoxShadow(
blurRadius: 15,
color: Colors.black.withOpacity(0.05),
offset: const Offset(0, 5),
),
],
),
child: Column(
children: [
// Step 1: Verify Identity
if (!showNewPassword) ...[
// Name Field
TextField(
controller: nameController,
style: const TextStyle(fontSize: 16),
decoration: InputDecoration(
hintText: 'Full Name',
prefixIcon: const Icon(Icons.person_outline,
color: Colors.red),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Colors.white,
contentPadding:
const EdgeInsets.symmetric(vertical: 18),
),
),
Divider(
height: 1,
color: Colors.grey.shade200,
),
// Email Field
TextField(
controller: emailController,
style: const TextStyle(fontSize: 16),
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
hintText: 'Email Address',
prefixIcon: const Icon(Icons.email_outlined,
color: Colors.red),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Colors.white,
contentPadding:
const EdgeInsets.symmetric(vertical: 18),
),
),
],
// Step 2: New Password
if (showNewPassword) ...[
// New Password Field
TextField(
controller: passwordController,
obscureText: _hidePassword,
style: const TextStyle(fontSize: 16),
decoration: InputDecoration(
hintText: 'New Password',
prefixIcon: const Icon(Icons.lock_outline,
color: Colors.red),
suffixIcon: IconButton(
icon: Icon(
_hidePassword
? Icons.visibility_off
: Icons.visibility,
color: Colors.grey,
),
onPressed: () {
setState(() {
_hidePassword = !_hidePassword;
});
},
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Colors.white,
contentPadding:
const EdgeInsets.symmetric(vertical: 18),
),
),
// Password Strength Indicator
if (passwordController.text.isNotEmpty) ...[
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child:
_buildPasswordStrength(passwordController.text),
),
],
],
],
),
),
const SizedBox(height: 30),
// Action Button
SizedBox(
width: double.infinity,
height: 55,
child: ElevatedButton(
onPressed: _isLoading
? null
: (showNewPassword ? resetPassword : verifyUser),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
),
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: Text(
showNewPassword ? 'Save Password' : 'Verify',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
letterSpacing: 1,
),
),
),
),
// Back Button (only on step 2)
if (showNewPassword) ...[
const SizedBox(height: 15),
TextButton(
onPressed: () {
setState(() {
showNewPassword = false;
passwordController.clear();
});
},
child: const Text(
'Back',
style: TextStyle(color: Colors.grey),
),
),
],
const SizedBox(height: 20),
],
),
),
),
),
);
}
Widget _buildPasswordStrength(String password) {
int strength = _calculatePasswordStrength(password);
String strengthText = '';
Color strengthColor = Colors.red;
if (strength <= 2) {
strengthText = 'Weak';
strengthColor = Colors.red;
} else if (strength <= 4) {
strengthText = 'Medium';
strengthColor = Colors.orange;
} else {
strengthText = 'Strong';
strengthColor = Colors.green;
}
return Row(
children: [
Expanded(
child: LinearProgressIndicator(
value: strength / 6,
backgroundColor: Colors.grey.shade200,
valueColor: AlwaysStoppedAnimation<Color>(strengthColor),
minHeight: 4,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 10),
Text(
strengthText,
style: TextStyle(
fontSize: 12,
color: strengthColor,
fontWeight: FontWeight.w500,
),
),
],
);
}
int _calculatePasswordStrength(String password) {
int strength = 0;
if (password.length >= 6) strength++;
if (password.length >= 10) strength++;
if (password.contains(RegExp(r'[A-Z]'))) strength++;
if (password.contains(RegExp(r'[a-z]'))) strength++;
if (password.contains(RegExp(r'[0-9]'))) strength++;
if (password.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]'))) strength++;
return strength;
}
}

203
lib/splash_screen.dart Normal file
View File

@ -0,0 +1,203 @@
import 'package:flutter/material.dart';
import 'dart:async';
import 'early_screen.dart';
class SplashScreen extends StatefulWidget {
const SplashScreen({super.key});
@override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
double _loadingProgress = 0.0;
Timer? _timer;
Timer? _navigationTimer;
@override
void initState() {
super.initState();
// Loading bar animation dari 0% ke 100% dalam 3 detik
_timer = Timer.periodic(const Duration(milliseconds: 30), (timer) {
setState(() {
if (_loadingProgress < 1.0) {
_loadingProgress += 0.01;
} else {
_timer?.cancel();
}
});
});
// Navigate setelah 3.5 detik
_navigationTimer = Timer(const Duration(milliseconds: 3500), () {
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => const EarlyScreen(),
),
);
}
});
}
@override
void dispose() {
_timer?.cancel();
_navigationTimer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.red.shade900, // Merah sangat tua
Colors.red.shade800, // Merah tua
Colors.red.shade700, // Merah gelap
Colors.red.shade600, // Merah sedang
Colors.red.shade500, // Merah terang
Colors.white.withOpacity(0.95), // Putih sedikit di paling bawah
],
stops: const [0.0, 0.2, 0.4, 0.6, 0.8, 1.0],
),
),
child: SafeArea(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Logo dengan efek glow merah
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
blurRadius: 40,
color: Colors.red.shade300.withOpacity(0.6),
spreadRadius: 5,
),
],
),
child: Image.asset(
"assets/logo.png",
width: 150,
height: 150,
),
),
const SizedBox(height: 40),
// Nama Aplikasi
ShaderMask(
shaderCallback: (bounds) => LinearGradient(
colors: [Colors.white, Colors.red.shade100],
).createShader(bounds),
child: const Text(
'Kopikaocare',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 3,
),
),
),
const SizedBox(height: 10),
const Text(
'Kopi Kesehatan Anda',
style: TextStyle(
fontSize: 14,
color: Colors.white70,
letterSpacing: 1.5,
),
),
const SizedBox(height: 80),
// Loading Bar Container
Padding(
padding: const EdgeInsets.symmetric(horizontal: 60),
child: Column(
children: [
// Loading Bar Background
Container(
width: double.infinity,
height: 6,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(10),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: LinearProgressIndicator(
value: _loadingProgress,
backgroundColor: Colors.transparent,
valueColor: const AlwaysStoppedAnimation<Color>(
Colors.white,
),
),
),
),
const SizedBox(height: 12),
// Persentase Loading
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Memuat...',
style: TextStyle(
fontSize: 12,
color: Colors.white70,
letterSpacing: 0.5,
),
),
Text(
'${(_loadingProgress * 100).toInt()}%',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 0.5,
),
),
],
),
],
),
),
const SizedBox(height: 30),
// Loading dots
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildDot(0),
_buildDot(1),
_buildDot(2),
],
),
],
),
),
),
),
);
}
Widget _buildDot(int index) {
return AnimatedContainer(
duration: const Duration(milliseconds: 500),
margin: const EdgeInsets.symmetric(horizontal: 4),
width: 6,
height: 6,
decoration: BoxDecoration(
color: Colors.white.withOpacity(
_loadingProgress > 0.3 * (index + 1) ? 1.0 : 0.2,
),
shape: BoxShape.circle,
),
);
}
}

7
lib/user_data.dart Normal file
View File

@ -0,0 +1,7 @@
import 'package:flutter/material.dart';
class UserData {
static ValueNotifier<String> name = ValueNotifier("Deni");
static ValueNotifier<String> email = ValueNotifier("deni@gmail.com");
static ValueNotifier<String> imagePath = ValueNotifier("assets/profile.jpg");
}

204
lib/ver.dart Normal file
View File

@ -0,0 +1,204 @@
import 'package:flutter/material.dart';
class VerScreen extends StatelessWidget {
const VerScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xfff8f9fa),
appBar: AppBar(
title: const Text(
'Versi Aplikasi',
style: TextStyle(fontWeight: FontWeight.bold),
),
backgroundColor: Colors.red,
foregroundColor: Colors.white,
elevation: 0,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
children: [
// Header Icon
Container(
width: double.infinity,
padding: const EdgeInsets.all(30),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.red.shade700,
Colors.red.shade500,
Colors.red.shade300,
],
),
borderRadius: BorderRadius.circular(25),
boxShadow: [
BoxShadow(
blurRadius: 20,
color: Colors.red.withOpacity(0.3),
),
],
),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
shape: BoxShape.circle,
),
child: const Icon(
Icons.coffee,
size: 60,
color: Colors.white,
),
),
const SizedBox(height: 20),
const Text(
'KopiCare',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 1,
),
),
const SizedBox(height: 5),
Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
),
child: const Text(
'Version 2.0.0',
style: TextStyle(
fontSize: 14,
color: Colors.white,
),
),
),
],
),
),
const SizedBox(height: 25),
// Info Card
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
blurRadius: 15,
color: Colors.red.withOpacity(0.08),
offset: const Offset(0, 5),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Tentang Aplikasi',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xff2d2d2d),
),
),
const SizedBox(height: 10),
const Text(
'Aplikasi ini digunakan untuk monitoring dan pengeringan kopi & kakao berbasis IoT (ESP32).',
style: TextStyle(
fontSize: 14,
color: Color(0xff666666),
height: 1.5,
),
),
const SizedBox(height: 20),
const Text(
'Fitur Unggulan',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xff2d2d2d),
),
),
const SizedBox(height: 10),
_buildFeature('Monitoring Real-time', Icons.thermostat),
_buildFeature('Kontrol Otomatis', Icons.smart_toy),
_buildFeature('Riwayat Data', Icons.history),
_buildFeature('Analisis Pengeringan', Icons.analytics),
],
),
),
const SizedBox(height: 20),
// Footer
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15),
border: Border.all(color: Colors.red.withOpacity(0.1)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
shape: BoxShape.circle,
),
child:
const Icon(Icons.favorite, color: Colors.red, size: 16),
),
const SizedBox(width: 8),
const Text(
'© 2026 KopiCare Team',
style: TextStyle(
fontSize: 12,
color: Color(0xff888888),
),
),
],
),
),
],
),
),
);
}
Widget _buildFeature(String title, IconData icon) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: Colors.red, size: 18),
),
const SizedBox(width: 10),
Text(
title,
style: const TextStyle(
fontSize: 13,
color: Color(0xff555555),
),
),
],
),
);
}
}

1
linux/.gitignore vendored Normal file
View File

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

145
linux/CMakeLists.txt Normal file
View File

@ -0,0 +1,145 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.10)
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 "kopikaocare")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.example.kopikaocare")
# 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)
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
# Define the application target. To change its name, change BINARY_NAME above,
# not the value here, or `flutter run` will no longer work.
#
# Any new source files that you add to the application should be added here.
add_executable(${BINARY_NAME}
"main.cc"
"my_application.cc"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
)
# Apply the standard set of build settings. This can be removed for applications
# that need different build settings.
apply_standard_settings(${BINARY_NAME})
# Add dependency libraries. Add any application-specific dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
# 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,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <file_selector_linux/file_selector_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
}

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