first commit

This commit is contained in:
pradifa 2025-07-18 14:50:04 +07:00
commit 0bcd9c4d21
165 changed files with 10557 additions and 0 deletions

7
.firebaserc Normal file
View File

@ -0,0 +1,7 @@
{
"projects": {
"default": "coba-smartmedibox"
},
"targets": {},
"etags": {}
}

45
.gitignore vendored Normal file
View File

@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.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: "ea121f8859e4b13e47a8f845e4586164519588bc"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
- platform: android
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
- platform: ios
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
- platform: linux
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
- platform: macos
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
- platform: web
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
- platform: windows
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
# 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'

16
README.md Normal file
View File

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

28
analysis_options.yaml Normal file
View File

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

14
android/.gitignore vendored Normal file
View File

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

View File

@ -0,0 +1,81 @@
import java.util.Properties
import java.io.FileInputStream
plugins {
id("com.android.application")
// START: FlutterFire Configuration
id("com.google.gms.google-services")
// END: FlutterFire Configuration
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
val keystoreProperties = Properties().apply {
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
load(FileInputStream(keystorePropertiesFile))
}
}
android {
namespace = "com.example.apkcoba"
compileSdk = 35
// ndkVersion = flutter.ndkVersion
ndkVersion = "27.0.12077973"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
isCoreLibraryDesugaringEnabled = true
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.apkcoba"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = 23
targetSdk = 35
versionCode = 1
versionName = "1.0"
}
signingConfigs {
create("release") {
storeFile = file("E:/fluttercoba/apkcoba/android/app/medibox-release-key.jks")
storePassword = "medibox2025"
keyAlias = "medibox"
keyPassword = "medibox2025"
}
}
buildTypes {
getByName("release") {
isMinifyEnabled = false
isShrinkResources = false
signingConfig = signingConfigs.getByName("release")
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
buildFeatures {
viewBinding = true
}
}
flutter {
source = "../.."
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib:1.8.0")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
}

View File

@ -0,0 +1,30 @@
{
"project_info": {
"project_number": "531750565610",
"firebase_url": "https://coba-smartmedibox-default-rtdb.firebaseio.com",
"project_id": "coba-smartmedibox",
"storage_bucket": "coba-smartmedibox.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:531750565610:android:392b728b9109c3a5404993",
"android_client_info": {
"package_name": "com.example.apkcoba"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyAoQ1OITgUoHmGuIgJMCc9nzE5Nt8B45AA"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}

View File

@ -0,0 +1,8 @@
<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"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
</manifest>

View File

@ -0,0 +1,64 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_..." />
<!-- <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> -->
<application
android:label="Medibox"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<!-- android:roundIcon="@mipmap/ic_medicine"> -->
<!-- <service
android:name="dev.fluttercommunity.plus.flutter_background_service.BackgroundService"
android:exported="false"/> -->
<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" />
<service
android:name="id.flutter.flutter_background_service.BackgroundService"
android:foregroundServiceType="location|microphone"
/>
</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,7 @@
package com.example.apkcoba
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity() {
// Tidak perlu override configureFlutterEngine lagi
}

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,7 @@
pr laporan
1. pada tinjauan pustaka tambahkan buzzer, ganti lcd oled menjadi lcd i2c dan hapus modul df, sd card, speaker mini
2. ganti diagram blok dan keterangan diagram blok
3. ganti skematik beserta keterangan nya
4. ganti flowchart alat beserta keterangan nya
5. ganti desain 3d
6. ganti pengujian alat

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 836 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 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,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="default_notification_channel_id">my_foreground</string>
</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>

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

@ -0,0 +1,35 @@
buildscript {
val kotlinVersion by extra("1.8.10")
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:7.4.2")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
assets/images/logoputih.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

BIN
assets/images/logowarna.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

26
firebase.json Normal file
View File

@ -0,0 +1,26 @@
{
"flutter": {
"platforms": {
"android": {
"default": {
"projectId": "coba-smartmedibox",
"appId": "1:531750565610:android:392b728b9109c3a5404993",
"fileOutput": "android/app/google-services.json"
}
},
"dart": {
"lib/firebase_options.dart": {
"projectId": "coba-smartmedibox",
"configurations": {
"android": "1:531750565610:android:392b728b9109c3a5404993",
"web": "1:531750565610:web:5acfbccca5e41ee8404993",
"windows": "1:531750565610:web:8b42d0e7829dcf2d404993"
}
}
}
}
},
"functions": {
"source": "functions"
}
}

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.apkcoba;
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.apkcoba.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.apkcoba.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.apkcoba.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.apkcoba;
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.apkcoba;
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,99 @@
<?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"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 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>Apkcoba</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>apkcoba</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.
}
}

4
key.properties Normal file
View File

@ -0,0 +1,4 @@
storePassword=medibox2025
keyPassword=medibox2025
keyAlias=medibox
storeFile=E:/fluttercoba/apkcoba/android/app/medibox-release-key.jks

View File

@ -0,0 +1,485 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:firebase_database/firebase_database.dart';
import 'bottom_navigation.dart';
class AturJadwal1Screen extends StatefulWidget {
const AturJadwal1Screen({super.key});
@override
State<AturJadwal1Screen> createState() => _AturJadwal1ScreenState();
}
class _AturJadwal1ScreenState extends State<AturJadwal1Screen> {
final _namaObatController = TextEditingController();
final _jumlahObatController = TextEditingController();
bool isJamManual = true;
int dosisCount = 1;
List<TimeOfDay> manualTimes = [];
List<TimeOfDay> dosisTimes = [
TimeOfDay(hour: 6, minute: 0),
TimeOfDay(hour: 14, minute: 0),
TimeOfDay(hour: 22, minute: 0),
TimeOfDay(hour: 0, minute: 0),
];
final dbRef = FirebaseDatabase.instance.ref();
bool _isFirstEntry =
true; // Untuk cek apakah jadwal pertama sudah ada obat dan jumlah
@override
void initState() {
super.initState();
_checkExistingObatJumlah();
}
Future<void> _checkExistingObatJumlah() async {
try {
final obatSnap = await dbRef.child("jadwal/kotak1/obat1").get();
final jumlahSnap = await dbRef.child("jadwal/kotak1/jumlahObat").get();
if (obatSnap.exists &&
obatSnap.value != null &&
obatSnap.value.toString().trim().isNotEmpty &&
jumlahSnap.exists &&
jumlahSnap.value != null &&
jumlahSnap.value.toString().trim().isNotEmpty) {
// Jika data ada, artinya jadwal pertama sudah ada obat dan jumlah obat
setState(() {
_isFirstEntry = false;
// Set controller agar user tetap melihat data yang sudah ada (optional)
_namaObatController.text = obatSnap.value.toString();
_jumlahObatController.text = jumlahSnap.value.toString();
});
}
} catch (e) {
print('Error checking existing obat/jumlah: $e');
}
}
void simpanData() async {
final user = FirebaseAuth.instance.currentUser;
if (user == null) return;
final namaObat = _namaObatController.text.trim();
final jumlahObatText = _jumlahObatController.text.trim();
if (_isFirstEntry) {
if (namaObat.isEmpty || jumlahObatText.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
"Lengkapi data nama obat dan jumlah obat terlebih dahulu",
),
),
);
return;
}
if (int.tryParse(jumlahObatText) == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Jumlah obat harus berupa angka")),
);
return;
}
}
final int inputJumlahObat = int.tryParse(jumlahObatText) ?? 0;
final times =
isJamManual ? manualTimes : dosisTimes.take(dosisCount).toList();
final DatabaseReference kotakRef = dbRef.child("jadwal/kotak1");
try {
final snapshot = await kotakRef.get();
int index = 0;
final existingTimes = <String>[];
for (final child in snapshot.children) {
if (child.key != 'jumlahObat' &&
child.key != 'obat1' &&
child.key != 'userId') {
index++;
final jam = child.child('jam').value;
final menit = child.child('menit').value;
if (jam != null && menit != null) {
existingTimes.add('$jam:$menit');
}
}
}
if (index + times.length > 4) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Maksimal 4 jadwal diperbolehkan')),
);
return;
}
int existingJumlahObat = 0;
final jumlahSnap = snapshot.child('jumlahObat');
if (jumlahSnap.exists && jumlahSnap.value != null) {
existingJumlahObat =
(jumlahSnap.value is int)
? jumlahSnap.value as int
: int.tryParse(jumlahSnap.value.toString()) ?? 0;
}
int totalJumlahObat =
_isFirstEntry ? inputJumlahObat : existingJumlahObat;
if (_isFirstEntry && totalJumlahObat > 15) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Jumlah obat maksimal 15')),
);
return;
}
for (final time in times) {
final newTime = "${time.hour}:${time.minute}";
if (existingTimes.contains(newTime)) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Jadwal ini sudah ada')));
return;
}
}
// Simpan data utama (hanya sekali)
if (_isFirstEntry) {
await kotakRef.child('obat1').set(namaObat);
await kotakRef.child('jumlahObat').set(inputJumlahObat);
await kotakRef.child('userId').set(user.uid); // dipindah ke sini
}
// Simpan jadwal jam dan menit saja
for (final time in times) {
await kotakRef.child('$index').set({
'jam': time.hour,
'menit': time.minute,
});
index++;
}
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Jadwal berhasil disimpan')));
Navigator.pop(context);
} catch (e) {
print('Gagal menyimpan data: $e');
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Gagal menyimpan jadwal')));
}
}
Future<void> pilihJamManual() async {
if (manualTimes.length >= 4) return;
TimeOfDay? picked = await showTimePicker(
context: context,
initialTime: TimeOfDay.now(),
);
if (picked != null) {
setState(() {
manualTimes.add(picked);
});
}
}
Future<void> ubahJamDosis(int index) async {
TimeOfDay? picked = await showTimePicker(
context: context,
initialTime: dosisTimes[index],
);
if (picked != null) {
setState(() {
dosisTimes[index] = picked;
});
}
}
Widget buildInputField(
String label,
TextEditingController controller, {
bool isNumber = false,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label),
SizedBox(height: 6),
TextField(
controller: controller,
keyboardType: isNumber ? TextInputType.number : TextInputType.text,
decoration: InputDecoration(
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10),
),
),
SizedBox(height: 12),
],
);
}
Widget buildManualTimePicker() {
return Column(
children: [
for (int i = 0; i < manualTimes.length; i++)
ListTile(
title: Text(manualTimes[i].format(context)),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => setState(() => manualTimes.removeAt(i)),
),
),
if (manualTimes.length < 4)
ElevatedButton(onPressed: pilihJamManual, child: Text('Tambah Jam')),
],
);
}
Widget buildDosisTimePicker() {
return Column(
children: [
Wrap(
spacing: 10,
children:
[1, 2, 3, 4].map((e) {
return ChoiceChip(
label: Text('Obat ${e}x1'),
selected: dosisCount == e,
onSelected: (_) => setState(() => dosisCount = e),
);
}).toList(),
),
for (int i = 0; i < dosisCount; i++)
ListTile(
title: Text(dosisTimes[i].format(context)),
trailing: IconButton(
icon: Icon(Icons.edit),
onPressed: () => ubahJamDosis(i),
),
),
],
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color(0xFF3FA535),
body: Column(
children: [
SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: Row(
children: [
IconButton(
icon: Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
SizedBox(width: 8),
Text(
"Atur Jadwal Obat",
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
Expanded(
child: Container(
width: double.infinity,
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(30),
topRight: Radius.circular(30),
),
),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_isFirstEntry) ...[
buildInputField("Masukan Nama Obat", _namaObatController),
buildInputField(
"Masukan Jumlah Obat (Maksimal 15 Obat)",
_jumlahObatController,
isNumber: true,
),
] else ...[
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(
"Nama obat dan jumlah obat sudah ada. Tidak perlu mengisi ulang.",
style: TextStyle(
color: const Color.fromARGB(255, 245, 5, 13),
fontWeight: FontWeight.bold,
),
),
),
buildInputField(
"Nama Obat",
_namaObatController..text = _namaObatController.text,
isNumber: false,
),
buildInputField(
"Jumlah Obat",
_jumlahObatController
..text = _jumlahObatController.text,
isNumber: true,
),
],
SizedBox(height: 10),
Text(
"Pilih Aturan Jam:",
style: TextStyle(fontWeight: FontWeight.bold),
),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () => setState(() => isJamManual = true),
style: ElevatedButton.styleFrom(
backgroundColor:
isJamManual ? Colors.green : Colors.white,
foregroundColor:
isJamManual ? Colors.white : Colors.green,
side: BorderSide(color: Colors.green),
),
child: Text("Jam Manual"),
),
),
SizedBox(width: 8),
Expanded(
child: ElevatedButton(
onPressed:
() => setState(() => isJamManual = false),
style: ElevatedButton.styleFrom(
backgroundColor:
!isJamManual ? Colors.green : Colors.white,
foregroundColor:
!isJamManual ? Colors.white : Colors.green,
side: BorderSide(color: Colors.green),
),
child: Text("Jam Dosis"),
),
),
],
),
SizedBox(height: 16),
isJamManual
? buildManualTimePicker()
: buildDosisTimePicker(),
SizedBox(height: 20),
Center(
child: ElevatedButton(
onPressed: simpanData,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
),
child: const Text(
"Simpan",
style: TextStyle(
color: Color.fromARGB(255, 255, 255, 255),
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
),
),
],
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: 0, // sesuaikan dengan index aktif halaman sekarang
type: BottomNavigationBarType.fixed,
selectedItemColor: const Color(0xFF3FA535),
unselectedItemColor: Colors.grey.shade600,
showUnselectedLabels: true,
selectedLabelStyle: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
unselectedLabelStyle: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 12,
),
elevation: 8,
backgroundColor: Colors.white,
iconSize: 28,
onTap: (index) {
switch (index) {
case 0:
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => MainNavigation(currentIndex: 0),
),
);
break;
case 1:
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => MainNavigation(currentIndex: 1),
),
);
break;
case 2:
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => MainNavigation(currentIndex: 2),
),
);
break;
case 3:
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => MainNavigation(currentIndex: 3),
),
);
break;
}
},
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
activeIcon: Icon(Icons.home),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.history_outlined),
activeIcon: Icon(Icons.history),
label: 'History',
),
BottomNavigationBarItem(
icon: Icon(Icons.notifications_outlined),
activeIcon: Icon(Icons.notifications),
label: 'Notification',
),
BottomNavigationBarItem(
icon: Icon(Icons.person_outline),
activeIcon: Icon(Icons.person),
label: 'Account',
),
],
),
//bottomNavigationBar: MainNavigation(currentIndex: ),
);
}
}

View File

@ -0,0 +1,485 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:firebase_database/firebase_database.dart';
import 'bottom_navigation.dart';
class AturJadwal2Screen extends StatefulWidget {
const AturJadwal2Screen({super.key});
@override
State<AturJadwal2Screen> createState() => _AturJadwal2ScreenState();
}
class _AturJadwal2ScreenState extends State<AturJadwal2Screen> {
final _namaObatController = TextEditingController();
final _jumlahObatController = TextEditingController();
bool isJamManual = true;
int dosisCount = 1;
List<TimeOfDay> manualTimes = [];
List<TimeOfDay> dosisTimes = [
TimeOfDay(hour: 6, minute: 0),
TimeOfDay(hour: 14, minute: 0),
TimeOfDay(hour: 22, minute: 0),
TimeOfDay(hour: 0, minute: 0),
];
final dbRef = FirebaseDatabase.instance.ref();
bool _isFirstEntry =
true; // Untuk cek apakah jadwal pertama sudah ada obat dan jumlah
@override
void initState() {
super.initState();
_checkExistingObatJumlah();
}
Future<void> _checkExistingObatJumlah() async {
try {
final obatSnap = await dbRef.child("jadwal/kotak2/obat2").get();
final jumlahSnap = await dbRef.child("jadwal/kotak2/jumlahObat").get();
if (obatSnap.exists &&
obatSnap.value != null &&
obatSnap.value.toString().trim().isNotEmpty &&
jumlahSnap.exists &&
jumlahSnap.value != null &&
jumlahSnap.value.toString().trim().isNotEmpty) {
// Jika data ada, artinya jadwal pertama sudah ada obat dan jumlah obat
setState(() {
_isFirstEntry = false;
// Set controller agar user tetap melihat data yang sudah ada (optional)
_namaObatController.text = obatSnap.value.toString();
_jumlahObatController.text = jumlahSnap.value.toString();
});
}
} catch (e) {
print('Error checking existing obat/jumlah: $e');
}
}
void simpanData() async {
final user = FirebaseAuth.instance.currentUser;
if (user == null) return;
final namaObat = _namaObatController.text.trim();
final jumlahObatText = _jumlahObatController.text.trim();
if (_isFirstEntry) {
if (namaObat.isEmpty || jumlahObatText.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
"Lengkapi data nama obat dan jumlah obat terlebih dahulu",
),
),
);
return;
}
if (int.tryParse(jumlahObatText) == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Jumlah obat harus berupa angka")),
);
return;
}
}
final int inputJumlahObat = int.tryParse(jumlahObatText) ?? 0;
final times =
isJamManual ? manualTimes : dosisTimes.take(dosisCount).toList();
final DatabaseReference kotakRef = dbRef.child("jadwal/kotak2");
try {
final snapshot = await kotakRef.get();
int index = 0;
final existingTimes = <String>[];
for (final child in snapshot.children) {
if (child.key != 'jumlahObat' &&
child.key != 'obat2' &&
child.key != 'userId') {
index++;
final jam = child.child('jam').value;
final menit = child.child('menit').value;
if (jam != null && menit != null) {
existingTimes.add('$jam:$menit');
}
}
}
if (index + times.length > 4) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Maksimal 4 jadwal diperbolehkan')),
);
return;
}
int existingJumlahObat = 0;
final jumlahSnap = snapshot.child('jumlahObat');
if (jumlahSnap.exists && jumlahSnap.value != null) {
existingJumlahObat =
(jumlahSnap.value is int)
? jumlahSnap.value as int
: int.tryParse(jumlahSnap.value.toString()) ?? 0;
}
int totalJumlahObat =
_isFirstEntry ? inputJumlahObat : existingJumlahObat;
if (_isFirstEntry && totalJumlahObat > 15) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Jumlah obat maksimal 15')),
);
return;
}
for (final time in times) {
final newTime = "${time.hour}:${time.minute}";
if (existingTimes.contains(newTime)) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Jadwal ini sudah ada')));
return;
}
}
// Simpan data utama (hanya sekali)
if (_isFirstEntry) {
await kotakRef.child('obat2').set(namaObat);
await kotakRef.child('jumlahObat').set(inputJumlahObat);
await kotakRef.child('userId').set(user.uid); // dipindah ke sini
}
// Simpan jadwal jam dan menit saja
for (final time in times) {
await kotakRef.child('$index').set({
'jam': time.hour,
'menit': time.minute,
});
index++;
}
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Jadwal berhasil disimpan')));
Navigator.pop(context);
} catch (e) {
print('Gagal menyimpan data: $e');
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Gagal menyimpan jadwal')));
}
}
Future<void> pilihJamManual() async {
if (manualTimes.length >= 4) return;
TimeOfDay? picked = await showTimePicker(
context: context,
initialTime: TimeOfDay.now(),
);
if (picked != null) {
setState(() {
manualTimes.add(picked);
});
}
}
Future<void> ubahJamDosis(int index) async {
TimeOfDay? picked = await showTimePicker(
context: context,
initialTime: dosisTimes[index],
);
if (picked != null) {
setState(() {
dosisTimes[index] = picked;
});
}
}
Widget buildInputField(
String label,
TextEditingController controller, {
bool isNumber = false,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label),
SizedBox(height: 6),
TextField(
controller: controller,
keyboardType: isNumber ? TextInputType.number : TextInputType.text,
decoration: InputDecoration(
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10),
),
),
SizedBox(height: 12),
],
);
}
Widget buildManualTimePicker() {
return Column(
children: [
for (int i = 0; i < manualTimes.length; i++)
ListTile(
title: Text(manualTimes[i].format(context)),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => setState(() => manualTimes.removeAt(i)),
),
),
if (manualTimes.length < 4)
ElevatedButton(onPressed: pilihJamManual, child: Text('Tambah Jam')),
],
);
}
Widget buildDosisTimePicker() {
return Column(
children: [
Wrap(
spacing: 10,
children:
[1, 2, 3, 4].map((e) {
return ChoiceChip(
label: Text('Obat ${e}x1'),
selected: dosisCount == e,
onSelected: (_) => setState(() => dosisCount = e),
);
}).toList(),
),
for (int i = 0; i < dosisCount; i++)
ListTile(
title: Text(dosisTimes[i].format(context)),
trailing: IconButton(
icon: Icon(Icons.edit),
onPressed: () => ubahJamDosis(i),
),
),
],
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color(0xFF3FA535),
body: Column(
children: [
SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: Row(
children: [
IconButton(
icon: Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
SizedBox(width: 8),
Text(
"Atur Jadwal Obat",
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
Expanded(
child: Container(
width: double.infinity,
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(30),
topRight: Radius.circular(30),
),
),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_isFirstEntry) ...[
buildInputField("Masukan Nama Obat", _namaObatController),
buildInputField(
"Masukan Jumlah Obat (Maksimal 15 Obat)",
_jumlahObatController,
isNumber: true,
),
] else ...[
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(
"Nama obat dan jumlah obat sudah ada. Tidak perlu mengisi ulang.",
style: TextStyle(
color: const Color.fromARGB(255, 245, 5, 13),
fontWeight: FontWeight.bold,
),
),
),
buildInputField(
"Nama Obat",
_namaObatController..text = _namaObatController.text,
isNumber: false,
),
buildInputField(
"Jumlah Obat",
_jumlahObatController
..text = _jumlahObatController.text,
isNumber: true,
),
],
SizedBox(height: 10),
Text(
"Pilih Aturan Jam:",
style: TextStyle(fontWeight: FontWeight.bold),
),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () => setState(() => isJamManual = true),
style: ElevatedButton.styleFrom(
backgroundColor:
isJamManual ? Colors.green : Colors.white,
foregroundColor:
isJamManual ? Colors.white : Colors.green,
side: BorderSide(color: Colors.green),
),
child: Text("Jam Manual"),
),
),
SizedBox(width: 8),
Expanded(
child: ElevatedButton(
onPressed:
() => setState(() => isJamManual = false),
style: ElevatedButton.styleFrom(
backgroundColor:
!isJamManual ? Colors.green : Colors.white,
foregroundColor:
!isJamManual ? Colors.white : Colors.green,
side: BorderSide(color: Colors.green),
),
child: Text("Jam Dosis"),
),
),
],
),
SizedBox(height: 16),
isJamManual
? buildManualTimePicker()
: buildDosisTimePicker(),
SizedBox(height: 20),
Center(
child: ElevatedButton(
onPressed: simpanData,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
),
child: const Text(
"Simpan",
style: TextStyle(
color: Color.fromARGB(255, 255, 255, 255),
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
),
),
],
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: 0, // sesuaikan dengan index aktif halaman sekarang
type: BottomNavigationBarType.fixed,
selectedItemColor: const Color(0xFF3FA535),
unselectedItemColor: Colors.grey.shade600,
showUnselectedLabels: true,
selectedLabelStyle: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
unselectedLabelStyle: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 12,
),
elevation: 8,
backgroundColor: Colors.white,
iconSize: 28,
onTap: (index) {
switch (index) {
case 0:
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => MainNavigation(currentIndex: 0),
),
);
break;
case 1:
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => MainNavigation(currentIndex: 1),
),
);
break;
case 2:
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => MainNavigation(currentIndex: 2),
),
);
break;
case 3:
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => MainNavigation(currentIndex: 3),
),
);
break;
}
},
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
activeIcon: Icon(Icons.home),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.history_outlined),
activeIcon: Icon(Icons.history),
label: 'History',
),
BottomNavigationBarItem(
icon: Icon(Icons.notifications_outlined),
activeIcon: Icon(Icons.notifications),
label: 'Notification',
),
BottomNavigationBarItem(
icon: Icon(Icons.person_outline),
activeIcon: Icon(Icons.person),
label: 'Account',
),
],
),
//bottomNavigationBar: MainNavigation(currentIndex: ),
);
}
}

View File

@ -0,0 +1,99 @@
import 'package:apkcoba/notifikasi_page.dart';
import 'package:apkcoba/profile_page.dart';
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'dart:async';
import 'home_screen.dart';
import 'history_page.dart';
import 'notifikasi_page.dart';
import 'profile_page.dart';
import 'notifikasi_service.dart';
class MainNavigation extends StatefulWidget {
final int currentIndex;
const MainNavigation({super.key, this.currentIndex = 0});
@override
_MainNavigationState createState() => _MainNavigationState();
}
class _MainNavigationState extends State<MainNavigation> {
late int _selectedIndex;
final bool _hasSentKotak1 = false;
final bool _hasSentKotak2 = false;
DateTime? _lastSentKotak1;
DateTime? _lastSentKotak2;
static const Duration minInterval = Duration(seconds: 10);
@override
void initState() {
super.initState();
//syncTimestampIfAuthorized();
//NotifikasiService.listenToAllNotifikasi();
_selectedIndex = widget.currentIndex; // <== GUNAKAN INI!
}
final List<Widget> _pages = [
HomeScreen(username: '', onSignOut: () {}),
HistoryPage(),
NotifikasiPage(), // Notification
ProfilePage(), // Account
];
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: _pages[_selectedIndex],
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedIndex,
onTap: _onItemTapped,
type: BottomNavigationBarType.fixed, // stabil untuk banyak item
selectedItemColor: const Color(0xFF3FA535),
unselectedItemColor: Colors.grey.shade600,
showUnselectedLabels: true,
selectedLabelStyle: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
unselectedLabelStyle: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 12,
),
elevation: 8,
backgroundColor: Colors.white,
iconSize: 28,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
activeIcon: Icon(Icons.home),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.history_outlined),
activeIcon: Icon(Icons.history),
label: 'History',
),
BottomNavigationBarItem(
icon: Icon(Icons.notifications_outlined),
activeIcon: Icon(Icons.notifications),
label: 'Notification',
),
BottomNavigationBarItem(
icon: Icon(Icons.person_outline),
activeIcon: Icon(Icons.person),
label: 'Account',
),
],
),
);
}
}

View File

@ -0,0 +1,35 @@
import 'package:firebase_auth/firebase_auth.dart';
class FirebaseAuthService {
final FirebaseAuth _auth = FirebaseAuth.instance;
Future<User?> signUpWithEmailAndPassword(
String email,
String password,
) async {
try {
UserCredential credential = await _auth.createUserWithEmailAndPassword(
email: email,
password: password,
);
return credential.user;
} on FirebaseAuthException {
rethrow; // lempar ulang ke pemanggil
}
}
Future<User?> signInWithEmailAndPassword(
String email,
String password,
) async {
try {
UserCredential credential = await _auth.signInWithEmailAndPassword(
email: email,
password: password,
);
return credential.user;
} on FirebaseAuthException {
rethrow;
}
}
}

80
lib/firebase_options.dart Normal file
View File

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

View File

@ -0,0 +1,77 @@
import 'package:flutter/material.dart';
class FormContainerWidget extends StatefulWidget {
final TextEditingController? controller;
final Key? fieldKey;
final bool? isPasswordField;
final String? hintText;
final String? labelText;
final String? helperText;
final FormFieldSetter<String>? onSaved;
final FormFieldValidator<String>? validator;
final ValueChanged<String>? onFieldSubmitted;
final TextInputType? inputType;
const FormContainerWidget({
super.key,
this.controller,
this.isPasswordField,
this.fieldKey,
this.hintText,
this.labelText,
this.helperText,
this.onSaved,
this.validator,
this.onFieldSubmitted,
this.inputType,
});
@override
_FormContainerWidgetState createState() => _FormContainerWidgetState();
}
class _FormContainerWidgetState extends State<FormContainerWidget> {
bool _obscureText = true;
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: Colors.grey.withOpacity(.35),
borderRadius: BorderRadius.circular(10),
),
child: TextFormField(
style: TextStyle(color: Colors.black),
controller: widget.controller,
keyboardType: widget.inputType,
key: widget.fieldKey,
obscureText: widget.isPasswordField == true ? _obscureText : false,
onSaved: widget.onSaved,
validator: widget.validator,
onFieldSubmitted: widget.onFieldSubmitted,
decoration: InputDecoration(
border: InputBorder.none,
filled: true,
hintText: widget.hintText,
hintStyle: TextStyle(color: Colors.black45),
suffixIcon: GestureDetector(
onTap: () {
setState(() {
_obscureText = !_obscureText;
});
},
child:
widget.isPasswordField == true
? Icon(
_obscureText ? Icons.visibility_off : Icons.visibility,
color: _obscureText == false ? Colors.blue : Colors.grey,
)
: Text(""),
),
),
),
);
}
}

462
lib/history_page.dart Normal file
View File

@ -0,0 +1,462 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'bottom_navigation.dart';
class HistoryPage extends StatefulWidget {
const HistoryPage({super.key});
@override
State<HistoryPage> createState() => _HistoryPageState();
}
class _HistoryPageState extends State<HistoryPage> {
final FirebaseAuth _auth = FirebaseAuth.instance;
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
final List<DocumentSnapshot> _historyDocs = [];
final int _limit = 10;
DocumentSnapshot? _lastDocument;
bool _isLoadingMore = false;
bool _hasMore = true;
bool _isInitialLoading = true;
final ScrollController _scrollController = ScrollController();
// Filter variables
int? filterDay;
int? filterMonth;
int? filterYear;
int? filterHour;
int? filterMinute;
@override
void initState() {
super.initState();
_loadInitialHistory();
_scrollController.addListener(_scrollListener);
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
void _scrollListener() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 100 &&
!_isLoadingMore &&
_hasMore) {
_loadMoreHistory();
}
}
Future<void> _loadInitialHistory() async {
final user = _auth.currentUser;
if (user == null) return;
try {
final querySnapshot =
await _firestore
.collection('users')
.doc(user.uid)
.collection('history')
.orderBy('timestamp', descending: true)
.limit(_limit)
.get();
setState(() {
_historyDocs.clear();
_historyDocs.addAll(querySnapshot.docs);
_lastDocument =
querySnapshot.docs.isNotEmpty ? querySnapshot.docs.last : null;
_hasMore = querySnapshot.docs.length == _limit;
_isInitialLoading = false;
});
} catch (e) {
print('Error loading initial history: $e');
setState(() {
_isInitialLoading = false;
});
}
}
Future<void> _loadMoreHistory() async {
if (!_hasMore || _isLoadingMore || _lastDocument == null) return;
setState(() => _isLoadingMore = true);
final user = _auth.currentUser;
if (user == null) return;
try {
final querySnapshot =
await _firestore
.collection('users')
.doc(user.uid)
.collection('history')
.orderBy('timestamp', descending: true)
.startAfterDocument(_lastDocument!)
.limit(_limit)
.get();
setState(() {
_historyDocs.addAll(querySnapshot.docs);
_lastDocument =
querySnapshot.docs.isNotEmpty
? querySnapshot.docs.last
: _lastDocument;
_hasMore = querySnapshot.docs.length == _limit;
_isLoadingMore = false;
});
} catch (e) {
print('Error loading more history: $e');
setState(() {
_isLoadingMore = false;
});
}
}
String _formatDate(dynamic timestamp) {
if (timestamp == null) return '';
DateTime dateTime;
if (timestamp is Timestamp) {
dateTime = timestamp.toDate().toLocal();
} else if (timestamp is String) {
dateTime = DateTime.tryParse(timestamp)?.toLocal() ?? DateTime.now();
} else {
return '';
}
final time =
"${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}";
final date =
"${dateTime.day.toString().padLeft(2, '0')}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.year}";
return "$time $date";
}
Future<void> _deleteHistory(String docId) async {
final user = _auth.currentUser;
if (user == null) return;
await _firestore
.collection('users')
.doc(user.uid)
.collection('history')
.doc(docId)
.delete();
setState(() {
_historyDocs.removeWhere((doc) => doc.id == docId);
});
}
Future<void> _refresh() async {
setState(() {
_isInitialLoading = true;
_hasMore = true;
_lastDocument = null;
});
await _loadInitialHistory();
}
List<DocumentSnapshot> applyFilter(List<DocumentSnapshot> docs) {
return docs.where((doc) {
final data = doc.data() as Map<String, dynamic>?;
if (data == null) return false;
final timestamp = data['timestamp'];
if (timestamp == null) return false;
DateTime dt;
if (timestamp is Timestamp) {
dt = timestamp.toDate();
} else if (timestamp is String) {
dt = DateTime.tryParse(timestamp) ?? DateTime.now();
} else {
return false;
}
if (filterYear != null && dt.year != filterYear) return false;
if (filterMonth != null && dt.month != filterMonth) return false;
if (filterDay != null && dt.day != filterDay) return false;
if (filterHour != null && dt.hour != filterHour) return false;
if (filterMinute != null && dt.minute != filterMinute) return false;
return true;
}).toList();
}
Widget buildDropdown(
String label,
List<int> items,
int? selectedValue,
ValueChanged<int?> onChanged,
) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('$label: '),
DropdownButton<int?>(
value: selectedValue,
hint: const Text('Semua'),
items:
[null, ...items].map<DropdownMenuItem<int?>>((val) {
return DropdownMenuItem<int?>(
value: val,
child: Text(val?.toString() ?? 'Semua'),
);
}).toList(),
onChanged: onChanged,
),
],
);
}
Widget buildFilterRow() {
final currentYear = DateTime.now().year;
return Padding(
padding: const EdgeInsets.all(8.0),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
buildDropdown(
'Tahun',
List.generate(10, (i) => currentYear - i),
filterYear,
(val) => setState(() => filterYear = val),
),
const SizedBox(width: 8),
buildDropdown(
'Bulan',
List.generate(12, (i) => i + 1),
filterMonth,
(val) => setState(() => filterMonth = val),
),
const SizedBox(width: 8),
buildDropdown(
'Tanggal',
List.generate(31, (i) => i + 1),
filterDay,
(val) => setState(() => filterDay = val),
),
const SizedBox(width: 8),
buildDropdown(
'Jam',
List.generate(24, (i) => i),
filterHour,
(val) => setState(() => filterHour = val),
),
const SizedBox(width: 8),
buildDropdown(
'Menit',
List.generate(60, (i) => i),
filterMinute,
(val) => setState(() => filterMinute = val),
),
const SizedBox(width: 12),
ElevatedButton(
onPressed: () {
setState(() {
filterDay = null;
filterMonth = null;
filterYear = null;
filterHour = null;
filterMinute = null;
});
},
child: const Text('Reset Filter'),
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
final user = _auth.currentUser;
if (user == null) {
return const Center(child: Text("User belum login."));
}
final filteredHistory = applyFilter(_historyDocs);
return Scaffold(
backgroundColor: const Color(0xFFE9F5EC),
body: SafeArea(
child: Column(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
alignment: Alignment.center,
decoration: const BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(32),
bottomRight: Radius.circular(32),
),
),
child: const Text(
'History',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
buildFilterRow(),
Expanded(
child:
_isInitialLoading
? const Center(child: CircularProgressIndicator())
: filteredHistory.isEmpty
? const Center(child: Text("Belum ada riwayat."))
: RefreshIndicator(
onRefresh: _refresh,
child: ListView.builder(
controller: _scrollController,
physics: const AlwaysScrollableScrollPhysics(),
itemCount:
filteredHistory.length + (_hasMore ? 1 : 0),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
itemBuilder: (context, index) {
if (index >= filteredHistory.length) {
return const Padding(
padding: EdgeInsets.all(16.0),
child: Center(
child: CircularProgressIndicator(),
),
);
}
final data =
filteredHistory[index].data()
as Map<String, dynamic>;
final docId = filteredHistory[index].id;
final kotak = data['kotak'] ?? 'Tidak diketahui';
final namaObat =
data['namaObat'] ?? 'Tidak diketahui';
final timestamp = data['timestamp'];
return Dismissible(
key: Key(docId),
direction: DismissDirection.endToStart,
background: Container(
padding: const EdgeInsets.symmetric(
horizontal: 20,
),
alignment: Alignment.centerRight,
color: Colors.red,
child: const Icon(
Icons.delete,
color: Colors.white,
),
),
confirmDismiss: (_) async {
return await showDialog(
context: context,
builder:
(_) => AlertDialog(
title: const Text("Hapus History"),
content: const Text(
"Apakah kamu yakin ingin menghapus riwayat ini?",
),
actions: [
TextButton(
onPressed:
() => Navigator.of(
context,
).pop(false),
child: const Text("Batal"),
),
TextButton(
onPressed:
() => Navigator.of(
context,
).pop(true),
child: const Text(
"Hapus",
style: TextStyle(
color: Colors.red,
),
),
),
],
),
);
},
onDismissed: (_) => _deleteHistory(docId),
child: Container(
margin: const EdgeInsets.only(bottom: 12),
padding: EdgeInsets.symmetric(
vertical: 14,
horizontal: 16,
),
// padding: const EdgeInsets.symmetric(
// vertical: 14,
// horizontal: 16,
// ),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: const [
BoxShadow(
color: Colors.black12,
blurRadius: 4,
offset: Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment:
MainAxisAlignment.start,
children: [
Text(
"Kotak ${kotak.toString().split(' ').last}",
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 25,
),
),
const SizedBox(width: 10),
Expanded(
child: Text(
"Obat $namaObat",
style: const TextStyle(
fontSize: 20,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 6),
Text(
"Waktu : ${_formatDate(timestamp)}",
style: const TextStyle(
fontSize: 17,
color: Colors.black54,
),
),
],
),
),
);
},
),
),
),
],
),
),
);
}
}

710
lib/home_screen.dart Normal file
View File

@ -0,0 +1,710 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:firebase_database/firebase_database.dart';
import 'package:flutter_background_service/flutter_background_service.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'jadwal_kotak_obat1_screen.dart';
import 'jadwal_kotak_obat2_screen.dart';
import 'login_screen.dart';
//import 'firebase_messaging_service.dart';
import 'notifikasi_service.dart';
import 'toast.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({
super.key,
required String username,
required void Function() onSignOut,
});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
Timer? _syncTimer;
final bool _hasSentKotak1 = false;
final bool _hasSentKotak2 = false;
DateTime? _lastSentKotak1;
DateTime? _lastSentKotak2;
static const Duration minInterval = Duration(seconds: 10);
String username = '';
bool isLoading = true;
// Tambahan state untuk tombol dan status
bool button1 = false;
bool button2 = false;
bool buzzer = false;
bool lampu = false;
bool _canAccessKotak1 = false;
bool _canAccessKotak2 = false;
String infoText = "Tidak Ada Informasi";
@override
void initState() {
super.initState();
//NotifikasiService.listenToAllNotifikasi(); // Realtime listener
_initializeAsyncTasks(); // panggil fungsi async tanpa await
_checkUserAccessToKotak();
_syncTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
_updateStatus(); // update tombol dan info tiap 2 detik
});
}
@override
void dispose() {
_syncTimer?.cancel();
super.dispose();
}
Future<void> _initializeAsyncTasks() async {
await fetchUsername();
}
Future<void> fetchUsername() async {
try {
final user = FirebaseAuth.instance.currentUser;
if (user != null) {
final uid = user.uid;
final doc =
await FirebaseFirestore.instance.collection('users').doc(uid).get();
if (doc.exists && doc.data() != null) {
if (!mounted) return;
setState(() {
username = doc['username'] ?? 'User';
isLoading = false;
});
}
}
} catch (e) {
print('Error fetching username: $e');
if (!mounted) return;
setState(() {
username = 'User';
isLoading = false;
});
}
}
Future<void> _showLogoutConfirmationDialog() async {
final shouldLogout = await showDialog<bool>(
context: context,
builder:
(context) => AlertDialog(
title: const Text('Konfirmasi Logout'),
content: const Text(
'Apakah Anda yakin ingin logout dari akun ini?',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Tidak'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Ya'),
),
],
),
);
if (shouldLogout == true) {
_logout();
}
}
// Fungsi baru: Update status tombol dan info
Future<void> _updateStatus() async {
final user = FirebaseAuth.instance.currentUser;
if (user == null) return;
final uid = user.uid;
try {
final button1Ref = FirebaseDatabase.instance.ref('status/button1');
final button2Ref = FirebaseDatabase.instance.ref('status/button2');
final buzzerRef = FirebaseDatabase.instance.ref('status/buzzer');
final lampuRef = FirebaseDatabase.instance.ref('status/led');
final kotak1UserIdRef = FirebaseDatabase.instance.ref(
'jadwal/kotak1/userId',
);
final kotak2UserIdRef = FirebaseDatabase.instance.ref(
'jadwal/kotak2/userId',
);
final button1Snap = await button1Ref.get();
final button2Snap = await button2Ref.get();
final buzzerSnap = await buzzerRef.get();
final lampuSnap = await lampuRef.get();
final kotak1UserIdSnap = await kotak1UserIdRef.get();
final kotak2UserIdSnap = await kotak2UserIdRef.get();
// Cek userId sesuai
final button1UserMatch =
kotak1UserIdSnap.exists && kotak1UserIdSnap.value == uid;
final button2UserMatch =
kotak2UserIdSnap.exists && kotak2UserIdSnap.value == uid;
if (!mounted) return;
setState(() {
button1 = button1Snap.exists && button1Snap.value == true;
button2 = button2Snap.exists && button2Snap.value == true;
buzzer = buzzerSnap.exists && buzzerSnap.value == true;
lampu = lampuSnap.exists && lampuSnap.value == true;
// Jika userId tidak cocok maka tombol tetap tampil tapi tidak bisa ditekan, tapi kita simpan status tombol sesuai DB
if (!button1UserMatch) button1 = button1; // tetap tampil status
if (!button2UserMatch) button2 = button2; // tetap tampil status
});
// Update infoText sesuai kondisi:
String info = "Tidak Ada Informasi";
// Cek alarm1 dan alarm2
final alarm1Snap =
await FirebaseDatabase.instance.ref('status/alarm1').get();
final alarm2Snap =
await FirebaseDatabase.instance.ref('status/alarm2').get();
// Cek jumlahObat kotak1 dan kotak2
final jumlahObat1Snap =
await FirebaseDatabase.instance.ref('jadwal/kotak1/jumlahObat').get();
final jumlahObat2Snap =
await FirebaseDatabase.instance.ref('jadwal/kotak2/jumlahObat').get();
final kotak1UserIdCheck =
kotak1UserIdSnap.exists && kotak1UserIdSnap.value == uid;
final kotak2UserIdCheck =
kotak2UserIdSnap.exists && kotak2UserIdSnap.value == uid;
if (kotak1UserIdCheck) {
if (alarm1Snap.exists && alarm1Snap.value == true) {
info = "Alarm Kotak 1 Aktif";
} else if (jumlahObat1Snap.exists) {
final val = jumlahObat1Snap.value;
int? jumlah = (val is int) ? val : int.tryParse(val.toString());
if (jumlah != null) {
if (jumlah == 0) {
info = "Obat Kotak 1 Habis, Segera isi ulang";
} else if (jumlah <= 3 && jumlah > 0) {
info = "Obat Kotak 1 Sisa $jumlah, Waktunya isi ulang";
}
}
}
}
if (kotak2UserIdCheck) {
if (alarm2Snap.exists && alarm2Snap.value == true) {
info = "Alarm Kotak 2 Aktif";
} else if (jumlahObat2Snap.exists) {
final val = jumlahObat2Snap.value;
int? jumlah = (val is int) ? val : int.tryParse(val.toString());
if (jumlah != null) {
if (jumlah == 0) {
info = "Obat Kotak 2 Habis, Segera isi ulang";
} else if (jumlah <= 3 && jumlah > 0) {
info = "Obat Kotak 2 Sisa $jumlah, Waktunya isi ulang";
}
}
}
}
if (!mounted) return;
setState(() {
infoText = info;
});
} catch (e) {
print('Gagal update status tombol dan info: $e');
}
}
Future<void> _logout() async {
await FirebaseAuth.instance.signOut();
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.remove('userId');
FlutterBackgroundService().invoke("stopService");
print("✅ Logout berhasil dan background service dihentikan");
print('❌ userId dihapus dari SharedPreferences');
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const LoginScreen()),
);
showToast(message: "Successfully Logout");
}
// Fungsi untuk toggle button kotak 1
void _toggleButton1() async {
final user = FirebaseAuth.instance.currentUser;
if (user == null) return;
final userId = user.uid;
final kotak1UserIdSnap =
await FirebaseDatabase.instance.ref('jadwal/kotak1/userId').get();
if (!kotak1UserIdSnap.exists || kotak1UserIdSnap.value != userId) return;
await FirebaseDatabase.instance.ref('status/button1').set(!button1);
}
// Fungsi untuk toggle button kotak 2
void _toggleButton2() async {
final user = FirebaseAuth.instance.currentUser;
if (user == null) return;
final userId = user.uid;
final kotak2UserIdSnap =
await FirebaseDatabase.instance.ref('jadwal/kotak2/userId').get();
if (!kotak2UserIdSnap.exists || kotak2UserIdSnap.value != userId) return;
await FirebaseDatabase.instance.ref('status/button2').set(!button2);
}
Future<void> _checkUserAccessToKotak() async {
final user = FirebaseAuth.instance.currentUser;
if (user == null) return;
final uid = user.uid;
try {
final kotak1UserIdSnap =
await FirebaseDatabase.instance.ref('jadwal/kotak1/userId').get();
final kotak2UserIdSnap =
await FirebaseDatabase.instance.ref('jadwal/kotak2/userId').get();
bool canAccessKotak1 = false;
bool canAccessKotak2 = false;
// Jika userId kosong atau tidak ada, maka semua user boleh akses
if (!kotak1UserIdSnap.exists ||
kotak1UserIdSnap.value == null ||
kotak1UserIdSnap.value.toString().trim().isEmpty) {
canAccessKotak1 = true;
} else {
canAccessKotak1 = kotak1UserIdSnap.value == uid;
}
if (!kotak2UserIdSnap.exists ||
kotak2UserIdSnap.value == null ||
kotak2UserIdSnap.value.toString().trim().isEmpty) {
canAccessKotak2 = true;
} else {
canAccessKotak2 = kotak2UserIdSnap.value == uid;
}
if (!mounted) return;
setState(() {
_canAccessKotak1 = canAccessKotak1;
_canAccessKotak2 = canAccessKotak2;
});
} catch (e) {
print('Error checking access to kotak: $e');
}
}
void _showAccessDeniedDialog(String kotakName) {
showDialog(
context: context,
builder:
(_) => AlertDialog(
title: const Text('Akses Ditolak'),
content: Text('$kotakName Sudah diisi user lain'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('OK'),
),
],
),
);
}
Future<void> _refresh() async {
setState(() {
isLoading = true;
});
await Future.wait([
_initializeAsyncTasks(),
_checkUserAccessToKotak(),
_updateStatus(),
]);
if (mounted) {
setState(() {
isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
if (isLoading) {
return const Center(child: CircularProgressIndicator());
}
return RefreshIndicator(
onRefresh: _refresh,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Column(
children: [
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 35, horizontal: 24),
decoration: const BoxDecoration(
color: Color(0xFF3FA535),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(40),
bottomRight: Radius.circular(40),
),
),
child: Row(
mainAxisAlignment:
MainAxisAlignment
.center, // Menyusun elemen-elemen ke tengah horizontal
crossAxisAlignment:
CrossAxisAlignment
.center, // Menyusun elemen-elemen ke tengah vertikal
children: [
const Icon(
Icons.account_circle,
color: Colors.white,
size: 35,
),
const SizedBox(width: 10),
Text(
'Hello, $username',
style: const TextStyle(
fontSize: 20,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
IconButton(
onPressed: _showLogoutConfirmationDialog,
icon: const Icon(
Icons.logout,
color: Colors.white,
size: 30,
),
),
],
),
),
Container(
margin: const EdgeInsets.all(24),
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.grey.shade300,
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
'assets/images/logowarna.png',
width: 60,
height: 60,
),
SizedBox(width: 10), // jarak antar gambar
Image.asset(
'assets/images/logopolije.png',
width: 60,
height: 60,
),
],
),
const SizedBox(height: 10),
const Text(
'SmartMediBox',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
const Text(
'Pilih kotak obat untuk atur jadwal obat',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 14, color: Colors.black54),
),
const SizedBox(height: 30),
// Tombol Jadwal Kotak Obat 1 dengan pengecekan akses
ElevatedButton(
onPressed:
_canAccessKotak1
? () {
Navigator.push(
context,
MaterialPageRoute(
builder:
(context) =>
const JadwalKotakObat1Screen(),
),
);
}
: () => _showAccessDeniedDialog('Kotak Obat 1'),
style: ElevatedButton.styleFrom(
backgroundColor:
_canAccessKotak1
? const Color(0xFF3FA535)
: Colors.grey,
padding: const EdgeInsets.symmetric(vertical: 16),
minimumSize: const Size.fromHeight(50),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(
'Jadwal Kotak Obat 1',
style: TextStyle(
fontSize: 16,
color: _canAccessKotak1 ? Colors.white : Colors.white70,
),
),
),
const SizedBox(height: 20),
// Tombol Jadwal Kotak Obat 2 dengan pengecekan akses
ElevatedButton(
onPressed:
_canAccessKotak2
? () {
Navigator.push(
context,
MaterialPageRoute(
builder:
(context) =>
const JadwalKotakObat2Screen(),
),
);
}
: () => _showAccessDeniedDialog('Kotak Obat 2'),
style: ElevatedButton.styleFrom(
backgroundColor:
_canAccessKotak2
? const Color(0xFF3FA535)
: Colors.grey,
padding: const EdgeInsets.symmetric(vertical: 16),
minimumSize: const Size.fromHeight(50),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(
'Jadwal Kotak Obat 2',
style: TextStyle(
fontSize: 16,
color: _canAccessKotak2 ? Colors.white : Colors.white70,
),
),
),
],
),
),
// ======== TAMBAHAN CONTAINER SESUAI DESAIN =========
Container(
margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 0),
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.grey.shade300,
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
child: Column(
children: [
// Row Tombol Kotak 1 & Kotak 2
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Tombol Kotak 1
Expanded(
child: Column(
children: [
Text(
"Tombol Kotak 1",
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 6),
ElevatedButton(
onPressed: _toggleButton1,
style: ElevatedButton.styleFrom(
backgroundColor:
button1 ? Colors.green : Colors.red,
minimumSize: const Size(80, 36),
padding: const EdgeInsets.symmetric(
horizontal: 8,
),
),
child: Text(
button1 ? 'ON' : 'OFF',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
const SizedBox(width: 16),
// Tombol Kotak 2
Expanded(
child: Column(
children: [
Text(
"Tombol Kotak 2",
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 6),
ElevatedButton(
onPressed: _toggleButton2,
style: ElevatedButton.styleFrom(
backgroundColor:
button2 ? Colors.green : Colors.red,
minimumSize: const Size(80, 36),
padding: const EdgeInsets.symmetric(
horizontal: 8,
),
),
child: Text(
button2 ? 'ON' : 'OFF',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
],
),
const SizedBox(height: 16),
// Row Buzzer & Lampu (status read-only)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Buzzer
Expanded(
child: Column(
children: [
Text(
"Buzzer",
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 6),
Container(
alignment: Alignment.center,
decoration: BoxDecoration(
color: buzzer ? Colors.green : Colors.red,
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(
buzzer ? 'ON' : 'OFF',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
const SizedBox(width: 16),
// Lampu
Expanded(
child: Column(
children: [
Text(
"Lampu",
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 6),
Container(
alignment: Alignment.center,
decoration: BoxDecoration(
color: lampu ? Colors.green : Colors.red,
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(
lampu ? 'ON' : 'OFF',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
],
),
const SizedBox(height: 20),
// Info Text
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
vertical: 16,
horizontal: 20,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Text(
"Info :\n$infoText",
textAlign: TextAlign.center,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
height: 1.4,
),
),
),
],
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,408 @@
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_database/firebase_database.dart';
import 'bottom_navigation.dart';
import 'dart:async';
class JadwalKotakObat1Screen extends StatefulWidget {
const JadwalKotakObat1Screen({super.key});
@override
State<JadwalKotakObat1Screen> createState() => _JadwalKotakObat1ScreenState();
}
class _JadwalKotakObat1ScreenState extends State<JadwalKotakObat1Screen> {
final DatabaseReference _database = FirebaseDatabase.instance.ref();
final String? uid = FirebaseAuth.instance.currentUser?.uid;
StreamSubscription<DatabaseEvent>? _jadwalSubscription;
List<Map<String, String>> _jadwalList = [];
bool _showInputIsiUlang = false;
final TextEditingController _isiUlangController = TextEditingController();
@override
void initState() {
super.initState();
_loadJadwalObat1();
}
void _loadJadwalObat1() {
_jadwalSubscription = _database.child('jadwal/kotak1').onValue.listen((
event,
) {
final snapshot = event.snapshot;
final data = snapshot.value;
if (data is Map) {
final List<Map<String, String>> jadwal = [];
// Ambil nama obat dari path "jadwal/kotak1/obat1"
final namaObat = data['obat1']?.toString() ?? 'nama obat';
data.forEach((key, value) {
if (key != 'jumlahObat' &&
key != 'userId' &&
key != 'obat1' &&
value is Map) {
final jam = value['jam']?.toString().padLeft(2, '0');
final menit = value['menit']?.toString().padLeft(2, '0');
if (jam != null && menit != null) {
jadwal.add({'jam': '$jam:$menit', 'obat': namaObat});
}
}
});
jadwal.sort((a, b) => a['jam']!.compareTo(b['jam']!));
if (!mounted) return;
setState(() {
_jadwalList = jadwal;
});
} else {
if (!mounted) return;
setState(() {
_jadwalList = [];
});
}
});
}
@override
void dispose() {
_jadwalSubscription?.cancel();
_isiUlangController.dispose();
super.dispose();
}
Future<void> _hapusJadwal(String jamString) async {
if (uid == null) return;
final ref = _database.child('jadwal/kotak1');
final snapshot = await ref.get();
if (snapshot.exists && snapshot.value is Map) {
final Map<dynamic, dynamic> data = Map<dynamic, dynamic>.from(
snapshot.value as Map,
);
String? keyToDelete;
data.forEach((key, value) {
if (value is Map &&
value.containsKey('jam') &&
value.containsKey('menit')) {
final jam = value['jam'].toString().padLeft(2, '0');
final menit = value['menit'].toString().padLeft(2, '0');
final waktu = '$jam:$menit';
if (waktu == jamString) {
keyToDelete = key.toString();
}
}
});
if (keyToDelete != null) {
// Hapus jadwal tertentu
await ref.child(keyToDelete!).remove();
// Ambil ulang data setelah penghapusan
final updatedSnapshot = await ref.get();
if (updatedSnapshot.exists && updatedSnapshot.value is Map) {
final updatedData = Map<dynamic, dynamic>.from(
updatedSnapshot.value as Map,
);
// Cek apakah masih ada jadwal tersisa (yang berbentuk Map dan punya jam/menit)
final masihAdaJadwal = updatedData.values.any(
(item) =>
item is Map &&
item.containsKey('jam') &&
item.containsKey('menit'),
);
if (!masihAdaJadwal) {
// Tidak ada lagi jadwal, hapus field tambahan
await ref.child('jumlahObat').remove();
await ref.child('obat1').remove();
await ref.child('userId').remove();
}
}
}
}
}
void _navigateToAturJadwal1() {
Navigator.pushNamed(context, '/aturJadwal1');
}
void _toggleIsiUlang() {
setState(() => _showInputIsiUlang = !_showInputIsiUlang);
}
Future<void> _simpanIsiUlang() async {
final jumlahText = _isiUlangController.text.trim();
final jumlahBaru = int.tryParse(jumlahText);
if (jumlahBaru == null || jumlahBaru <= 0) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Masukkan jumlah yang valid')));
return;
}
final ref = _database.child('jadwal/kotak1/jumlahObat');
final snapshot = await ref.get();
if (!snapshot.exists || snapshot.value == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Masukkan jadwal terlebih dahulu')),
);
return;
}
int existing = 0;
if (snapshot.value is int) {
existing = snapshot.value as int;
} else if (snapshot.value is String) {
existing = int.tryParse(snapshot.value as String) ?? 0;
}
if (existing + jumlahBaru > 15) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Jumlah obat tidak boleh lebih dari 15')),
);
return;
}
await ref.set(existing + jumlahBaru);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Obat berhasil diisi ulang')));
setState(() {
_showInputIsiUlang = false;
_isiUlangController.clear();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF3FA535),
body: Column(
children: [
SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
const SizedBox(width: 8),
const Text(
'Jadwal Kotak Obat 1',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
),
),
Expanded(
child: Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(30),
topRight: Radius.circular(30),
),
),
child: Column(
children: [
Expanded(
child: ListView.builder(
itemCount: _jadwalList.length,
itemBuilder: (context, index) {
final item = _jadwalList[index];
final jam = item['jam'] ?? '';
final obat = item['obat'] ?? 'nama obat';
return Padding(
padding: const EdgeInsets.symmetric(vertical: 5.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
"${index + 1}. “$obat$jam",
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
),
),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
),
onPressed: () => _hapusJadwal(jam),
child: const Text(
'Hapus',
style: TextStyle(color: Colors.white),
),
),
],
),
);
},
),
),
if (_showInputIsiUlang) ...[
TextField(
controller: _isiUlangController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: 'Masukkan jumlah obat',
border: OutlineInputBorder(),
),
),
SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
OutlinedButton(
onPressed: () {
setState(() {
_showInputIsiUlang = false;
_isiUlangController.clear();
});
},
child: Text('Batal'),
),
ElevatedButton(
onPressed: _simpanIsiUlang,
child: Text('Simpan'),
),
],
),
] else
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: _navigateToAturJadwal1,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF3FA535),
),
child: const Text(
"Tambah Jadwal Obat",
style: TextStyle(color: Colors.white),
),
),
const SizedBox(width: 12),
ElevatedButton(
onPressed: _toggleIsiUlang,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
),
child: const Text(
"Isi Ulang Obat 1",
style: TextStyle(color: Colors.white),
),
),
],
),
],
),
),
),
],
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: 0, // sesuaikan dengan index aktif halaman sekarang
type: BottomNavigationBarType.fixed,
selectedItemColor: const Color(0xFF3FA535),
unselectedItemColor: Colors.grey.shade600,
showUnselectedLabels: true,
selectedLabelStyle: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
unselectedLabelStyle: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 12,
),
elevation: 8,
backgroundColor: Colors.white,
iconSize: 28,
onTap: (index) {
switch (index) {
case 0:
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => MainNavigation(currentIndex: 0),
),
);
break;
case 1:
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => MainNavigation(currentIndex: 1),
),
);
break;
case 2:
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => MainNavigation(currentIndex: 2),
),
);
break;
case 3:
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => MainNavigation(currentIndex: 3),
),
);
break;
}
},
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
activeIcon: Icon(Icons.home),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.history_outlined),
activeIcon: Icon(Icons.history),
label: 'History',
),
BottomNavigationBarItem(
icon: Icon(Icons.notifications_outlined),
activeIcon: Icon(Icons.notifications),
label: 'Notification',
),
BottomNavigationBarItem(
icon: Icon(Icons.person_outline),
activeIcon: Icon(Icons.person),
label: 'Account',
),
],
),
);
}
}

View File

@ -0,0 +1,408 @@
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_database/firebase_database.dart';
import 'bottom_navigation.dart';
import 'dart:async';
class JadwalKotakObat2Screen extends StatefulWidget {
const JadwalKotakObat2Screen({super.key});
@override
State<JadwalKotakObat2Screen> createState() => _JadwalKotakObat2ScreenState();
}
class _JadwalKotakObat2ScreenState extends State<JadwalKotakObat2Screen> {
final DatabaseReference _database = FirebaseDatabase.instance.ref();
final String? uid = FirebaseAuth.instance.currentUser?.uid;
StreamSubscription<DatabaseEvent>? _jadwalSubscription;
List<Map<String, String>> _jadwalList = [];
bool _showInputIsiUlang = false;
final TextEditingController _isiUlangController = TextEditingController();
@override
void initState() {
super.initState();
_loadJadwalObat2();
}
void _loadJadwalObat2() {
_jadwalSubscription = _database.child('jadwal/kotak2').onValue.listen((
event,
) {
final snapshot = event.snapshot;
final data = snapshot.value;
if (data is Map) {
final List<Map<String, String>> jadwal = [];
// Ambil nama obat dari path "jadwal/kotak1/obat1"
final namaObat = data['obat2']?.toString() ?? 'nama obat';
data.forEach((key, value) {
if (key != 'jumlahObat' &&
key != 'userId' &&
key != 'obat2' &&
value is Map) {
final jam = value['jam']?.toString().padLeft(2, '0');
final menit = value['menit']?.toString().padLeft(2, '0');
if (jam != null && menit != null) {
jadwal.add({'jam': '$jam:$menit', 'obat': namaObat});
}
}
});
jadwal.sort((a, b) => a['jam']!.compareTo(b['jam']!));
if (!mounted) return;
setState(() {
_jadwalList = jadwal;
});
} else {
if (!mounted) return;
setState(() {
_jadwalList = [];
});
}
});
}
@override
void dispose() {
_jadwalSubscription?.cancel(); // batalkan listener Firebase
_isiUlangController.dispose(); // dispose controller text juga
super.dispose();
}
Future<void> _hapusJadwal(String jamString) async {
if (uid == null) return;
final ref = _database.child('jadwal/kotak2');
final snapshot = await ref.get();
if (snapshot.exists && snapshot.value is Map) {
final Map<dynamic, dynamic> data = Map<dynamic, dynamic>.from(
snapshot.value as Map,
);
String? keyToDelete;
data.forEach((key, value) {
if (value is Map &&
value.containsKey('jam') &&
value.containsKey('menit')) {
final jam = value['jam'].toString().padLeft(2, '0');
final menit = value['menit'].toString().padLeft(2, '0');
final waktu = '$jam:$menit';
if (waktu == jamString) {
keyToDelete = key.toString();
}
}
});
if (keyToDelete != null) {
// Hapus jadwal tertentu
await ref.child(keyToDelete!).remove();
// Ambil ulang data setelah penghapusan
final updatedSnapshot = await ref.get();
if (updatedSnapshot.exists && updatedSnapshot.value is Map) {
final updatedData = Map<dynamic, dynamic>.from(
updatedSnapshot.value as Map,
);
// Cek apakah masih ada jadwal tersisa (yang berbentuk Map dan punya jam/menit)
final masihAdaJadwal = updatedData.values.any(
(item) =>
item is Map &&
item.containsKey('jam') &&
item.containsKey('menit'),
);
if (!masihAdaJadwal) {
// Tidak ada lagi jadwal, hapus field tambahan
await ref.child('jumlahObat').remove();
await ref.child('obat2').remove();
await ref.child('userId').remove();
}
}
}
}
}
void _navigateToAturJadwal2() {
Navigator.pushNamed(context, '/aturJadwal2');
}
void _toggleIsiUlang() {
setState(() => _showInputIsiUlang = !_showInputIsiUlang);
}
Future<void> _simpanIsiUlang() async {
final jumlahText = _isiUlangController.text.trim();
final jumlahBaru = int.tryParse(jumlahText);
if (jumlahBaru == null || jumlahBaru <= 0) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Masukkan jumlah yang valid')));
return;
}
final ref = _database.child('jadwal/kotak2/0/jumlahObat');
final snapshot = await ref.get();
if (!snapshot.exists || snapshot.value == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Masukkan jadwal terlebih dahulu')),
);
return;
}
int existing = 0;
if (snapshot.value is int) {
existing = snapshot.value as int;
} else if (snapshot.value is String) {
existing = int.tryParse(snapshot.value as String) ?? 0;
}
if (existing + jumlahBaru > 15) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Jumlah obat tidak boleh lebih dari 15')),
);
return;
}
await ref.set(existing + jumlahBaru);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Obat berhasil diisi ulang')));
setState(() {
_showInputIsiUlang = false;
_isiUlangController.clear();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF3FA535),
body: Column(
children: [
SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
const SizedBox(width: 8),
const Text(
'Jadwal Kotak Obat 2',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
),
),
Expanded(
child: Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(30),
topRight: Radius.circular(30),
),
),
child: Column(
children: [
Expanded(
child: ListView.builder(
itemCount: _jadwalList.length,
itemBuilder: (context, index) {
final item = _jadwalList[index];
final jam = item['jam'] ?? '';
final obat = item['obat'] ?? 'nama obat';
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
"${index + 1}. “$obat$jam",
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
),
),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
),
onPressed: () => _hapusJadwal(jam),
child: const Text(
'Hapus',
style: TextStyle(color: Colors.white),
),
),
],
),
);
},
),
),
if (_showInputIsiUlang) ...[
TextField(
controller: _isiUlangController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: 'Masukkan jumlah obat',
border: OutlineInputBorder(),
),
),
SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
OutlinedButton(
onPressed: () {
setState(() {
_showInputIsiUlang = false;
_isiUlangController.clear();
});
},
child: Text('Batal'),
),
ElevatedButton(
onPressed: _simpanIsiUlang,
child: Text('Simpan'),
),
],
),
] else
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: _navigateToAturJadwal2,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF3FA535),
),
child: const Text(
"Tambah Jadwal Obat",
style: TextStyle(color: Colors.white),
),
),
const SizedBox(width: 12),
ElevatedButton(
onPressed: _toggleIsiUlang,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
),
child: const Text(
"Isi Ulang Obat 2",
style: TextStyle(color: Colors.white),
),
),
],
),
],
),
),
),
],
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: 0, // sesuaikan dengan index aktif halaman sekarang
type: BottomNavigationBarType.fixed,
selectedItemColor: const Color(0xFF3FA535),
unselectedItemColor: Colors.grey.shade600,
showUnselectedLabels: true,
selectedLabelStyle: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
unselectedLabelStyle: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 12,
),
elevation: 8,
backgroundColor: Colors.white,
iconSize: 28,
onTap: (index) {
switch (index) {
case 0:
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => MainNavigation(currentIndex: 0),
),
);
break;
case 1:
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => MainNavigation(currentIndex: 1),
),
);
break;
case 2:
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => MainNavigation(currentIndex: 2),
),
);
break;
case 3:
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => MainNavigation(currentIndex: 3),
),
);
break;
}
},
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
activeIcon: Icon(Icons.home),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.history_outlined),
activeIcon: Icon(Icons.history),
label: 'History',
),
BottomNavigationBarItem(
icon: Icon(Icons.notifications_outlined),
activeIcon: Icon(Icons.notifications),
label: 'Notification',
),
BottomNavigationBarItem(
icon: Icon(Icons.person_outline),
activeIcon: Icon(Icons.person),
label: 'Account',
),
],
),
);
}
}

245
lib/login_screen.dart Normal file
View File

@ -0,0 +1,245 @@
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_background_service/flutter_background_service.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'signup_screen.dart';
import 'firebase_auth_services.dart';
import 'toast.dart';
import 'bottom_navigation.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final FirebaseAuthService _auth = FirebaseAuthService();
final TextEditingController _emailController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
bool _isSigningIn = false;
bool _obscurePassword = true;
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
void _signIn() async {
setState(() => _isSigningIn = true);
String email = _emailController.text.trim();
String password = _passwordController.text.trim();
try {
UserCredential userCredential = await FirebaseAuth.instance
.signInWithEmailAndPassword(email: email, password: password);
setState(() => _isSigningIn = false);
if (userCredential.user != null) {
final user = FirebaseAuth.instance.currentUser;
if (user != null) {
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setString('userId', user.uid);
FlutterBackgroundService().startService();
print("✅ Login sukses & background service dimulai kembali");
print("✅ userId berhasil disimpan ke SharedPreferences: ${user.uid}");
}
showToast(message: "Login berhasil!");
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const MainNavigation()),
);
}
} on FirebaseAuthException catch (e) {
setState(() => _isSigningIn = false);
String errorMessage = "Terjadi kesalahan saat login.";
switch (e.code) {
case 'invalid-email':
errorMessage = "Format email tidak valid.";
break;
case 'user-disabled':
errorMessage = "Pengguna ini telah dinonaktifkan.";
break;
case 'user-not-found':
errorMessage = "Email belum terdaftar.";
break;
case 'wrong-password':
errorMessage = "Password salah.";
break;
case 'too-many-requests':
errorMessage = "Terlalu banyak percobaan login. Coba lagi nanti.";
break;
}
showToast(message: errorMessage);
} catch (e) {
setState(() => _isSigningIn = false);
showToast(message: "Terjadi kesalahan tidak terduga.");
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFFFFFFF),
body: Column(
children: [
Container(
width: double.infinity,
padding: const EdgeInsets.only(top: 80, bottom: 30),
decoration: const BoxDecoration(
color: Color(0xFF3FA535),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(40),
bottomRight: Radius.circular(40),
),
),
child: Column(
children: [
Image.asset(
'assets/images/logoputih.png',
width: 50,
height: 50,
),
const SizedBox(height: 10),
const Text(
'Sign in to Your Account',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 5),
const Text(
'Enter your email and password to sign in',
style: TextStyle(fontSize: 14, color: Colors.white70),
),
],
),
),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.grey.shade300,
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Sign In',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 20),
TextField(
controller: _emailController,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.email_outlined),
labelText: 'Email',
border: UnderlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: _passwordController,
obscureText: _obscurePassword,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.lock_outline),
labelText: 'Password',
border: const UnderlineInputBorder(),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
? Icons.visibility_off
: Icons.visibility,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
),
),
const SizedBox(height: 30),
ElevatedButton(
onPressed: _signIn,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF3FA535),
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
),
child:
_isSigningIn
? const CircularProgressIndicator(
color: Colors.white,
)
: const Text(
'Sign In',
style: TextStyle(
fontSize: 16,
color: Colors.white,
),
),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text("Don't have an account? "),
GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SignUpScreen(),
),
);
},
child: const Text(
'Sign Up',
style: TextStyle(
color: Color(0xFF3FA535),
fontWeight: FontWeight.bold,
),
),
),
],
),
],
),
),
),
),
],
),
);
}
}

223
lib/main.dart Normal file
View File

@ -0,0 +1,223 @@
import 'dart:async';
import 'dart:io';
import 'dart:ui';
//import 'package:apkcoba/bottom_navigation.dart';
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter_background_service/flutter_background_service.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
// Import halaman
import 'history_page.dart';
import 'notifikasi_page.dart';
import 'profile_page.dart';
import 'splash_screen.dart';
import 'login_screen.dart';
import 'signup_screen.dart';
import 'home_screen.dart';
import 'firebase_options.dart';
import 'jadwal_kotak_obat1_screen.dart';
import 'atur_jadwal1_screen.dart';
import 'jadwal_kotak_obat2_screen.dart';
import 'atur_jadwal2_screen.dart';
import 'notifikasi_service.dart';
import 'bottom_navigation.dart';
// import 'background_service.dart';
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// Firebase initialization untuk Web dan Non-Web
if (kIsWeb) {
await Firebase.initializeApp(
options: const FirebaseOptions(
apiKey: "AIzaSyAUVen3yiSUrNJw7HnRif0_IBmUdeStY0c",
appId: "1:531750565610:web:5acfbccca5e41ee8404993",
messagingSenderId: "531750565610",
projectId: "coba-smartmedibox",
databaseURL: "https://coba-smartmedibox-default-rtdb.firebaseio.com",
),
);
} else {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
}
await initializeService();
//await NotifikasiService.init(); // Tambahkan ini supaya plugin notifikasi lokal siap
//NotifikasiService.listenToAllNotifikasi();
//await initializeNotifications();
//await initializeService();
runApp(const MyApp());
}
Future<void> initializeService() async {
final service = FlutterBackgroundService();
/// OPTIONAL, using custom notification channel id
const AndroidNotificationChannel channel = AndroidNotificationChannel(
'my_foreground', // id
'MY FOREGROUND SERVICE', // title
description:
'This channel is used for important notifications.', // description
importance: Importance.low, // importance must be at low or higher level
);
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('@mipmap/ic_launcher'); // TANPA .png
const InitializationSettings initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
);
await flutterLocalNotificationsPlugin.initialize(initializationSettings);
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>()
?.createNotificationChannel(channel);
await service.configure(
androidConfiguration: AndroidConfiguration(
// this will be executed when app is in foreground or background in separated isolate
onStart: onStart,
// auto start service
autoStart: true,
isForegroundMode: true,
notificationChannelId: 'my_foreground',
initialNotificationTitle: 'Smart Medicine Box',
initialNotificationContent: 'Medibox Sedang Berjalan',
foregroundServiceNotificationId: 888,
foregroundServiceTypes: [AndroidForegroundType.location],
),
iosConfiguration: IosConfiguration(
// auto start service
autoStart: true,
// this will be executed when app is in foreground in separated isolate
onForeground: onStart,
// you have to enable background fetch capability on xcode project
onBackground: onIosBackground,
),
);
}
// to ensure this is executed
// run app from xcode, then from xcode menu, select Simulate Background Fetch
@pragma('vm:entry-point')
Future<bool> onIosBackground(ServiceInstance service) async {
WidgetsFlutterBinding.ensureInitialized();
DartPluginRegistrant.ensureInitialized();
SharedPreferences preferences = await SharedPreferences.getInstance();
await preferences.reload();
final log = preferences.getStringList('log') ?? <String>[];
log.add(DateTime.now().toIso8601String());
await preferences.setStringList('log', log);
return true;
}
@pragma('vm:entry-point')
void onStart(ServiceInstance service) async {
DartPluginRegistrant.ensureInitialized();
await Firebase.initializeApp();
print('✅ Firebase initialized');
SharedPreferences prefs = await SharedPreferences.getInstance();
String? userId;
int retry = 0;
while ((userId = prefs.getString('userId')) == null && retry < 30) {
print("⏳ Menunggu userId tersimpan... [percobaan ke-$retry]");
await Future.delayed(Duration(seconds: 2));
await prefs.reload();
retry++;
}
if (userId == null) {
print(
"❌ Gagal mendapatkan userId setelah beberapa percobaan. onStart dihentikan.",
);
return;
}
print("✅ userId ditemukan: $userId");
await NotifikasiService.init();
if (userId != null) {
print('✅ Memulai listener notifikasi...');
await NotifikasiService.listenToAllNotifikasiWithUserId(userId);
syncTimestampIfAuthorizedWithListener(userId);
}
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
if (service is AndroidServiceInstance) {
service.on('setAsForeground').listen((event) {
service.setAsForegroundService();
});
service.on('setAsBackground').listen((event) {
service.setAsBackgroundService();
});
}
service.on('stopService').listen((event) {
service.stopSelf();
});
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: navigatorKey,
title: 'SmartMediBox',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
useMaterial3: true,
),
home: const SplashScreen(),
routes: {
'/login': (context) => const LoginScreen(),
'/signUp': (context) => const SignUpScreen(),
'/home': (context) => const MainNavigation(currentIndex: 0),
'/home2': (context) => HomeScreen(username: '', onSignOut: () {}),
'/jadwalKotakObat1': (context) => const JadwalKotakObat1Screen(),
'/aturJadwal1': (context) => const AturJadwal1Screen(),
'/jadwalKotakObat2': (context) => const JadwalKotakObat2Screen(),
'/aturJadwal2': (context) => const AturJadwal2Screen(),
'/historyPage': (context) => const HistoryPage(),
'/notifikasiPage': (context) => const NotifikasiPage(),
'/profilePage': (context) => const ProfilePage(),
},
);
}
}

449
lib/notifikasi_page.dart Normal file
View File

@ -0,0 +1,449 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'bottom_navigation.dart';
class NotifikasiPage extends StatefulWidget {
const NotifikasiPage({super.key});
@override
State<NotifikasiPage> createState() => _NotifikasiPageState();
}
class _NotifikasiPageState extends State<NotifikasiPage> {
final FirebaseAuth _auth = FirebaseAuth.instance;
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
final List<DocumentSnapshot> _notifikasiDocs = [];
final int _limit = 10;
DocumentSnapshot? _lastDocument;
bool _isLoadingMore = false;
bool _hasMore = true;
bool _isInitialLoading = true;
final ScrollController _scrollController = ScrollController();
// Filter state
int? filterDay;
int? filterMonth;
int? filterYear;
int? filterHour;
int? filterMinute;
@override
void initState() {
super.initState();
_loadInitialNotifikasi();
_scrollController.addListener(_scrollListener);
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
void _scrollListener() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 100 &&
!_isLoadingMore &&
_hasMore) {
_loadMoreNotifikasi();
}
}
Future<void> _loadInitialNotifikasi() async {
final user = _auth.currentUser;
if (user == null) return;
try {
final querySnapshot =
await _firestore
.collection('users')
.doc(user.uid)
.collection('notifikasi')
.orderBy('timestamp', descending: true)
.limit(_limit)
.get();
setState(() {
_notifikasiDocs.clear();
_notifikasiDocs.addAll(querySnapshot.docs);
_lastDocument =
querySnapshot.docs.isNotEmpty ? querySnapshot.docs.last : null;
_hasMore = querySnapshot.docs.length == _limit;
_isInitialLoading = false;
});
} catch (e) {
print('Error loading initial notifications: $e');
setState(() {
_isInitialLoading = false;
});
}
}
Future<void> _loadMoreNotifikasi() async {
if (!_hasMore || _isLoadingMore || _lastDocument == null) return;
setState(() => _isLoadingMore = true);
final user = _auth.currentUser;
if (user == null) return;
try {
final querySnapshot =
await _firestore
.collection('users')
.doc(user.uid)
.collection('notifikasi')
.orderBy('timestamp', descending: true)
.startAfterDocument(_lastDocument!)
.limit(_limit)
.get();
setState(() {
_notifikasiDocs.addAll(querySnapshot.docs);
_lastDocument =
querySnapshot.docs.isNotEmpty
? querySnapshot.docs.last
: _lastDocument;
_hasMore = querySnapshot.docs.length == _limit;
_isLoadingMore = false;
});
} catch (e) {
print('Error loading more notifications: $e');
setState(() {
_isLoadingMore = false;
});
}
}
String _formatDate(Timestamp? timestamp) {
if (timestamp == null) return '';
final dateTime = timestamp.toDate().toLocal();
final time =
"${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}";
final date =
"${dateTime.day.toString().padLeft(2, '0')}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.year}";
return "$time $date";
}
Future<void> _deleteNotifikasi(String docId) async {
final user = _auth.currentUser;
if (user == null) return;
await _firestore
.collection('users')
.doc(user.uid)
.collection('notifikasi')
.doc(docId)
.delete();
setState(() {
_notifikasiDocs.removeWhere((doc) => doc.id == docId);
});
}
Future<void> _refresh() async {
setState(() {
_isInitialLoading = true;
_hasMore = true;
_lastDocument = null;
});
await _loadInitialNotifikasi();
}
List<DocumentSnapshot> applyFilter(List<DocumentSnapshot> docs) {
return docs.where((doc) {
final data = doc.data() as Map<String, dynamic>?;
if (data == null) return false;
final timestamp = data['timestamp'] as Timestamp?;
if (timestamp == null) return false;
final dt = timestamp.toDate();
if (filterYear != null && dt.year != filterYear) return false;
if (filterMonth != null && dt.month != filterMonth) return false;
if (filterDay != null && dt.day != filterDay) return false;
if (filterHour != null && dt.hour != filterHour) return false;
if (filterMinute != null && dt.minute != filterMinute) return false;
return true;
}).toList();
}
Widget buildDropdown(
String label,
List<int> items,
int? selectedValue,
ValueChanged<int?> onChanged,
) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('$label: '),
DropdownButton<int?>(
value: selectedValue,
hint: const Text('Semua'),
items:
[null, ...items].map((val) {
return DropdownMenuItem<int?>(
value: val,
child: Text(val?.toString() ?? 'Semua'),
);
}).toList(),
onChanged: onChanged,
),
],
);
}
Widget buildFilterRow() {
final currentYear = DateTime.now().year;
return Padding(
padding: const EdgeInsets.all(8.0),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
buildDropdown(
'Tahun',
List.generate(10, (i) => currentYear - i),
filterYear,
(val) => setState(() => filterYear = val),
),
const SizedBox(width: 8),
buildDropdown(
'Bulan',
List.generate(12, (i) => i + 1),
filterMonth,
(val) => setState(() => filterMonth = val),
),
const SizedBox(width: 8),
buildDropdown(
'Tanggal',
List.generate(31, (i) => i + 1),
filterDay,
(val) => setState(() => filterDay = val),
),
const SizedBox(width: 8),
buildDropdown(
'Jam',
List.generate(24, (i) => i),
filterHour,
(val) => setState(() => filterHour = val),
),
const SizedBox(width: 8),
buildDropdown(
'Menit',
List.generate(60, (i) => i),
filterMinute,
(val) => setState(() => filterMinute = val),
),
const SizedBox(width: 12),
ElevatedButton(
onPressed: () {
setState(() {
filterDay = null;
filterMonth = null;
filterYear = null;
filterHour = null;
filterMinute = null;
});
},
child: const Text('Reset Filter'),
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
final user = _auth.currentUser;
if (user == null) {
return const Center(child: Text("User belum login."));
}
final filteredNotifikasi = applyFilter(_notifikasiDocs);
return Scaffold(
backgroundColor: const Color(0xFFE9F5EC),
body: SafeArea(
child: Column(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
alignment: Alignment.center,
decoration: const BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(32),
bottomRight: Radius.circular(32),
),
),
child: const Text(
'Notifikasi',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
buildFilterRow(),
Expanded(
child:
_isInitialLoading
? const Center(child: CircularProgressIndicator())
: filteredNotifikasi.isEmpty
? const Center(child: Text("Belum ada notifikasi."))
: RefreshIndicator(
onRefresh: _refresh,
child: ListView.builder(
controller: _scrollController,
physics: const AlwaysScrollableScrollPhysics(),
itemCount:
filteredNotifikasi.length + (_hasMore ? 1 : 0),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
itemBuilder: (context, index) {
if (index >= filteredNotifikasi.length) {
return const Padding(
padding: EdgeInsets.all(16.0),
child: Center(
child: CircularProgressIndicator(),
),
);
}
final data =
filteredNotifikasi[index].data()
as Map<String, dynamic>;
final docId = filteredNotifikasi[index].id;
final judul = data['judul'] ?? 'Tidak diketahui';
final pesan = data['pesan'] ?? '';
final timestamp = data['timestamp'] as Timestamp?;
return Dismissible(
key: Key(docId),
direction: DismissDirection.endToStart,
background: Container(
padding: const EdgeInsets.symmetric(
horizontal: 20,
),
alignment: Alignment.centerRight,
color: Colors.red,
child: const Icon(
Icons.delete,
color: Colors.white,
),
),
confirmDismiss: (_) async {
return await showDialog(
context: context,
builder:
(_) => AlertDialog(
title: const Text("Hapus Notifikasi"),
content: const Text(
"Apakah kamu yakin ingin menghapus notifikasi ini?",
),
actions: [
TextButton(
onPressed:
() => Navigator.of(
context,
).pop(false),
child: const Text("Batal"),
),
TextButton(
onPressed:
() => Navigator.of(
context,
).pop(true),
child: const Text(
"Hapus",
style: TextStyle(
color: Colors.red,
),
),
),
],
),
);
},
onDismissed: (_) => _deleteNotifikasi(docId),
child: Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.symmetric(
vertical: 14,
horizontal: 16,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Row untuk judul dan pesan dengan jarak yang lebih rapat
Row(
mainAxisAlignment:
MainAxisAlignment.start,
children: [
// Judul
Text(
judul,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
const SizedBox(
width: 8,
), // Menambahkan sedikit jarak antara judul dan pesan
// Pesan
Expanded(
child: Text(
pesan,
style: const TextStyle(
fontSize: 16,
),
overflow:
TextOverflow
.ellipsis, // Membatasi panjang pesan yang ditampilkan
),
),
],
),
const SizedBox(height: 6),
// Waktu tetap di bawah judul dan pesan
Text(
"Waktu : ${_formatDate(timestamp)}",
style: const TextStyle(
fontSize: 14,
color: Colors.black54,
),
),
],
),
),
);
},
),
),
),
],
),
),
);
}
}

222
lib/notifikasi_service.dart Normal file
View File

@ -0,0 +1,222 @@
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:shared_preferences/shared_preferences.dart';
class NotifikasiService {
static final FlutterLocalNotificationsPlugin _notificationsPlugin =
FlutterLocalNotificationsPlugin();
// Flag internal untuk mencegah pengiriman notifikasi duplikat
static final Map<String, bool> _notifikasiTerkirim = {
'alarm1': false,
'alarm2': false,
'stok0_kotak1': false,
'stok0_kotak2': false,
'stok3_kotak1': false,
'stok3_kotak2': false,
};
static Future<void> init() async {
const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('@mipmap/ic_launcher');
const InitializationSettings initializationSettings =
InitializationSettings(android: initializationSettingsAndroid);
await _notificationsPlugin.initialize(initializationSettings);
}
static Future<void> simpanKeFirestore(
String userId,
String title,
String body,
) async {
final firestore = FirebaseFirestore.instance;
await firestore
.collection('users')
.doc(userId)
.collection('notifikasi')
.add({
'judul': title,
'pesan': body,
'timestamp': FieldValue.serverTimestamp(),
});
}
static Future<void> showNotification(String title, String body) async {
print('showNotification dipanggil dengan judul: $title, body: $body');
// Dihapus karena penyimpanan ke Firestore sudah dilakukan di tempat lain
// final user = FirebaseAuth.instance.currentUser;
// if (user != null) {
// await simpanKeFirestore(user.uid, title, body);
// }
const AndroidNotificationDetails androidDetails =
AndroidNotificationDetails(
'obat_channel_id',
'Obat Channel',
importance: Importance.max,
priority: Priority.high,
);
const NotificationDetails platformDetails = NotificationDetails(
android: androidDetails,
);
await _notificationsPlugin.show(
DateTime.now().millisecondsSinceEpoch ~/ 1000,
title,
body,
platformDetails,
);
}
static Future<void> listenToAllNotifikasiWithUserId(String userId) async {
final ref = FirebaseDatabase.instance.ref();
for (int kotak = 1; kotak <= 2; kotak++) {
ref.child('status/alarm$kotak').onValue.listen((event) async {
print("Alarm $kotak listener dipanggil");
bool alarmAktif = event.snapshot.value == true;
final alarmKey = 'alarm$kotak';
final userIdSnapshot =
await ref.child('jadwal/kotak$kotak/userId').get();
if (!userIdSnapshot.exists || userIdSnapshot.value != userId) {
_notifikasiTerkirim[alarmKey] = false;
return;
}
if (alarmAktif) {
if (!_notifikasiTerkirim[alarmKey]!) {
String title = 'Alarm Kotak $kotak Aktif';
String body = 'Waktunya minum obat';
await showNotification(title, body);
await simpanKeFirestore(userId, title, body);
_notifikasiTerkirim[alarmKey] = true;
}
} else {
_notifikasiTerkirim[alarmKey] = false;
}
});
}
for (int kotak = 1; kotak <= 2; kotak++) {
ref.child('jadwal/kotak$kotak/jumlahObat').onValue.listen((event) async {
final jumlahValue = event.snapshot.value;
int jumlah = 0;
if (jumlahValue is int) {
jumlah = jumlahValue;
} else if (jumlahValue is String) {
jumlah = int.tryParse(jumlahValue) ?? 0;
}
final userIdSnapshot =
await ref.child('jadwal/kotak$kotak/userId').get();
if (!userIdSnapshot.exists || userIdSnapshot.value != userId) {
for (int i = 0; i <= 3; i++) {
_notifikasiTerkirim['stok${i}_kotak$kotak'] = false;
}
return;
}
if (jumlah == 0) {
final key = 'stok0_kotak$kotak';
if (!_notifikasiTerkirim[key]!) {
String title = 'Obat Kotak $kotak Habis';
String body = 'Isi ulang obat segera!';
await showNotification(title, body);
await simpanKeFirestore(userId, title, body);
_notifikasiTerkirim[key] = true;
}
for (int i = 1; i <= 3; i++) {
_notifikasiTerkirim['stok${i}_kotak$kotak'] = false;
}
} else if (jumlah > 0 && jumlah <= 3) {
final key = 'stok$jumlah\_kotak$kotak';
if (!_notifikasiTerkirim[key]!) {
String title = 'Obat Sisa di Kotak $kotak';
String body = 'Obat tinggal $jumlah, waktunya isi ulang';
await showNotification(title, body);
await simpanKeFirestore(userId, title, body);
_notifikasiTerkirim[key] = true;
}
for (int i = 1; i <= 3; i++) {
if (i != jumlah) {
_notifikasiTerkirim['stok${i}_kotak$kotak'] = false;
}
}
_notifikasiTerkirim['stok0_kotak$kotak'] = false;
} else {
for (int i = 0; i <= 3; i++) {
_notifikasiTerkirim['stok${i}_kotak$kotak'] = false;
}
}
});
}
}
}
void syncTimestampIfAuthorizedWithListener(String userId) {
final ref = FirebaseDatabase.instance.ref();
ref.child('status/kotak1').onValue.listen((event) async {
final now = DateTime.now();
final timestamp = now.toIso8601String();
final statusValue = event.snapshot.value;
if (statusValue != true) return;
final userIdSnap1 = await ref.child('jadwal/kotak1/userId').get();
final obatSnap1 = await ref.child('jadwal/kotak1/obat1').get();
if (userIdSnap1.value != userId) return;
final namaObat = obatSnap1.value?.toString() ?? 'Tidak diketahui';
await FirebaseFirestore.instance
.collection('users')
.doc(userId)
.collection('history')
.add({
'timestamp': timestamp,
'namaObat': namaObat,
'kotak': 'Kotak 1',
});
print('✅ History Kotak 1 dikirim: $namaObat');
await ref.child('status/kotak1').set(false);
});
ref.child('status/kotak2').onValue.listen((event) async {
final now = DateTime.now();
final timestamp = now.toIso8601String();
final statusValue = event.snapshot.value;
if (statusValue != true) return;
final userIdSnap2 = await ref.child('jadwal/kotak2/userId').get();
final obatSnap2 = await ref.child('jadwal/kotak2/obat2').get();
if (userIdSnap2.value != userId) return;
final namaObat = obatSnap2.value?.toString() ?? 'Tidak diketahui';
await FirebaseFirestore.instance
.collection('users')
.doc(userId)
.collection('history')
.add({
'timestamp': timestamp,
'namaObat': namaObat,
'kotak': 'Kotak 2',
});
print('✅ History Kotak 2 dikirim: $namaObat');
await ref.child('status/kotak2').set(false);
});
}

279
lib/profile_page.dart Normal file
View File

@ -0,0 +1,279 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:flutter/material.dart';
import 'package:flutter_background_service/flutter_background_service.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'login_screen.dart';
import 'toast.dart';
class ProfilePage extends StatefulWidget {
const ProfilePage({super.key});
@override
State<ProfilePage> createState() => _ProfilePageState();
}
class _ProfilePageState extends State<ProfilePage> {
final FirebaseAuth _auth = FirebaseAuth.instance;
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
final DatabaseReference _dbRef = FirebaseDatabase.instance.ref();
String _username = '';
String _email = '';
String _obat1 = '-';
String _obat2 = '-';
String _jumlahObat1 = '-';
String _jumlahObat2 = '-';
bool _loading = true;
@override
void initState() {
super.initState();
_loadProfileData();
}
Future<void> _showLogoutConfirmationDialog() async {
final shouldLogout = await showDialog<bool>(
context: context,
builder:
(context) => AlertDialog(
title: const Text('Konfirmasi Logout'),
content: const Text(
'Apakah Anda yakin ingin logout dari akun ini?',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Tidak'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Ya'),
),
],
),
);
if (shouldLogout == true) {
_logout();
}
}
Future<void> _loadProfileData() async {
final user = _auth.currentUser;
if (user == null) {
setState(() {
_loading = false;
});
return;
}
final uid = user.uid;
final userDoc = await _firestore.collection('users').doc(uid).get();
final dataUser = userDoc.data();
final username =
dataUser != null && dataUser.containsKey('username')
? dataUser['username'] as String
: '';
final email =
dataUser != null && dataUser.containsKey('email')
? dataUser['email'] as String
: '';
Future<String> getObatIfUserMatch(
String kotak,
String obatKey,
String userIdPath,
) async {
final userIdSnap = await _dbRef.child('jadwal/$kotak/$userIdPath').get();
if (!userIdSnap.exists || userIdSnap.value != uid) return '-';
final obatSnap = await _dbRef.child('jadwal/$kotak/$obatKey').get();
if (!obatSnap.exists ||
(obatSnap.value == null ||
obatSnap.value.toString().trim().isEmpty)) {
return '-';
}
return obatSnap.value.toString();
}
Future<String> getJumlahObatIfUserMatch(
String kotak,
String jumlahKey,
String userIdPath,
) async {
final userIdSnap = await _dbRef.child('jadwal/$kotak/$userIdPath').get();
if (!userIdSnap.exists || userIdSnap.value != uid) return '-';
final jumlahSnap = await _dbRef.child('jadwal/$kotak/$jumlahKey').get();
if (!jumlahSnap.exists ||
jumlahSnap.value == null ||
jumlahSnap.value.toString().trim().isEmpty) {
return '-';
}
return jumlahSnap.value.toString();
}
final obat1 = await getObatIfUserMatch('kotak1', 'obat1', 'userId');
final obat2 = await getObatIfUserMatch('kotak2', 'obat2', 'userId');
final jumlah1 = await getJumlahObatIfUserMatch(
'kotak1',
'jumlahObat',
'userId',
);
final jumlah2 = await getJumlahObatIfUserMatch(
'kotak2',
'jumlahObat',
'userId',
);
setState(() {
_username = username;
_email = email;
_obat1 = obat1;
_obat2 = obat2;
_jumlahObat1 = jumlah1;
_jumlahObat2 = jumlah2;
_loading = false;
});
}
Future<void> _logout() async {
await FirebaseAuth.instance.signOut();
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.remove('userId');
FlutterBackgroundService().invoke("stopService");
print("✅ Logout berhasil dan background service dihentikan");
print('❌ userId dihapus dari SharedPreferences');
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const LoginScreen()),
);
showToast(message: "Successfully signed out");
}
Future<void> _refresh() async {
setState(() {
_loading = true;
});
await _loadProfileData();
}
Widget _buildField(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
const SizedBox(height: 6),
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: const [
BoxShadow(
color: Colors.black12,
blurRadius: 4,
offset: Offset(0, 2),
),
],
),
child: Text(
value,
style: const TextStyle(fontSize: 18, color: Colors.black87),
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
if (_loading) {
return const Scaffold(
backgroundColor: Color(0xFFE9F5EC),
body: Center(child: CircularProgressIndicator()),
);
}
return Scaffold(
backgroundColor: const Color(0xFFE9F5EC),
body: SafeArea(
child: Column(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
alignment: Alignment.center,
width: double.infinity,
decoration: const BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(32),
bottomRight: Radius.circular(32),
),
),
child: const Text(
'Profile',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
Expanded(
child: RefreshIndicator(
onRefresh: _refresh,
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 24,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildField('Username', _username),
_buildField('Email', _email),
_buildField('Nama Obat 1', _obat1),
_buildField('Nama Obat 2', _obat2),
_buildField(
'Jumlah Obat Tersisa di Kotak 1',
_jumlahObat1,
),
_buildField(
'Jumlah Obat Tersisa di Kotak 2',
_jumlahObat2,
),
const SizedBox(height: 30),
TextButton(
onPressed: _showLogoutConfirmationDialog,
style: TextButton.styleFrom(
foregroundColor: Colors.red,
textStyle: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
child: const Text('Logout Akun'),
),
],
),
),
),
),
],
),
),
);
}
}

298
lib/signup_screen.dart Normal file
View File

@ -0,0 +1,298 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'toast.dart';
import 'login_screen.dart';
class SignUpScreen extends StatefulWidget {
const SignUpScreen({super.key});
@override
State<SignUpScreen> createState() => _SignUpScreenState();
}
class _SignUpScreenState extends State<SignUpScreen> {
final TextEditingController _usernameController = TextEditingController();
final TextEditingController _emailController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
final TextEditingController _confirmPasswordController =
TextEditingController();
bool _obscurePassword = true;
bool _obscureConfirm = true;
bool _isSigningUp = false;
@override
void dispose() {
_usernameController.dispose();
_emailController.dispose();
_passwordController.dispose();
_confirmPasswordController.dispose();
super.dispose();
}
Future<void> _signUp() async {
String username = _usernameController.text.trim();
String email = _emailController.text.trim();
String password = _passwordController.text;
String confirmPassword = _confirmPasswordController.text;
if (username.isEmpty || email.isEmpty || password.isEmpty) {
showToast(message: "Semua field wajib diisi.");
return;
}
if (password != confirmPassword) {
showToast(message: "Konfirmasi password tidak cocok.");
return;
}
setState(() {
_isSigningUp = true;
});
try {
// 1. Buat akun dengan Firebase Auth
UserCredential userCredential = await FirebaseAuth.instance
.createUserWithEmailAndPassword(email: email, password: password);
// 2. Ambil UID pengguna
String uid = userCredential.user!.uid;
// 3. Simpan data user ke Firestore
await FirebaseFirestore.instance.collection('users').doc(uid).set({
'email': email,
'username': username,
'createdAt': Timestamp.now(),
});
showToast(message: "Akun berhasil dibuat!");
// 4. Navigasi ke Login Screen
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const LoginScreen()),
);
}
} on FirebaseAuthException catch (e) {
switch (e.code) {
case 'email-already-in-use':
showToast(message: "Email sudah digunakan.");
break;
case 'invalid-email':
showToast(message: "Format email tidak valid.");
break;
case 'weak-password':
showToast(message: "Password terlalu lemah (minimal 6 karakter).");
break;
default:
showToast(message: "Error: ${e.message}");
}
} catch (e) {
showToast(message: "Terjadi kesalahan saat mendaftar.");
} finally {
if (mounted) {
setState(() {
_isSigningUp = false;
});
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: Column(
children: [
// Header
Container(
width: double.infinity,
padding: const EdgeInsets.only(top: 80, bottom: 30),
decoration: const BoxDecoration(
color: Color(0xFF3FA535),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(40),
bottomRight: Radius.circular(40),
),
),
child: Column(
children: [
Image.asset(
'assets/images/logoputih.png',
width: 50,
height: 50,
),
const SizedBox(height: 10),
const Text(
'Sign Up to Your Account',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 5),
const Text(
'Enter your email and password to sign up',
style: TextStyle(fontSize: 14, color: Colors.white70),
),
],
),
),
// Form
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.grey.shade300,
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Sign Up',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 20),
// Username
TextField(
controller: _usernameController,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.person_outline),
labelText: 'Username',
border: UnderlineInputBorder(),
),
),
const SizedBox(height: 16),
// Email
TextField(
controller: _emailController,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.email_outlined),
labelText: 'Email',
border: UnderlineInputBorder(),
),
),
const SizedBox(height: 16),
// Password
TextField(
controller: _passwordController,
obscureText: _obscurePassword,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.lock_outline),
labelText: 'Password',
border: const UnderlineInputBorder(),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
? Icons.visibility_off
: Icons.visibility,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
),
),
const SizedBox(height: 16),
// Confirm Password
TextField(
controller: _confirmPasswordController,
obscureText: _obscureConfirm,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.lock_outline),
labelText: 'Confirm Password',
border: const UnderlineInputBorder(),
suffixIcon: IconButton(
icon: Icon(
_obscureConfirm
? Icons.visibility_off
: Icons.visibility,
),
onPressed: () {
setState(() {
_obscureConfirm = !_obscureConfirm;
});
},
),
),
),
const SizedBox(height: 30),
// Button
ElevatedButton(
onPressed: _signUp,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF3FA535),
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
),
child:
_isSigningUp
? const CircularProgressIndicator(
color: Colors.white,
)
: const Text(
'Sign Up',
style: TextStyle(
fontSize: 16,
color: Colors.white,
),
),
),
const SizedBox(height: 20),
// Redirect ke Login
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text("Already have an account? "),
GestureDetector(
onTap: () {
Navigator.pop(context);
},
child: const Text(
'Sign In',
style: TextStyle(
color: Color(0xFF3FA535),
fontWeight: FontWeight.bold,
),
),
),
],
),
],
),
),
),
),
],
),
);
}
}

57
lib/splash_screen.dart Normal file
View File

@ -0,0 +1,57 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
//import 'firebase_messaging_service.dart';
import 'login_screen.dart';
class SplashScreen extends StatefulWidget {
const SplashScreen({super.key});
@override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
@override
void initState() {
super.initState();
//FirebaseMessagingService.init(context);
Timer(const Duration(seconds: 3), () {
final user = FirebaseAuth.instance.currentUser;
if (user != null) {
// User sudah login
Navigator.pushNamed(context, '/home');
} else {
// User belum login
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const LoginScreen()),
);
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFDFF3DD),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset('assets/images/logowarna.png', width: 120, height: 120),
const SizedBox(height: 20),
const Text(
'SmartMediBox',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
],
),
),
);
}
}

14
lib/toast.dart Normal file
View File

@ -0,0 +1,14 @@
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
void showToast({required String message}) {
Fluttertoast.showToast(
msg: message,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
timeInSecForIosWeb: 1,
backgroundColor: Colors.blue,
textColor: Colors.white,
fontSize: 16.0,
);
}

1
linux/.gitignore vendored Normal file
View File

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

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