upload project skripsi

This commit is contained in:
aryaziz22 2026-07-07 09:58:33 +07:00
commit 250c4d1e50
221 changed files with 15317 additions and 0 deletions

22
.env.example Normal file
View File

@ -0,0 +1,22 @@
# Firebase Web Configuration
FIREBASE_API_KEY_WEB=your_web_api_key_here
FIREBASE_APP_ID_WEB=your_web_app_id_here
FIREBASE_MESSAGING_SENDER_ID=your_messaging_sender_id_here
FIREBASE_PROJECT_ID=your_project_id_here
FIREBASE_AUTH_DOMAIN=your_auth_domain_here
FIREBASE_STORAGE_BUCKET=your_storage_bucket_here
FIREBASE_MEASUREMENT_ID_WEB=your_web_measurement_id_here
# Firebase Android Configuration
FIREBASE_API_KEY_ANDROID=your_android_api_key_here
FIREBASE_APP_ID_ANDROID=your_android_app_id_here
# Firebase iOS Configuration
FIREBASE_API_KEY_IOS=your_ios_api_key_here
FIREBASE_APP_ID_IOS=your_ios_app_id_here
FIREBASE_IOS_BUNDLE_ID=your_ios_bundle_id_here
# Firebase Windows Configuration
FIREBASE_API_KEY_WINDOWS=your_windows_api_key_here
FIREBASE_APP_ID_WINDOWS=your_windows_app_id_here
FIREBASE_MEASUREMENT_ID_WINDOWS=your_windows_measurement_id_here

56
.gitignore vendored Normal file
View File

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

416
Full code esp32.py Normal file
View File

@ -0,0 +1,416 @@
import network # Library untuk koneksi Wi-Fi
import urequests # Library untuk HTTP request (API Firebase/Firestore)
import time # Library untuk jeda (delay) dan pengambilan waktu
import machine # Library untuk kontrol pin hardware ESP32
import ntptime # Library untuk sinkronisasi waktu dari server internet
# ==================================================
# 1. KONFIGURASI UTAMA
# ==================================================
# Kredensial Jaringan Wi-Fi
WIFI_SSID = "WARKOP"
WIFI_PASSWORD = "redi1234"
# Kredensial Firebase & Firestore
API_KEY = "AIzaSyCvv9MrSLOQPlarND0iq8vjH3nUANHcKMI"
PROJECT_ID = "hydronutrify"
DOC_ID_FASE = "kqAxWYNRWqZtfqihpo31b5C0vNv1" # ID Dokumen untuk siklus tanam
DOC_ID_DATA = "latest_monitoring" # ID Dokumen untuk data real-time
# Autentikasi Pengguna Firebase
USER_EMAIL = "oktagendeveloper@gmail.com"
USER_PASSWORD = "Mahasiswa22"
# Variabel Global untuk menyimpan token sesi (diisi otomatis saat login)
auth_token = ""
my_user_id = ""
# Pengaturan Waktu & Aktuator (Pompa)
PUMP_DURATION = 10 # Lama pompa menyala (10 detik)
DELAY_AFTER_PUMP = 30 # Jeda tunggu agar larutan nutrisi tercampur rata di bak
LOOP_INTERVAL = 3600 # Jeda sistem membaca sensor kembali (3600 detik = 1 Jam)
MAX_RETRY_NTP = 5 # Maksimal percobaan sinkronisasi waktu jika gagal
# Logika Modul Relay (Active High)
RELAY_ON = 1 # Angka 1 mengirim sinyal HIGH (Pompa Menyala)
RELAY_OFF = 0 # Angka 0 mengirim sinyal LOW (Pompa Mati)
# ==================================================
# 2. PARAMETER KALIBRASI SENSOR
# ==================================================
# Didapat dari rumus regresi linear (y = mx + c) saat kalibrasi
PH_SLOPE = -17.33919
PH_INTERCEPT = 9.59919
TDS_SLOPE = 1424.404800
TDS_INTERCEPT = -736.856200
# ==================================================
# 3. INISIALISASI HARDWARE
# ==================================================
print("=== INISIALISASI HARDWARE ===")
# Inisialisasi Sensor Analog (ADC)
ph_pin = machine.ADC(machine.Pin(35))
ph_pin.atten(machine.ADC.ATTN_11DB) # Setting range pembacaan tegangan 0 - 3.3V
tds_pin = machine.ADC(machine.Pin(34))
tds_pin.atten(machine.ADC.ATTN_11DB)
# Inisialisasi Pin Relay untuk Pompa
pin_nutrisi_a = machine.Pin(13, machine.Pin.OUT)
pin_nutrisi_b = machine.Pin(12, machine.Pin.OUT)
pin_air_baku = machine.Pin(14, machine.Pin.OUT)
pin_ph_up = machine.Pin(27, machine.Pin.OUT)
pin_ph_down = machine.Pin(26, machine.Pin.OUT)
# Dikelompokkan dalam dictionary agar mudah dipanggil
pumps = {
"nutrisi": [pin_nutrisi_a, pin_nutrisi_b],
"air_baku": pin_air_baku,
"ph_up": pin_ph_up,
"ph_down": pin_ph_down
}
# Keamanan: Matikan semua relay saat alat baru dinyalakan
# Diberi jeda 0.1 detik antar pin untuk mencegah lonjakan listrik (Brownout)
for pin in [pin_nutrisi_a, pin_nutrisi_b, pin_air_baku, pin_ph_up, pin_ph_down]:
pin.value(RELAY_OFF)
time.sleep(0.1)
print("Semua relay berhasil di-OFF-kan.\n")
# Variabel untuk filter rata-rata pembacaan pH
ph_buffer = []
BUFFER_SIZE = 10
# ==================================================
# 4. HELPER WIFI, LOGIN & NTP
# ==================================================
def connect_wifi():
"""Fungsi untuk menyambungkan ESP32 ke jaringan Wi-Fi."""
wlan = network.WLAN(network.STA_IF)
wlan.active(False) # Mematikan radio Wi-Fi sesaat untuk membersihkan memori (mencegah error internal state)
time.sleep(0.5)
wlan.active(True) # Menyalakan ulang radio Wi-Fi
if not wlan.isconnected():
wlan.connect(WIFI_SSID, WIFI_PASSWORD)
print("Connecting WiFi", end="")
timeout = 15
while not wlan.isconnected() and timeout > 0:
print(".", end="")
time.sleep(1)
timeout -= 1
if wlan.isconnected():
print("\nWiFi Connected!")
return True
return False
def login_firebase():
"""Fungsi untuk otentikasi (login) ke Firebase menggunakan Email & Password."""
global auth_token, my_user_id
url = f"https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key={API_KEY}"
payload = {"email": USER_EMAIL, "password": USER_PASSWORD, "returnSecureToken": True}
try:
res = urequests.post(url, json=payload)
data = res.json()
if res.status_code == 200:
auth_token, my_user_id = data["idToken"], data["localId"] # Menyimpan Token dan ID User
print("Login Firebase Berhasil!")
res.close()
return True
res.close()
except Exception as e:
print("Login error:", e)
return False
def sync_time():
"""Fungsi untuk mengambil waktu global (NTP) dan mengubahnya ke WIB (UTC+7)."""
print("Mencoba sinkronisasi NTP...")
for i in range(MAX_RETRY_NTP):
try:
ntptime.settime() # Ambil waktu UTC
# Konversi waktu dari detik UTC ditambah 7 jam (7 * 3600 detik)
t = time.localtime(time.time() + 7*3600)
print(f"Waktu NTP tersinkron: {t[0]:04d}-{t[1]:02d}-{t[2]:02d} {t[3]:02d}:{t[4]:02d}")
return True
except:
print(f"Gagal NTP ({i+1})... mencoba lagi.")
time.sleep(2)
return False
# ==================================================
# 5. PEMBACAAN SENSOR PENGKONDISI SINYAL
# ==================================================
def baca_tegangan(pin, samples=15):
"""Filter Median (Nilai Tengah) khusus untuk sensor pH."""
data = []
for _ in range(samples):
data.append(pin.read())
time.sleep_ms(10)
data.sort()
return data[len(data)//2] * (3.3 / 4095) # Mengubah nilai mentah ADC ke Voltase
def baca_tegangan_stabil(pin, samples=50):
"""Filter Trimmed Mean khusus untuk sensor TDS yang fluktuatif."""
data = []
for _ in range(samples):
data.append(pin.read())
time.sleep_ms(5)
data.sort()
# Membuang 10% data nilai ekstrem atas dan bawah (Noise Reduction)
potong = samples // 10
data_bersih = data[potong : samples - potong]
rata_rata = sum(data_bersih) / len(data_bersih)
return rata_rata * (3.3 / 4095)
def get_sensor_data():
"""Fungsi utama untuk membaca pH dan TDS, lalu memasukkannya ke rumus regresi."""
global ph_buffer
# === Membaca sensor pH ===
v_ph = baca_tegangan(ph_pin)
ph_raw = (PH_SLOPE * v_ph) + PH_INTERCEPT # Rumus kalibrasi
# Memasukkan nilai pH ke dalam antrean (buffer) rata-rata keliling (Moving Average)
ph_buffer.append(ph_raw)
if len(ph_buffer) > BUFFER_SIZE:
ph_buffer.pop(0)
ph_val = sum(ph_buffer) / len(ph_buffer)
if ph_val < 0 or ph_val > 14: ph_val = ph_raw # Proteksi nilai tidak masuk akal
# === Membaca sensor TDS ===
v_tds = baca_tegangan_stabil(tds_pin)
tds_val = (TDS_SLOPE * v_tds) + TDS_INTERCEPT # Rumus kalibrasi
if tds_val < 0: tds_val = 0 # TDS tidak boleh minus
return round(ph_val, 2), int(tds_val)
# ==================================================
# 6. KOMUNIKASI FIRESTORE
# ==================================================
def fetch_growth_phase():
"""Mengambil status fase pertumbuhan (Semai/Vegetatif/Generatif) dari Firestore."""
url = f"https://firestore.googleapis.com/v1/projects/{PROJECT_ID}/databases/(default)/documents/planting_cycles/{DOC_ID_FASE}"
headers = {"Authorization": f"Bearer {auth_token}"}
try:
res = urequests.get(url, headers=headers)
if res.status_code == 200:
data = res.json()
phase_str = data["fields"]["growth_phase"]["stringValue"]
res.close()
# Menerjemahkan string Firestore ke kode Logika (F1/F2/F3)
if phase_str == "Semai": return "F1"
elif phase_str == "Vegetatif": return "F2"
elif phase_str == "Generatif": return "F3"
else:
print(f"-> Gagal ambil fase dari DB (Code: {res.status_code})")
print("-> Alasan Firebase:", res.text)
res.close()
except Exception as e:
print("-> Error koneksi fetch fase:", e)
return "F1" # Jika internet putus, anggap berada di fase teraman (F1)
def upload_to_firestore(ph, tds, st):
"""Mengirim hasil bacaan sensor dan status pompa ke Firestore."""
# Validasi Tahun: Jika jam internal ESP32 menunjukkan tahun 2000 (belum sync), paksa sync
if time.localtime()[0] < 2024:
sync_time()
# Membentuk string timestamp (Format RFC 3339) dengan zona WIB (+07:00)
waktu_wib_sec = time.time() + (7 * 3600)
t = time.localtime(waktu_wib_sec)
timestamp_str = f"{t[0]:04d}-{t[1]:02d}-{t[2]:02d}T{t[3]:02d}:{t[4]:02d}:{t[5]:02d}+07:00"
# URL update API Firestore beserta updateMask agar divalidasi oleh Security Rules
url = f"https://firestore.googleapis.com/v1/projects/{PROJECT_ID}/databases/(default)/documents/hidroponik_data/{DOC_ID_DATA}?updateMask.fieldPaths=pH_value&updateMask.fieldPaths=nutrient_level&updateMask.fieldPaths=pump_nutrient_status&updateMask.fieldPaths=pump_water_status&updateMask.fieldPaths=pump_ph_up_status&updateMask.fieldPaths=pump_ph_down_status&updateMask.fieldPaths=timestamp&updateMask.fieldPaths=user_id"
headers = {"Authorization": f"Bearer {auth_token}"}
# Payload format JSON (Tipe data harus persis dengan Firestore Rules)
payload = {
"fields": {
"pH_value": {"doubleValue": float(ph)},
"nutrient_level": {"integerValue": int(tds)},
"pump_nutrient_status": {"booleanValue": st["nut"] == RELAY_ON},
"pump_water_status": {"booleanValue": st["air"] == RELAY_ON},
"pump_ph_up_status": {"booleanValue": st["pup"] == RELAY_ON},
"pump_ph_down_status": {"booleanValue": st["pdown"] == RELAY_ON},
"timestamp": {"timestampValue": timestamp_str},
"user_id": {"stringValue": my_user_id}
}
}
try:
print("Mengunggah ke Firestore...")
res = urequests.patch(url, json=payload, headers=headers)
if res.status_code == 200:
print("-> Upload Berhasil!")
else:
print(f"-> Upload Gagal (Code: {res.status_code})")
print("-> Detail Error:", res.text)
res.close()
except Exception as e:
print("-> Upload Error:", e)
# ==================================================
# 7. LOGIKA KONTROL SISTEM PAKAR (FORWARD CHAINING)
# ==================================================
def trigger_pump(pin, nama):
"""Menyalakan relay spesifik sesuai PUMP_DURATION lalu mematikannya kembali."""
print(f" ---> Mengaktifkan Pompa: {nama} ({PUMP_DURATION} Detik)")
pin.value(RELAY_ON)
time.sleep(PUMP_DURATION)
pin.value(RELAY_OFF)
print(f" ---> {nama} Selesai")
def eksekusi_pompa(st):
"""Fungsi jembatan untuk mengecek perintah logika mana yang bernilai ON."""
if st["pup"] == RELAY_ON: trigger_pump(pumps["ph_up"], "PH UP")
if st["pdown"] == RELAY_ON: trigger_pump(pumps["ph_down"], "PH DOWN")
if st["air"] == RELAY_ON: trigger_pump(pumps["air_baku"], "AIR BAKU")
if st["nut"] == RELAY_ON:
trigger_pump(pumps["nutrisi"][0], "NUTRISI A")
trigger_pump(pumps["nutrisi"][1], "NUTRISI B")
def run_forward_chaining(f_code, ph, tds):
"""
Mesin Inferensi (Forward Chaining).
Mengubah fakta pembacaan sensor menjadi aturan (Rule) keputusan status pompa.
Terdiri dari 24 Rule berdasarkan kombinasi (Fase, Status pH, Status TDS).
"""
# Fakta 1: Klasifikasi tingkat pH (P)
p_code = "P1" if ph < 4.0 else "P2" if ph > 6.0 else "P3"
# Fakta 2: Klasifikasi kebutuhan nutrisi TDS (T) bergantung pada Fase Tanam (F)
t_code = "T2"
if f_code == "F1":
if tds < 300: t_code = "T1"
elif tds > 500: t_code = "T3"
elif f_code == "F2":
if tds < 600: t_code = "T1"
elif tds > 800: t_code = "T3"
elif f_code == "F3":
if tds < 800: t_code = "T1"
elif tds > 1200: t_code = "T3"
# Default semua target keputusan adalah MATI
st = {"nut": RELAY_OFF, "air": RELAY_OFF, "pup": RELAY_OFF, "pdown": RELAY_OFF}
# Pemetaan 24 Rule
if f_code == "F1":
if p_code == "P1":
if t_code == "T1": st["pup"] = RELAY_ON; st["nut"] = RELAY_ON
elif t_code == "T2": st["pup"] = RELAY_ON
elif t_code == "T3": st["pup"] = RELAY_ON; st["air"] = RELAY_ON
elif p_code == "P2":
if t_code == "T1": st["pdown"] = RELAY_ON; st["nut"] = RELAY_ON
elif t_code == "T2": st["pdown"] = RELAY_ON
elif t_code == "T3": st["pdown"] = RELAY_ON; st["air"] = RELAY_ON
elif p_code == "P3":
if t_code == "T1": st["nut"] = RELAY_ON
elif t_code == "T3": st["air"] = RELAY_ON
elif f_code == "F2":
if p_code == "P1":
if t_code == "T1": st["pup"] = RELAY_ON; st["nut"] = RELAY_ON
elif t_code == "T2": st["pup"] = RELAY_ON
elif t_code == "T3": st["pup"] = RELAY_ON; st["air"] = RELAY_ON
elif p_code == "P2":
if t_code == "T1": st["pdown"] = RELAY_ON; st["nut"] = RELAY_ON
elif t_code == "T2": st["pdown"] = RELAY_ON
elif t_code == "T3": st["pdown"] = RELAY_ON; st["air"] = RELAY_ON
elif p_code == "P3":
if t_code == "T1": st["nut"] = RELAY_ON
elif t_code == "T3": st["air"] = RELAY_ON
elif f_code == "F3":
if p_code == "P1":
if t_code == "T1": st["pup"] = RELAY_ON; st["nut"] = RELAY_ON
elif t_code == "T2": st["pup"] = RELAY_ON
elif t_code == "T3": st["pup"] = RELAY_ON; st["air"] = RELAY_ON
elif p_code == "P2":
if t_code == "T1": st["pdown"] = RELAY_ON; st["nut"] = RELAY_ON
elif t_code == "T2": st["pdown"] = RELAY_ON
elif t_code == "T3": st["pdown"] = RELAY_ON; st["air"] = RELAY_ON
elif p_code == "P3":
if t_code == "T1": st["nut"] = RELAY_ON
elif t_code == "T3": st["air"] = RELAY_ON
# Mengembalikan nilai status pompa yang dihasilkan mesin inferensi (belum dijalankan fisiknya)
return st
# ==================================================
# 8. SIKLUS UTAMA (MAIN LOOP)
# ==================================================
print("=== MULAI KONEKSI ===")
if connect_wifi():
sync_time()
if login_firebase():
print("\nSistem Otonom HydroNutrify Aktif & Memasuki Main Loop.")
try:
while True:
print("\n" + "="*40)
# Langkah 1: Kumpulkan Data Lingkungan
f_code = fetch_growth_phase()
ph_val, tds_val = get_sensor_data()
print(f"Fase : {f_code} | pH : {ph_val} | TDS : {tds_val} PPM")
# Langkah 2: Proses Data menggunakan Algoritma Forward Chaining
status_pompa = run_forward_chaining(f_code, ph_val, tds_val)
# Langkah 3: Identifikasi apakah perlu tindakan aktuator
pompa_aktif = (status_pompa["nut"] == RELAY_ON or
status_pompa["air"] == RELAY_ON or
status_pompa["pup"] == RELAY_ON or
status_pompa["pdown"] == RELAY_ON)
# Langkah 4: Metode Double Update untuk User Interface (UI) Real-Time
if pompa_aktif:
# Update ke-1: Beri tahu Firebase (dan Dashboard User) bahwa pompa menyala
print("-> Pompa AKTIF! Mengirim status ON ke Firebase...")
upload_to_firestore(ph_val, tds_val, status_pompa)
# Tindakan Nyata: Nyalakan relay secara bergiliran
eksekusi_pompa(status_pompa)
# Update ke-2: Setelah 10 detik, lapor kembali bahwa semua pompa sudah mati
print("-> Pompa Selesai. Memperbarui status Firebase menjadi OFF...")
status_mati_semua = {"nut": RELAY_OFF, "air": RELAY_OFF, "pup": RELAY_OFF, "pdown": RELAY_OFF}
upload_to_firestore(ph_val, tds_val, status_mati_semua)
else:
# Jika sensor ideal, cukup laporkan nilai sensornya saja
print("-> Kondisi Ideal (Semua Pompa OFF). Mengirim ke Firebase...")
upload_to_firestore(ph_val, tds_val, status_pompa)
# Langkah 5: Masuk ke mode tunggu (1 Jam)
print(f"Menunggu {LOOP_INTERVAL} detik untuk siklus berikutnya...")
time.sleep(LOOP_INTERVAL)
# Penanganan jika program dihentikan secara manual di Thonny (Ctrl+C)
except KeyboardInterrupt:
print("\nPROGRAM DIHENTIKAN MANUAL")
for pin in [pin_nutrisi_a, pin_nutrisi_b, pin_air_baku, pin_ph_up, pin_ph_down]:
pin.value(RELAY_OFF)
print("Semua pompa dimatikan demi keamanan.")
# Penanganan jika terjadi error crash pada koneksi / memory ESP32
except Exception as e:
print("\nLoop Error Kritis:", e)
time.sleep(5)
else:
print("Sistem berhenti: Gagal Login Firebase.")
else:
print("Sistem berhenti: Tidak ada koneksi WiFi.")

16
README.md Normal file
View File

@ -0,0 +1,16 @@
# mobile_monitoring
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,56 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
// Google services Gradle plugin
id("com.google.gms.google-services")
}
android {
namespace = "com.example.mobile_monitoring"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.mobile_monitoring"
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
dependencies {
// Import the Firebase BoM
implementation(platform("com.google.firebase:firebase-bom:34.15.0"))
// Add the dependencies for Firebase products you want to use
// When using the BoM, don't specify versions in Firebase dependencies
implementation("com.google.firebase:firebase-analytics")
// Add the dependencies for any other desired Firebase products
// https://firebase.google.com/docs/android/setup#available-libraries
}
flutter {
source = "../.."
}

View File

@ -0,0 +1,29 @@
{
"project_info": {
"project_number": "712867239915",
"project_id": "hydronutrify",
"storage_bucket": "hydronutrify.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:712867239915:android:7a7db4cfb9d15a364e6bae",
"android_client_info": {
"package_name": "com.example.mobile_monitoring"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyAI0VsDbdr7pAxVpofSh3IP6sgYRmEkrx0"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

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

View File

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

View File

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

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

@ -0,0 +1,28 @@
plugins {
// Google services Gradle plugin (required for Firebase)
id("com.google.gms.google-services") apply false
}
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View File

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

View File

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

View File

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

BIN
assets/images/app_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

29
firebase.json Normal file
View File

@ -0,0 +1,29 @@
{
"flutter": {
"platforms": {
"android": {
"default": {
"projectId": "iot-hidroponik-1f15d",
"appId": "1:818899803821:android:3170c751b511c51cd53024",
"fileOutput": "android/app/google-services.json"
}
},
"dart": {
"lib/firebase_options.dart": {
"projectId": "iot-hidroponik-1f15d",
"configurations": {
"android": "1:818899803821:android:3170c751b511c51cd53024",
"ios": "1:818899803821:ios:f62f4dc11073557fd53024",
"macos": "1:818899803821:ios:f62f4dc11073557fd53024",
"web": "1:818899803821:web:12f08afaecaefcb9d53024",
"windows": "1:818899803821:web:b980c246619717ccd53024"
}
}
}
}
},
"firestore": {
"rules": "firestore.rules",
"indexes": "firestore.indexes.json"
}
}

21
firestore.indexes.json Normal file
View File

@ -0,0 +1,21 @@
{
"indexes": [
{
"collectionGroup": "hidroponik_data",
"queryScope": "COLLECTION",
"fields": [
{ "fieldPath": "user_id", "order": "ASCENDING" },
{ "fieldPath": "timestamp", "order": "DESCENDING" }
]
},
{
"collectionGroup": "hidroponik_data",
"queryScope": "COLLECTION",
"fields": [
{ "fieldPath": "user_id", "order": "ASCENDING" },
{ "fieldPath": "timestamp", "order": "ASCENDING" }
]
}
],
"fieldOverrides": []
}

25
firestore.rules Normal file
View File

@ -0,0 +1,25 @@
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// 1. Collection 'users' - Akses untuk data profil pengguna
match /users/{userId} {
allow read, write: if request.auth != null;
}
// 2. Collection 'planting_cycles' - Akses untuk fase & siklus tanam
match /planting_cycles/{cycleId} {
allow read, write: if request.auth != null;
}
// 3. Collection 'hidroponik_data' - Akses untuk data sensor pH & TDS (ESP32 / Monitoring)
match /hidroponik_data/{document=**} {
allow read, write: if request.auth != null;
}
// 4. Collection 'sensor_calibration' - Akses untuk data kalibrasi sensor
match /sensor_calibration/{document=**} {
allow read, write: if request.auth != null;
}
}
}

34
ios/.gitignore vendored Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
{"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":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"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":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@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: 374 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 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>Mobile Monitoring</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>mobile_monitoring</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.
}
}

View File

@ -0,0 +1,51 @@
/// Konfigurasi umum aplikasi HydroNutrify
class AppSettings {
AppSettings._(); // Private constructor untuk mencegah instansiasi
// App Configuration
static const String fontFamily = 'Poppins';
static const bool debugMode = false;
static const bool enableAnalytics = true;
static const bool enableCrashReporting = true;
// API Configuration
static const String apiBaseUrl = 'https://api.hydronutrify.com';
static const Duration apiTimeout = Duration(seconds: 30);
static const int maxRetries = 3;
// Firebase Configuration
static const bool enableFirebaseLogging = true;
// UI Configuration
static const double defaultPadding = 16.0;
static const double defaultBorderRadius = 8.0;
static const double smallBorderRadius = 4.0;
static const double largeBorderRadius = 16.0;
// Splash Screen
static const Duration splashDuration = Duration(seconds: 3);
// Cache Duration
static const Duration cacheDuration = Duration(hours: 1);
static const Duration longCacheDuration = Duration(days: 7);
// Page Size
static const int defaultPageSize = 20;
static const int maxPageSize = 100;
// Refresh Intervals
static const Duration dataRefreshInterval = Duration(seconds: 30);
static const Duration uiRefreshInterval = Duration(milliseconds: 500);
// Validation
static const int minPasswordLength = 6;
static const int maxPasswordLength = 128;
static const int minNameLength = 3;
static const int maxNameLength = 50;
static const int maxPhoneLength = 15;
// Feature Flags
static const bool enableOfflineMode = true;
static const bool enableLocalNotifications = true;
static const bool enablePushNotifications = false;
}

View File

@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
/// Konfigurasi warna aplikasi HydroNutrify
class AppColors {
AppColors._(); // Private constructor untuk mencegah instansiasi
// Primary Colors
static const Color primary = Colors.green;
static const Color primaryLight = Color(0xFF66BB6A);
static const Color primaryDark = Color(0xFF2E7D32);
// Secondary Colors
static const Color secondary = Color(0xFF4CAF50);
static const Color secondaryLight = Color(0xFF81C784);
static const Color secondaryDark = Color(0xFF388E3C);
// Neutral Colors
static const Color white = Color(0xFFFFFFFF);
static const Color black = Color(0xFF000000);
static const Color scaffoldBackground = Color(0xFFF9F9F9);
static const Color pageBackground = Color(0xFFFAFAFA);
static const Color lightGray = Color(0xFFF5F5F5);
static const Color mediumGray = Color(0xFFBDBDBD);
static const Color darkGray = Color(0xFF757575);
// Status Colors
static const Color success = Color(0xFF4CAF50);
static const Color warning = Color(0xFFFFC107);
static const Color error = Color(0xFFF44336);
static const Color info = Color(0xFF2196F3);
static const Color navbarActive = Color(0xFF52C41A);
// Text Colors
static const Color textPrimary = Color(0xFF212121);
static const Color textSecondary = Color(0xFF757575);
static const Color textHint = Color(0xFFBDBDBD);
static const Color textInverse = Color(0xFFFFFFFF);
// Border Colors
static const Color borderLight = Color(0xFFE0E0E0);
static const Color borderDark = Color(0xFF9E9E9E);
// Shadow Color
static const Color shadow = Color(0x1A000000);
// Transparency
static Color blackWithOpacity(double opacity) => black.withValues(alpha: opacity);
static Color whiteWithOpacity(double opacity) => white.withValues(alpha: opacity);
}

View File

@ -0,0 +1,4 @@
// Export semua file constants di sini untuk kemudahan import
export 'colors.dart';
export 'strings.dart';
export 'app_settings.dart';

View File

@ -0,0 +1,94 @@
/// Konfigurasi string statis aplikasi HydroNutrify
class AppStrings {
AppStrings._(); // Private constructor untuk mencegah instansiasi
// App Metadata
static const String appName = 'HydroNutrify';
static const String appTagline = 'Hidroponik Jadi Mudah.';
static const String appVersion = '1.0.0';
// Navigation & Menu
static const String home = 'Beranda';
static const String settings = 'Pengaturan';
static const String profile = 'Profil';
static const String flowManagement = 'Manajemen Aliran';
static const String monitoring = 'Monitoring';
static const String graph = 'Grafik';
static const String monitoringGrafik = 'Monitoring Grafik & Statistik';
static const String seeAll = 'Lihat Semua';
// Authentication
static const String login = 'Masuk';
static const String register = 'Daftar';
static const String logout = 'Keluar';
static const String forgotPassword = 'Lupa Kata Sandi';
static const String email = 'Email';
static const String password = 'Kata Sandi';
static const String confirmPassword = 'Konfirmasi Kata Sandi';
static const String signIn = 'Masuk';
static const String signUp = 'Daftar';
static const String noAccount = 'Belum punya akun?';
static const String alreadyHaveAccount = 'Sudah punya akun?';
static const String changePassword = 'Ubah Kata Sandi';
static const String currentPassword = 'Kata Sandi Saat Ini';
static const String newPassword = 'Kata Sandi Baru';
// Profile
static const String myProfile = 'Profil Saya';
static const String fullName = 'Nama Lengkap';
static const String phone = 'Nomor Telepon';
static const String address = 'Alamat';
static const String editProfile = 'Edit Profil';
// Monitoring Data
static const String ph = 'pH';
static const String tds = 'TDS';
static const String temperature = 'Suhu';
static const String humidity = 'Kelembaban';
static const String ec = 'EC';
static const String waterLevel = 'Tingkat Air';
static const String growthPhase = 'Fase Pertumbuhan';
// Actions
static const String save = 'Simpan';
static const String cancel = 'Batal';
static const String edit = 'Edit';
static const String delete = 'Hapus';
static const String add = 'Tambah';
static const String next = 'Selanjutnya';
static const String retry = 'Coba Lagi';
static const String previous = 'Sebelumnya';
static const String confirm = 'Konfirmasi';
static const String close = 'Tutup';
// Messages
static const String loadingData = 'Memuat data...';
static const String noDataAvailable = 'Data tidak tersedia';
static const String noMonitoringData = 'Belum ada data monitoring';
static const String pleaseLogin = 'Silakan Login';
static const String failedToLoadData = 'Gagal memuat data';
static const String startPlantingFirst = 'Belum Memulai Tanam';
static const String successfullyUpdated = 'Berhasil diperbarui';
static const String failedToLoad = 'Gagal memuat data';
static const String pleaseWait = 'Mohon tunggu...';
static const String error = 'Error';
static const String success = 'Berhasil';
static const String warning = 'Peringatan';
// Errors
static const String invalidEmail = 'Email tidak valid';
static const String passwordTooShort = 'Kata sandi minimal 6 karakter';
static const String passwordMismatch = 'Kata sandi tidak cocok';
static const String fillAllFields = 'Harap isi semua kolom';
static const String networkError = 'Kesalahan jaringan. Periksa koneksi Anda';
static const String serverError = 'Kesalahan server. Coba lagi nanti';
static const String unknownError = 'Terjadi kesalahan tidak diketahui';
// Statistics
static const String weeklyStats = 'Statistik Mingguan';
static const String monthlyStats = 'Statistik Bulanan';
static const String average = 'Rata-rata';
static const String minimum = 'Minimum';
static const String maximum = 'Maksimum';
static const String trend = 'Tren';
}

6
lib/core/core.dart Normal file
View File

@ -0,0 +1,6 @@
// Export semua file core di sini untuk kemudahan import
export 'constants/constants.dart';
export 'theme/theme.dart';
export 'utils/cycle_phase_utils.dart';
export 'utils/phase_display_utils.dart';
export 'firebase_options.dart';

View File

@ -0,0 +1,89 @@
// 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;
import 'package:flutter_dotenv/flutter_dotenv.dart';
/// 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:
return ios;
case TargetPlatform.macOS:
return macos;
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 FirebaseOptions get web => FirebaseOptions(
apiKey: dotenv.env['FIREBASE_API_KEY_WEB'] ?? '',
appId: dotenv.env['FIREBASE_APP_ID_WEB'] ?? '',
messagingSenderId: dotenv.env['FIREBASE_MESSAGING_SENDER_ID'] ?? '',
projectId: dotenv.env['FIREBASE_PROJECT_ID'] ?? '',
authDomain: dotenv.env['FIREBASE_AUTH_DOMAIN'] ?? '',
storageBucket: dotenv.env['FIREBASE_STORAGE_BUCKET'] ?? '',
measurementId: dotenv.env['FIREBASE_MEASUREMENT_ID_WEB'] ?? '',
);
static FirebaseOptions get android => FirebaseOptions(
apiKey: dotenv.env['FIREBASE_API_KEY_ANDROID'] ?? '',
appId: dotenv.env['FIREBASE_APP_ID_ANDROID'] ?? '',
messagingSenderId: dotenv.env['FIREBASE_MESSAGING_SENDER_ID'] ?? '',
projectId: dotenv.env['FIREBASE_PROJECT_ID'] ?? '',
storageBucket: dotenv.env['FIREBASE_STORAGE_BUCKET'] ?? '',
);
static FirebaseOptions get ios => FirebaseOptions(
apiKey: dotenv.env['FIREBASE_API_KEY_IOS'] ?? '',
appId: dotenv.env['FIREBASE_APP_ID_IOS'] ?? '',
messagingSenderId: dotenv.env['FIREBASE_MESSAGING_SENDER_ID'] ?? '',
projectId: dotenv.env['FIREBASE_PROJECT_ID'] ?? '',
storageBucket: dotenv.env['FIREBASE_STORAGE_BUCKET'] ?? '',
iosBundleId: dotenv.env['FIREBASE_IOS_BUNDLE_ID'] ?? '',
);
static FirebaseOptions get macos => FirebaseOptions(
apiKey: dotenv.env['FIREBASE_API_KEY_IOS'] ?? '',
appId: dotenv.env['FIREBASE_APP_ID_IOS'] ?? '',
messagingSenderId: dotenv.env['FIREBASE_MESSAGING_SENDER_ID'] ?? '',
projectId: dotenv.env['FIREBASE_PROJECT_ID'] ?? '',
storageBucket: dotenv.env['FIREBASE_STORAGE_BUCKET'] ?? '',
iosBundleId: dotenv.env['FIREBASE_IOS_BUNDLE_ID'] ?? '',
);
static FirebaseOptions get windows => FirebaseOptions(
apiKey: dotenv.env['FIREBASE_API_KEY_WINDOWS'] ?? '',
appId: dotenv.env['FIREBASE_APP_ID_WINDOWS'] ?? '',
messagingSenderId: dotenv.env['FIREBASE_MESSAGING_SENDER_ID'] ?? '',
projectId: dotenv.env['FIREBASE_PROJECT_ID'] ?? '',
authDomain: dotenv.env['FIREBASE_AUTH_DOMAIN'] ?? '',
storageBucket: dotenv.env['FIREBASE_STORAGE_BUCKET'] ?? '',
measurementId: dotenv.env['FIREBASE_MEASUREMENT_ID_WINDOWS'] ?? '',
);
}

View File

@ -0,0 +1,295 @@
import 'package:flutter/material.dart';
import 'package:mobile_monitoring/core/constants/colors.dart';
import 'package:mobile_monitoring/core/constants/app_settings.dart';
/// Konfigurasi Theme untuk aplikasi HydroNutrify
class AppTheme {
AppTheme._(); // Private constructor untuk mencegah instansiasi
/// Light Theme
static ThemeData get lightTheme {
return ThemeData(
useMaterial3: true,
brightness: Brightness.light,
fontFamily: AppSettings.fontFamily,
// Color Scheme
colorScheme: ColorScheme.fromSeed(
seedColor: AppColors.primary,
brightness: Brightness.light,
),
// Scaffold Background
scaffoldBackgroundColor: AppColors.scaffoldBackground,
// AppBar Theme
appBarTheme: AppBarTheme(
backgroundColor: AppColors.primary,
foregroundColor: AppColors.white,
elevation: 0,
centerTitle: true,
titleTextStyle: TextStyle(
color: AppColors.white,
fontSize: 20,
fontWeight: FontWeight.w600,
fontFamily: AppSettings.fontFamily,
),
),
// Text Theme
textTheme: TextTheme(
// Large text styles
displayLarge: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: AppColors.textPrimary,
fontFamily: AppSettings.fontFamily,
),
displayMedium: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: AppColors.textPrimary,
fontFamily: AppSettings.fontFamily,
),
displaySmall: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
fontFamily: AppSettings.fontFamily,
),
// Heading styles
headlineLarge: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
fontFamily: AppSettings.fontFamily,
),
headlineMedium: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
fontFamily: AppSettings.fontFamily,
),
headlineSmall: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
fontFamily: AppSettings.fontFamily,
),
// Title styles
titleLarge: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
fontFamily: AppSettings.fontFamily,
),
titleMedium: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.textPrimary,
fontFamily: AppSettings.fontFamily,
),
titleSmall: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.textPrimary,
fontFamily: AppSettings.fontFamily,
),
// Body styles
bodyLarge: TextStyle(
fontSize: 16,
fontWeight: FontWeight.normal,
color: AppColors.textPrimary,
fontFamily: AppSettings.fontFamily,
),
bodyMedium: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: AppColors.textPrimary,
fontFamily: AppSettings.fontFamily,
),
bodySmall: TextStyle(
fontSize: 12,
fontWeight: FontWeight.normal,
color: AppColors.textSecondary,
fontFamily: AppSettings.fontFamily,
),
// Label styles
labelLarge: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.textPrimary,
fontFamily: AppSettings.fontFamily,
),
labelMedium: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.textPrimary,
fontFamily: AppSettings.fontFamily,
),
labelSmall: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary,
fontFamily: AppSettings.fontFamily,
),
),
// Button Styles
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: AppColors.white,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppSettings.defaultBorderRadius),
),
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.primary,
side: const BorderSide(color: AppColors.primary, width: 1),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppSettings.defaultBorderRadius),
),
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: AppColors.primary,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
textStyle: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
// Input Decoration Theme
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: AppColors.white,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppSettings.defaultBorderRadius),
borderSide: const BorderSide(color: AppColors.borderLight),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppSettings.defaultBorderRadius),
borderSide: const BorderSide(color: AppColors.borderLight),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppSettings.defaultBorderRadius),
borderSide: const BorderSide(color: AppColors.primary, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppSettings.defaultBorderRadius),
borderSide: const BorderSide(color: AppColors.error),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppSettings.defaultBorderRadius),
borderSide: const BorderSide(color: AppColors.error, width: 2),
),
labelStyle: const TextStyle(color: AppColors.textSecondary),
hintStyle: const TextStyle(color: AppColors.textHint),
errorStyle: const TextStyle(color: AppColors.error),
),
// Card Theme
cardTheme: CardThemeData(
color: AppColors.white,
elevation: 2,
margin: const EdgeInsets.all(8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppSettings.defaultBorderRadius),
),
),
// Dialog Theme
dialogTheme: DialogThemeData(
backgroundColor: AppColors.white,
elevation: 8,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppSettings.defaultBorderRadius),
),
),
// Floating Action Button Theme
floatingActionButtonTheme: FloatingActionButtonThemeData(
backgroundColor: AppColors.primary,
foregroundColor: AppColors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
// Progress Indicator
progressIndicatorTheme: const ProgressIndicatorThemeData(
color: AppColors.primary,
),
// Divider Theme
dividerTheme: const DividerThemeData(
color: AppColors.borderLight,
thickness: 1,
space: 16,
),
// Chip Theme
chipTheme: ChipThemeData(
backgroundColor: AppColors.lightGray,
disabledColor: AppColors.mediumGray,
selectedColor: AppColors.primary,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
labelStyle: const TextStyle(
color: AppColors.textPrimary,
fontSize: 14,
fontWeight: FontWeight.w500,
),
secondaryLabelStyle: const TextStyle(
color: AppColors.white,
fontSize: 14,
fontWeight: FontWeight.w500,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
// Bottom Navigation Bar Theme
bottomNavigationBarTheme: BottomNavigationBarThemeData(
backgroundColor: AppColors.white,
selectedItemColor: AppColors.primary,
unselectedItemColor: AppColors.mediumGray,
elevation: 8,
type: BottomNavigationBarType.fixed,
),
);
}
/// Dark Theme (Optional untuk masa depan)
static ThemeData get darkTheme {
return ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
fontFamily: AppSettings.fontFamily,
colorScheme: ColorScheme.fromSeed(
seedColor: AppColors.primaryLight,
brightness: Brightness.dark,
),
scaffoldBackgroundColor: Colors.grey[900],
);
}
}

View File

@ -0,0 +1,2 @@
// Export theme configuration
export 'app_theme.dart';

View File

@ -0,0 +1,58 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:mobile_monitoring/core/utils/phase_display_utils.dart';
class CyclePhaseUtils {
static const List<String> monthNames = [
'Jan',
'Feb',
'Mar',
'Apr',
'Mei',
'Jun',
'Jul',
'Agu',
'Sep',
'Okt',
'Nov',
'Des',
];
static DateTime? extractStartDate(Map<String, dynamic>? cycleData) {
final startValue = cycleData?['start_date'];
if (startValue is Timestamp) {
return startValue.toDate();
}
if (startValue is DateTime) {
return startValue;
}
return null;
}
static String calculateCyclePhase(int daysSincePlanting) {
if (daysSincePlanting <= 14) return PhaseDisplayUtils.semai.name;
if (daysSincePlanting <= 30) return PhaseDisplayUtils.vegetatif.name;
if (daysSincePlanting <= 60) return PhaseDisplayUtils.generatif.name;
return 'Panen';
}
static String calculateDisplayPhase(int daysSincePlanting) {
return PhaseDisplayUtils.fromDays(daysSincePlanting).name;
}
static int inferDaysFromPhase(String? phase) {
switch ((phase ?? '').toLowerCase()) {
case 'panen':
return 61;
default:
final phaseConfig = PhaseDisplayUtils.fromName(phase);
if (phaseConfig == PhaseDisplayUtils.semai) return 7;
if (phaseConfig == PhaseDisplayUtils.vegetatif) return 20;
if (phaseConfig == PhaseDisplayUtils.generatif) return 40;
return 0;
}
}
static String formatIndonesianDate(DateTime date) {
return '${date.day} ${monthNames[date.month - 1]} ${date.year}';
}
}

View File

@ -0,0 +1,116 @@
import 'package:flutter/material.dart';
class PhaseDisplayConfig {
final String name;
final Color color;
final int nutrientMin;
final int nutrientMax;
final int startDay;
final int? endDay;
const PhaseDisplayConfig({
required this.name,
required this.color,
required this.nutrientMin,
required this.nutrientMax,
required this.startDay,
required this.endDay,
});
String get dayRangeLabel =>
endDay == null ? '$startDay+' : '$startDay-$endDay';
}
class PhaseProgressData {
final String phaseName;
final int dayInPhase;
final int phaseDuration;
final double progress;
const PhaseProgressData({
required this.phaseName,
required this.dayInPhase,
required this.phaseDuration,
required this.progress,
});
}
class PhaseDisplayUtils {
static const semai = PhaseDisplayConfig(
name: 'Semai',
color: Color(0xFF2196F3),
nutrientMin: 300,
nutrientMax: 500,
startDay: 1,
endDay: 14,
);
static const vegetatif = PhaseDisplayConfig(
name: 'Vegetatif',
color: Color(0xFF4CAF50),
nutrientMin: 400,
nutrientMax: 800,
startDay: 15,
endDay: 30,
);
static const generatif = PhaseDisplayConfig(
name: 'Generatif',
color: Color(0xFFFF9800),
nutrientMin: 800,
nutrientMax: 1300,
startDay: 31,
endDay: null,
);
static const List<PhaseDisplayConfig> ordered = [semai, vegetatif, generatif];
static PhaseDisplayConfig fromDays(int daysSincePlanting) {
if (daysSincePlanting <= 14) return semai;
if (daysSincePlanting <= 30) return vegetatif;
return generatif;
}
static PhaseDisplayConfig fromName(String? name) {
final lower = (name ?? '').toLowerCase();
switch (lower) {
case 'semai':
return semai;
case 'generatif':
return generatif;
default:
return vegetatif;
}
}
static PhaseProgressData progressData(int daysSincePlanting) {
final phase = fromDays(daysSincePlanting);
if (phase == semai) {
return PhaseProgressData(
phaseName: phase.name,
dayInPhase: daysSincePlanting,
phaseDuration: 14,
progress: daysSincePlanting / 14,
);
}
if (phase == vegetatif) {
final dayInPhase = daysSincePlanting - 14;
return PhaseProgressData(
phaseName: phase.name,
dayInPhase: dayInPhase,
phaseDuration: 16,
progress: dayInPhase / 16,
);
}
final dayInPhase = daysSincePlanting - 30;
return PhaseProgressData(
phaseName: phase.name,
dayInPhase: dayInPhase,
phaseDuration: 30,
progress: (dayInPhase / 30).clamp(0.0, 1.0),
);
}
}

4
lib/data/data.dart Normal file
View File

@ -0,0 +1,4 @@
// Export semua file dari data layer
export 'models/models.dart';
export 'repositories/repositories.dart';
export 'services/services.dart';

View File

@ -0,0 +1,2 @@
// Export semua file dari models layer
export 'service_result.dart';

View File

@ -0,0 +1,35 @@
class ServiceResult<T> {
final bool success;
final String message;
final T? data;
final String? errorCode;
const ServiceResult({
required this.success,
required this.message,
this.data,
this.errorCode,
});
factory ServiceResult.success({
required String message,
T? data,
}) {
return ServiceResult<T>(
success: true,
message: message,
data: data,
);
}
factory ServiceResult.failure({
required String message,
String? errorCode,
}) {
return ServiceResult<T>(
success: false,
message: message,
errorCode: errorCode,
);
}
}

View File

@ -0,0 +1,25 @@
// Base Repository Interface
//
// Gunakan interface ini sebagai template untuk membuat repository-nya.
// Repository pattern memisahkan logika pengambilan data dari service.
//
// Contoh:
// ```dart
// abstract class UserRepository {
// Future<User> getUserById(String userId);
// Future<List<User>> getAllUsers();
// Future<void> updateUser(User user);
// Future<void> deleteUser(String userId);
// }
// ```
abstract class BaseRepository {
// Template method untuk operasi yang dapat diulang
Future<T> execute<T>(Future<T> Function() operation) async {
try {
return await operation();
} catch (e) {
rethrow;
}
}
}

View File

@ -0,0 +1,2 @@
// Export semua repositories dari data layer
export 'base_repository.dart';

View File

@ -0,0 +1,217 @@
import 'dart:async';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/foundation.dart';
import 'package:mobile_monitoring/data/models/service_result.dart';
/// Service untuk autentikasi (login, register, logout, reset password)
/// Backend logic - tidak ada dependency ke UI
class AuthService {
final FirebaseAuth _auth = FirebaseAuth.instance;
User? get currentUser => _auth.currentUser;
Stream<User?> get authStateChanges => _auth.authStateChanges();
/// Login dengan email dan password
/// Returns ServiceResult dengan status success/failure dan pesan
Future<ServiceResult<User?>> signInWithEmailAndPassword({
required String email,
required String password,
}) async {
try {
debugPrint('[AuthService] Login attempt for: $email');
// Set language untuk error messages
_auth.setLanguageCode('id');
// Timeout untuk mencegah hang
final userCredential = await _auth
.signInWithEmailAndPassword(email: email, password: password)
.timeout(
const Duration(seconds: 20),
onTimeout: () {
throw TimeoutException('Login timeout');
},
);
debugPrint('[AuthService] ✓ Login success - User: ${userCredential.user?.email}');
return ServiceResult.success(
message: 'Login berhasil',
data: userCredential.user,
);
} on TimeoutException {
debugPrint('[AuthService] ✗ Login timeout');
return ServiceResult.failure(
message: 'Login timeout. Periksa koneksi atau Google Play Services.',
errorCode: 'timeout',
);
} on FirebaseAuthException catch (e) {
debugPrint('[AuthService] ✗ FirebaseAuthException: ${e.code} - ${e.message}');
final message = _mapFirebaseAuthError(e.code);
return ServiceResult.failure(
message: message,
errorCode: e.code,
);
} catch (e) {
debugPrint('[AuthService] ✗ Unexpected error: $e');
return ServiceResult.failure(
message: 'Terjadi kesalahan saat login',
errorCode: 'unknown',
);
}
}
/// Register akun baru dengan email, password, dan nama
Future<ServiceResult<User?>> registerWithEmailAndPassword({
required String email,
required String password,
required String name,
}) async {
try {
debugPrint('[AuthService] Register attempt for: $email');
final userCredential = await _auth.createUserWithEmailAndPassword(
email: email,
password: password,
);
// Update display name
await userCredential.user?.updateDisplayName(name);
await userCredential.user?.reload();
debugPrint('[AuthService] ✓ Register success - User: ${userCredential.user?.email}');
return ServiceResult.success(
message: 'Registrasi berhasil',
data: userCredential.user,
);
} on FirebaseAuthException catch (e) {
debugPrint('[AuthService] ✗ Register FirebaseAuthException: ${e.code} - ${e.message}');
String message;
switch (e.code) {
case 'weak-password':
message = 'Password terlalu lemah';
break;
case 'email-already-in-use':
message = 'Email sudah terdaftar';
break;
case 'invalid-email':
message = 'Email tidak valid';
break;
default:
message = 'Terjadi kesalahan: ${e.message}';
}
return ServiceResult.failure(
message: message,
errorCode: e.code,
);
} catch (e) {
debugPrint('[AuthService] ✗ Register unexpected error: $e');
return ServiceResult.failure(
message: 'Terjadi kesalahan saat registrasi',
errorCode: 'unknown',
);
}
}
/// Logout user dari aplikasi
Future<ServiceResult<void>> signOut() async {
try {
debugPrint('[AuthService] Logout attempt');
await _auth.signOut();
debugPrint('[AuthService] ✓ Logout success');
return ServiceResult.success(message: 'Logout berhasil');
} catch (e) {
debugPrint('[AuthService] ✗ Logout error: $e');
return ServiceResult.failure(
message: 'Gagal logout',
errorCode: 'unknown',
);
}
}
/// Kirim email reset password
Future<ServiceResult<void>> resetPassword(String email) async {
try {
debugPrint('[AuthService] Reset password for: $email');
await _auth.sendPasswordResetEmail(email: email);
debugPrint('[AuthService] ✓ Reset password email sent');
return ServiceResult.success(
message: 'Link reset password telah dikirim ke email Anda',
);
} on FirebaseAuthException catch (e) {
debugPrint('[AuthService] ✗ Reset password FirebaseAuthException: ${e.code}');
String message;
switch (e.code) {
case 'user-not-found':
message = 'Email tidak terdaftar';
break;
case 'invalid-email':
message = 'Email tidak valid';
break;
default:
message = 'Terjadi kesalahan: ${e.message}';
}
return ServiceResult.failure(
message: message,
errorCode: e.code,
);
} catch (e) {
debugPrint('[AuthService] ✗ Reset password unexpected error: $e');
return ServiceResult.failure(
message: 'Terjadi kesalahan',
errorCode: 'unknown',
);
}
}
/// Validasi email format
bool isValidEmail(String email) {
return email.isNotEmpty && email.contains('@');
}
/// Validasi password
bool isValidPassword(String password) {
return password.length >= 6;
}
/// Map Firebase Auth error codes ke pesan yang user-friendly
String _mapFirebaseAuthError(String code) {
switch (code) {
case 'invalid-credential':
case 'wrong-password':
return 'Email atau password salah';
case 'user-not-found':
return 'Akun belum terdaftar';
case 'user-disabled':
return 'Akun dinonaktifkan';
case 'too-many-requests':
return 'Terlalu banyak percobaan, coba lagi nanti';
case 'network-request-failed':
return 'Koneksi internet bermasalah';
case 'operation-not-allowed':
return 'Email/Password belum diaktifkan di Firebase';
case 'app-not-authorized':
return 'App belum terdaftar di Firebase (package name/sha-1)';
case 'invalid-api-key':
return 'API key Firebase tidak valid';
case 'invalid-email':
return 'Format email tidak valid';
default:
return 'Login gagal: $code';
}
}
}

View File

@ -0,0 +1,343 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' show Color;
import 'package:mobile_monitoring/data/models/service_result.dart';
import 'package:mobile_monitoring/core/utils/cycle_phase_utils.dart';
/// Model untuk monitoring data di Flow page
class FlowMonitoringData {
final double ph;
final double tds;
final String phStatus;
final String tdsStatus;
final Color phColor;
final Color tdsColor;
final bool pumpPhUp;
final bool pumpPhDown;
final bool pumpNutrient;
final bool pumpWater;
FlowMonitoringData({
required this.ph,
required this.tds,
required this.phStatus,
required this.tdsStatus,
required this.phColor,
required this.tdsColor,
required this.pumpPhUp,
required this.pumpPhDown,
required this.pumpNutrient,
required this.pumpWater,
});
}
/// Model untuk recap row
class RecapRow {
final String day;
final String morning;
final String afternoon;
final String night;
final DateTime date;
RecapRow({
required this.day,
required this.morning,
required this.afternoon,
required this.night,
required this.date,
});
Map<String, dynamic> toMap() {
return {
'day': day,
'morning': morning,
'afternoon': afternoon,
'night': night,
'date': date,
};
}
}
/// Model untuk aggregate data per slot waktu
class _SlotAggregate {
double? _phSum;
double? _tdsSum;
int _count = 0;
void add({required double ph, required double tds}) {
_phSum = (_phSum ?? 0) + ph;
_tdsSum = (_tdsSum ?? 0) + tds;
_count++;
}
bool get hasData => _count > 0;
double? get phAverage => _count > 0 ? _phSum! / _count : null;
double? get tdsAverage => _count > 0 ? _tdsSum! / _count : null;
}
/// Error info untuk Flow page
class FlowDataError {
final String displayMessage;
final String detailMessage;
final bool isAuthError;
FlowDataError({
required this.displayMessage,
required this.detailMessage,
required this.isAuthError,
});
factory FlowDataError.fromException(Object error) {
final errorMsg = error.toString();
debugPrint('[FlowService] Error: $errorMsg');
bool isNotAuthError = errorMsg.contains('terautentikasi');
if (isNotAuthError) {
return FlowDataError(
displayMessage: 'Silakan Login',
detailMessage: 'Sesi Anda telah berakhir, login kembali',
isAuthError: true,
);
} else {
return FlowDataError(
displayMessage: 'Gagal memuat data',
detailMessage: 'Pastikan koneksi internet stabil',
isAuthError: false,
);
}
}
}
/// Service untuk Flow Page - Backend Logic
/// Tidak ada dependency ke UI (no BuildContext, no Color from material)
class FlowService {
static const List<String> dayNames = [
'Sen',
'Sel',
'Rab',
'Kam',
'Jum',
'Sab',
'Min',
];
FlowMonitoringData _parseMonitoringData(Map<String, dynamic> data) {
debugPrint('[FlowService] parseMonitoringData()');
// Extract values
final double ph = (data['pH_value'] as num?)?.toDouble() ?? 0.0;
final double tds = (data['nutrient_level'] as num?)?.toDouble() ?? 0.0;
// Extract pump status
final bool pumpPhUp = data['pump_ph_up_status'] ?? false;
final bool pumpPhDown = data['pump_ph_down_status'] ?? false;
final bool pumpNutrient = data['pump_nutrient_status'] ?? false;
final bool pumpWater = data['pump_water_status'] ?? false;
// Determine abnormality
final bool phPumpOn = pumpPhUp || pumpPhDown;
final bool nutrientPumpOn = pumpNutrient || pumpWater;
// Determine status and colors
final String phStatus = phPumpOn ? 'Abnormal' : 'Normal';
final String tdsStatus = nutrientPumpOn ? 'Abnormal' : 'Normal';
// Use color codes instead of Color objects for backend
// Frontend will convert these to Color objects
final Color phColor = phPumpOn
? const Color(0xFFFF9800)
: const Color(0xFF4CAF50);
final Color tdsColor = nutrientPumpOn
? const Color(0xFFFF9800)
: const Color(0xFF4CAF50);
debugPrint('[FlowService] pH: $ph, status: $phStatus');
debugPrint('[FlowService] TDS: $tds, status: $tdsStatus');
return FlowMonitoringData(
ph: ph,
tds: tds,
phStatus: phStatus,
tdsStatus: tdsStatus,
phColor: phColor,
tdsColor: tdsColor,
pumpPhUp: pumpPhUp,
pumpPhDown: pumpPhDown,
pumpNutrient: pumpNutrient,
pumpWater: pumpWater,
);
}
ServiceResult<FlowMonitoringData> parseMonitoringDataResult(
Map<String, dynamic> data,
) {
try {
final parsed = _parseMonitoringData(data);
return ServiceResult.success(
message: 'Data flow monitoring berhasil diproses',
data: parsed,
);
} catch (e) {
return ServiceResult.failure(
message: 'Gagal memproses data flow monitoring',
errorCode: 'parse_error',
);
}
}
List<RecapRow> _generateRecapRows(
List<Map<String, dynamic>> docs,
DateTime? startDate,
) {
debugPrint('[FlowService] generateRecapRows()');
debugPrint('[FlowService] Total docs: ${docs.length}');
debugPrint('[FlowService] Start date: $startDate');
final now = DateTime.now();
final filterEndDate = DateTime(now.year, now.month, now.day, 23, 59, 59);
// Determine filter start date
DateTime filterStartDate;
if (startDate != null) {
final plantingStart = DateTime(
startDate.year,
startDate.month,
startDate.day,
);
final sevenDaysAgo = now.subtract(const Duration(days: 6));
filterStartDate = plantingStart.isBefore(sevenDaysAgo)
? sevenDaysAgo
: plantingStart;
} else {
filterStartDate = now.subtract(const Duration(days: 6));
}
final dayCount = filterEndDate.difference(filterStartDate).inDays + 1;
debugPrint(
'[FlowService] Filter start: $filterStartDate, end: $filterEndDate',
);
debugPrint('[FlowService] Day count: $dayCount');
// Create buckets for each day and time slot
final buckets = List.generate(
dayCount,
(_) => List.generate(3, (_) => _SlotAggregate()),
);
// Aggregate data into buckets
int processedCount = 0;
for (final data in docs) {
final timestamp = data['timestamp'];
DateTime? dt;
if (timestamp is Timestamp) {
dt = timestamp.toDate();
} else if (timestamp is DateTime) {
dt = timestamp;
}
if (dt == null ||
dt.isBefore(filterStartDate) ||
dt.isAfter(filterEndDate)) {
continue;
}
final ph = (data['pH_value'] as num?)?.toDouble() ?? 0.0;
final tds = (data['nutrient_level'] as num?)?.toDouble() ?? 0.0;
final dayIdx = dt.difference(filterStartDate).inDays;
if (dayIdx >= 0 && dayIdx < dayCount) {
final slotIdx = _getSlotIndexForHour(dt.hour);
buckets[dayIdx][slotIdx].add(ph: ph, tds: tds);
processedCount++;
}
}
debugPrint('[FlowService] Processed $processedCount data points');
// Generate rows
final rows = <RecapRow>[];
for (var i = 0; i < dayCount; i++) {
final date = filterStartDate.add(Duration(days: i));
final dayName =
'${dayNames[(date.weekday - 1) % 7]} ${date.day}/${date.month}';
rows.add(
RecapRow(
day: dayName,
morning: _formatSlotText(buckets[i][0]),
afternoon: _formatSlotText(buckets[i][1]),
night: _formatSlotText(buckets[i][2]),
date: date,
),
);
}
debugPrint('[FlowService] Generated ${rows.length} recap rows');
return rows;
}
ServiceResult<List<RecapRow>> generateRecapRowsResult(
List<Map<String, dynamic>> docs,
DateTime? startDate,
) {
try {
final rows = _generateRecapRows(docs, startDate);
return ServiceResult.success(
message: 'Rekap monitoring berhasil dibuat',
data: rows,
);
} catch (e) {
return ServiceResult.failure(
message: 'Gagal membuat rekap monitoring',
errorCode: 'recap_error',
);
}
}
/// Get slot index based on hour (0=morning, 1=afternoon, 2=night)
int _getSlotIndexForHour(int hour) {
if (hour >= 5 && hour < 12) {
return 0; // Morning (5-12)
} else if (hour >= 12 && hour < 17) {
return 1; // Afternoon (12-17)
} else {
return 2; // Night (17-5)
}
}
/// Format slot aggregate data to display text
String _formatSlotText(_SlotAggregate agg) {
if (!agg.hasData) return '-';
final phAvg = agg.phAverage?.toStringAsFixed(1);
final tdsAvg = agg.tdsAverage?.toStringAsFixed(0);
final phPart = phAvg != null ? 'pH $phAvg' : '';
final tdsPart = tdsAvg != null
? '${phPart.isNotEmpty ? ' / ' : ''}${tdsAvg}ppm'
: '';
return (phPart + tdsPart).isEmpty ? '-' : phPart + tdsPart;
}
ServiceResult<FlowDataError> getErrorInfoResult(Object error) {
return ServiceResult.success(
message: 'Error flow berhasil dipetakan',
data: FlowDataError.fromException(error),
);
}
ServiceResult<bool> hasActiveCycleResult(Map<String, dynamic>? cycleData) {
return ServiceResult.success(
message: 'Status siklus aktif berhasil diperiksa',
data: cycleData != null && cycleData.isNotEmpty,
);
}
ServiceResult<DateTime?> extractStartDateResult(
Map<String, dynamic>? cycleData,
) {
final startDate = CyclePhaseUtils.extractStartDate(cycleData);
return ServiceResult.success(
message: 'Tanggal mulai siklus berhasil diambil',
data: startDate,
);
}
}

View File

@ -0,0 +1,45 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/foundation.dart';
import 'package:mobile_monitoring/data/models/service_result.dart';
/// Service untuk Grafik Page - Backend Logic
class GraphService {
final FirebaseAuth _auth = FirebaseAuth.instance;
ServiceResult<String?> getCurrentUserIdResult() {
final uid = _auth.currentUser?.uid;
debugPrint('[GraphService] getCurrentUserIdResult()');
debugPrint(' Current user email: ${_auth.currentUser?.email}');
debugPrint(' Retrieved UID: "$uid"');
if (uid == null || uid.isEmpty) {
return ServiceResult.failure(
message: 'User tidak terautentikasi',
errorCode: 'unauthenticated',
);
}
return ServiceResult.success(
message: 'User ID berhasil diambil',
data: uid,
);
}
/// Get current user
User? getCurrentUser() {
return _auth.currentUser;
}
/// Validate user authentication
bool isUserAuthenticated() {
final user = _auth.currentUser;
final isAuth = user != null && user.uid.isNotEmpty;
debugPrint('[GraphService] isUserAuthenticated: $isAuth');
return isAuth;
}
/// Check if user ID is valid
bool isValidUserId(String? userId) {
final isValid = userId != null && userId.isNotEmpty;
debugPrint('[GraphService] isValidUserId("$userId"): $isValid');
return isValid;
}
}

View File

@ -0,0 +1,83 @@
import 'package:flutter/foundation.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
/// Service untuk data monitoring hidroponik (pH, nutrisi, suhu, dll)
class HidroponikDataService {
static const String collectionName = 'hidroponik_data';
CollectionReference<Map<String, dynamic>> get _collection =>
FirebaseFirestore.instance.collection(collectionName);
/// Stream data terbaru untuk kartu monitoring
Stream<Map<String, dynamic>?> latestDataStream({String? userId}) {
return _collection.snapshots().map((snapshot) {
if (snapshot.docs.isEmpty) return null;
final allDocs = snapshot.docs
.map((doc) => {...doc.data(), 'doc_id': doc.id})
.toList();
// Cari dokumen yang memiliki user_id cocok
final userDocs = (userId != null && userId.isNotEmpty)
? allDocs.where((d) => d['user_id'] == userId).toList()
: <Map<String, dynamic>>[];
// Jika ada dokumen sesuai user_id pakai userDocs, jika belum ada pakai allDocs (fallback)
final targetDocs = userDocs.isNotEmpty ? userDocs : allDocs;
targetDocs.sort((a, b) {
final aTime = a['timestamp'];
final bTime = b['timestamp'];
if (aTime is Timestamp && bTime is Timestamp) {
return bTime.compareTo(aTime);
} else if (aTime is DateTime && bTime is DateTime) {
return bTime.compareTo(aTime);
}
return 0;
});
return targetDocs.first;
}).handleError((error) {
debugPrint('[HidroponikDataService] latestDataStream error: $error');
return null;
});
}
/// Stream histori data untuk grafik
Stream<List<Map<String, dynamic>>> historyStream({
int limit = 7,
String? userId,
}) {
return _collection.snapshots().map((snapshot) {
if (snapshot.docs.isEmpty) return <Map<String, dynamic>>[];
final allDocs = snapshot.docs
.map((doc) => {...doc.data(), 'doc_id': doc.id})
.toList();
// Cari dokumen yang memiliki user_id cocok
final userDocs = (userId != null && userId.isNotEmpty)
? allDocs.where((d) => d['user_id'] == userId).toList()
: <Map<String, dynamic>>[];
// Jika ada dokumen sesuai user_id pakai userDocs, jika belum ada pakai allDocs (fallback)
final targetDocs = userDocs.isNotEmpty ? userDocs : allDocs;
targetDocs.sort((a, b) {
final aTime = a['timestamp'];
final bTime = b['timestamp'];
if (aTime is Timestamp && bTime is Timestamp) {
return bTime.compareTo(aTime);
} else if (aTime is DateTime && bTime is DateTime) {
return bTime.compareTo(aTime);
}
return 0;
});
return targetDocs.take(limit <= 0 ? 1 : limit).toList();
}).handleError((error) {
debugPrint('[HidroponikDataService] historyStream error: $error');
return <Map<String, dynamic>>[];
});
}
}

View File

@ -0,0 +1,200 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/foundation.dart';
import 'package:mobile_monitoring/data/models/service_result.dart';
/// Model untuk data monitoring
class MonitoringData {
final String ph;
final String nutrisi;
final String phStatus;
final String nutrientStatus;
final bool pumpPhUp;
final bool pumpPhDown;
final bool pumpNutrient;
final bool pumpWater;
final bool isPhAbnormal;
final bool isNutrientAbnormal;
MonitoringData({
required this.ph,
required this.nutrisi,
required this.phStatus,
required this.nutrientStatus,
required this.pumpPhUp,
required this.pumpPhDown,
required this.pumpNutrient,
required this.pumpWater,
required this.isPhAbnormal,
required this.isNutrientAbnormal,
});
}
/// Model untuk status error
class HomeDataError {
final String displayMessage;
final String detailMessage;
final ErrorType errorType;
HomeDataError({
required this.displayMessage,
required this.detailMessage,
required this.errorType,
});
factory HomeDataError.fromException(Object error) {
final errorMsg = error.toString();
debugPrint('[HomeService] Error: $errorMsg');
bool isPermissionError = errorMsg.contains('permission') || errorMsg.contains('Permission');
bool isNotAuthError = errorMsg.contains('terautentikasi');
if (isPermissionError) {
return HomeDataError(
displayMessage: 'Akses Ditolak',
detailMessage: 'Anda tidak memiliki izin untuk melihat data ini',
errorType: ErrorType.permission,
);
} else if (isNotAuthError) {
return HomeDataError(
displayMessage: 'Silakan Login',
detailMessage: 'Sesi Anda telah berakhir, login kembali',
errorType: ErrorType.authentication,
);
} else {
return HomeDataError(
displayMessage: 'Gagal memuat data',
detailMessage: 'Pastikan koneksi internet stabil',
errorType: ErrorType.network,
);
}
}
}
enum ErrorType {
permission,
authentication,
network,
unknown,
}
/// Service untuk Home Page - Backend Logic
class HomeService {
final FirebaseAuth _auth = FirebaseAuth.instance;
ServiceResult<String?> getCurrentUserIdResult() {
final uid = _auth.currentUser?.uid;
debugPrint('[HomeService] getCurrentUserIdResult()');
debugPrint(' Current user email: ${_auth.currentUser?.email}');
debugPrint(' Retrieved UID: "$uid"');
debugPrint(' UID length: ${uid?.length ?? 0}');
debugPrint(' UID is empty: ${uid?.isEmpty ?? true}');
if (uid == null || uid.isEmpty) {
return ServiceResult.failure(
message: 'User tidak terautentikasi',
errorCode: 'unauthenticated',
);
}
return ServiceResult.success(
message: 'User ID berhasil diambil',
data: uid,
);
}
/// Get current user
User? getCurrentUser() {
return _auth.currentUser;
}
/// Get user display name
String getUserDisplayName() {
final name = _auth.currentUser?.displayName;
return name?.isNotEmpty == true ? name! : 'Petani';
}
/// Get user initials untuk avatar
String getUserInitials(String? name) {
if (name == null || name.isEmpty) return 'U';
final parts = name.trim().split(' ');
if (parts.length >= 2) {
return '${parts[0][0]}${parts[1][0]}'.toUpperCase();
}
return name[0].toUpperCase();
}
MonitoringData _parseMonitoringData(Map<String, dynamic> data) {
debugPrint('[HomeService] parseMonitoringData()');
debugPrint(' Raw data: ${data.toString()}');
// Extract pump status
final pumpPhUp = data['pump_ph_up_status'] ?? false;
final pumpPhDown = data['pump_ph_down_status'] ?? false;
final pumpNutrient = data['pump_nutrient_status'] ?? false;
final pumpWater = data['pump_water_status'] ?? false;
// Determine abnormality status
final bool isPhAbnormal = pumpPhUp || pumpPhDown;
final bool isNutrientAbnormal = pumpNutrient || pumpWater;
// Determine user-friendly status
final String phStatus = isPhAbnormal ? 'Abnormal' : 'Normal';
final String nutrientStatus = isNutrientAbnormal ? 'Abnormal' : 'Normal';
// Extract and format values
final ph = data['pH_value']?.toString() ?? '-';
final nutrisi = data['nutrient_level']?.toString() ?? '-';
debugPrint(' Parsed pH: $ph, status: $phStatus');
debugPrint(' Parsed nutrient: $nutrisi, status: $nutrientStatus');
debugPrint(' Pump status - PhUp: $pumpPhUp, PhDown: $pumpPhDown, Nutrient: $pumpNutrient, Water: $pumpWater');
return MonitoringData(
ph: ph,
nutrisi: nutrisi,
phStatus: phStatus,
nutrientStatus: nutrientStatus,
pumpPhUp: pumpPhUp,
pumpPhDown: pumpPhDown,
pumpNutrient: pumpNutrient,
pumpWater: pumpWater,
isPhAbnormal: isPhAbnormal,
isNutrientAbnormal: isNutrientAbnormal,
);
}
ServiceResult<MonitoringData> parseMonitoringDataResult(Map<String, dynamic> data) {
try {
final parsed = _parseMonitoringData(data);
return ServiceResult.success(
message: 'Data monitoring berhasil diproses',
data: parsed,
);
} catch (e) {
return ServiceResult.failure(
message: 'Gagal memproses data monitoring',
errorCode: 'parse_error',
);
}
}
/// Validate user authentication
bool isUserAuthenticated() {
final user = _auth.currentUser;
final isAuth = user != null && user.uid.isNotEmpty;
debugPrint('[HomeService] isUserAuthenticated: $isAuth');
return isAuth;
}
/// Check if user ID is valid
bool isValidUserId(String? userId) {
final isValid = userId != null && userId.isNotEmpty;
debugPrint('[HomeService] isValidUserId("$userId"): $isValid');
return isValid;
}
ServiceResult<HomeDataError> getErrorInfoResult(Object error) {
return ServiceResult.success(
message: 'Error info berhasil dipetakan',
data: HomeDataError.fromException(error),
);
}
}

View File

@ -0,0 +1,140 @@
import 'package:flutter/foundation.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:mobile_monitoring/core/utils/cycle_phase_utils.dart';
/// Service untuk mengelola siklus tanam dengan fase pertumbuhan otomatis
class PlantingCyclesService {
static const String collectionName = 'planting_cycles';
CollectionReference<Map<String, dynamic>> get _collection =>
FirebaseFirestore.instance.collection(collectionName);
/// Stream siklus tanam yang sedang aktif (per user dengan fallback)
Stream<Map<String, dynamic>?> getActiveCycleStream({required String userId}) {
return _collection.snapshots().map((snapshot) {
if (snapshot.docs.isEmpty) return null;
if (userId.isNotEmpty) {
final matches = snapshot.docs.where((d) => d.id == userId).toList();
if (matches.isNotEmpty && matches.first.data().isNotEmpty) {
return {...matches.first.data(), 'doc_id': matches.first.id};
}
}
// Fallback ke dokumen siklus tanam pertama di database
final firstDoc = snapshot.docs.first;
return {...firstDoc.data(), 'doc_id': firstDoc.id};
}).handleError((error) {
debugPrint('[PlantingCyclesService] Stream error: $error');
return null;
});
}
/// Tambah siklus tanam baru (gunakan userId sebagai doc ID)
Future<String> addCycle({
required String userId,
required DateTime startDate,
}) async {
if (userId.isEmpty) throw Exception('User tidak terautentikasi');
await _collection.doc(userId).set({
'user_id': userId,
'start_date': Timestamp.fromDate(startDate),
'days_since_planting': 0,
'growth_phase': 'Semai',
'created_at': FieldValue.serverTimestamp(),
'updated_at': FieldValue.serverTimestamp(),
});
return userId;
}
/// Update hari dan fase tanam berdasarkan start_date
Future<void> updateCycleProgress({
required String userId,
required DateTime startDate,
}) async {
final now = DateTime.now();
final daysSincePlanting = now.difference(startDate).inDays;
final growthPhase = CyclePhaseUtils.calculateCyclePhase(daysSincePlanting);
await _collection.doc(userId).set({
'days_since_planting': daysSincePlanting,
'growth_phase': growthPhase,
'updated_at': FieldValue.serverTimestamp(),
}, SetOptions(merge: true));
}
/// Hitung hari total siklus untuk panen
Future<int> calculateCycleDaysBeforeDelete({required String userId}) async {
if (userId.isEmpty) throw Exception('User tidak terautentikasi');
final doc = await _collection.doc(userId).get();
if (!doc.exists || doc.data() == null) {
throw Exception('Siklus tanam aktif tidak ditemukan');
}
final data = doc.data()!;
final startValue = data['start_date'];
DateTime? startDate;
if (startValue is Timestamp) {
startDate = startValue.toDate();
} else if (startValue is DateTime) {
startDate = startValue;
}
final daysSincePlanting = startDate == null
? (data['days_since_planting'] as num?)?.toInt() ?? 0
: DateTime.now().difference(startDate).inDays;
return daysSincePlanting;
}
/// Hapus siklus tanam (gunakan userId)
Future<void> deleteCycle({required String userId}) async {
await _collection.doc(userId).delete();
}
/// Get detail siklus berdasarkan userId dengan update otomatis
Future<Map<String, dynamic>?> getCycleByUserId({
required String userId,
}) async {
final doc = await _collection.doc(userId).get();
if (!doc.exists || doc.data() == null) return null;
final data = doc.data()!;
final startDate = (data['start_date'] as Timestamp).toDate();
// Update progress sebelum return
await updateCycleProgress(userId: userId, startDate: startDate);
// Get data terbaru
final updatedDoc = await _collection.doc(userId).get();
if (!updatedDoc.exists) return null;
return {...updatedDoc.data()!, 'doc_id': updatedDoc.id};
}
/// Get siklus aktif dengan info fase terkini
Future<Map<String, dynamic>?> getActiveCycle({required String userId}) async {
if (userId.isEmpty) throw Exception('User tidak terautentikasi');
final doc = await _collection.doc(userId).get();
if (!doc.exists || doc.data() == null) return null;
final data = doc.data()!;
final startDate = (data['start_date'] as Timestamp).toDate();
// Hitung days_since_planting dan growth_phase terkini
final now = DateTime.now();
final daysSincePlanting = now.difference(startDate).inDays;
final growthPhase = CyclePhaseUtils.calculateCyclePhase(daysSincePlanting);
return {
...data,
'doc_id': doc.id,
'days_since_planting': daysSincePlanting,
'growth_phase': growthPhase,
};
}
}

View File

@ -0,0 +1,192 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/foundation.dart';
import 'package:mobile_monitoring/data/models/service_result.dart';
/// Model untuk user profile info
class UserProfileInfo {
final String? displayName;
final String? email;
final String initials;
UserProfileInfo({
required this.displayName,
required this.email,
required this.initials,
});
}
/// Service untuk Profile Page - Backend Logic
/// Tidak ada dependency ke UI (no BuildContext, no widgets)
class ProfileService {
final FirebaseAuth _auth = FirebaseAuth.instance;
/// Get current user profile info
UserProfileInfo? getCurrentUserProfile() {
final user = _auth.currentUser;
if (user == null) {
debugPrint('[ProfileService] No current user');
return null;
}
debugPrint('[ProfileService] getCurrentUserProfile()');
debugPrint(' Name: ${user.displayName}');
debugPrint(' Email: ${user.email}');
return UserProfileInfo(
displayName: user.displayName,
email: user.email,
initials: _generateInitials(user.displayName),
);
}
/// Get current user
User? getCurrentUser() {
return _auth.currentUser;
}
/// Generate initials dari nama
/// Business logic untuk format display initials
String _generateInitials(String? name) {
if (name == null || name.isEmpty) return 'U';
final parts = name.trim().split(' ');
if (parts.length >= 2) {
return '${parts[0][0]}${parts[1][0]}'.toUpperCase();
}
return name[0].toUpperCase();
}
/// Generate initials - exposed untuk public use
String generateInitials(String? name) {
return _generateInitials(name);
}
/// Update user profile name
/// Backend logic untuk update nama user
Future<ServiceResult<void>> updateUserName(String newName) async {
try {
final user = _auth.currentUser;
if (user == null) {
debugPrint('[ProfileService] No current user');
return ServiceResult.failure(
message: 'Silakan login terlebih dahulu',
errorCode: 'no_user',
);
}
debugPrint('[ProfileService] updateUserName() - new name: "$newName"');
// Validasi input
if (newName.trim().isEmpty) {
return ServiceResult.failure(
message: 'Nama tidak boleh kosong',
errorCode: 'empty_name',
);
}
// Update display name
await user.updateDisplayName(newName.trim());
await user.reload();
debugPrint('[ProfileService] ✓ Name updated successfully');
return ServiceResult.success(
message: 'Nama berhasil diperbarui',
);
} catch (e) {
debugPrint('[ProfileService] ✗ Failed to update name: $e');
return ServiceResult.failure(
message: 'Gagal memperbarui nama: $e',
errorCode: 'update_failed',
);
}
}
/// Validate password change
/// Business logic untuk validate password lama dan ganti dengan yang baru
Future<ServiceResult<void>> validateAndUpdatePassword(
String oldPassword,
String newPassword,
) async {
try {
final user = _auth.currentUser;
if (user == null || user.email == null) {
debugPrint('[ProfileService] No current user or email');
return ServiceResult.failure(
message: 'Silakan login terlebih dahulu',
errorCode: 'no_user',
);
}
debugPrint('[ProfileService] validateAndUpdatePassword()');
debugPrint(' Email: ${user.email}');
// Validasi input
if (oldPassword.isEmpty || newPassword.isEmpty) {
return ServiceResult.failure(
message: 'Password tidak boleh kosong',
errorCode: 'empty_password',
);
}
if (newPassword.length < 6) {
return ServiceResult.failure(
message: 'Password baru minimal 6 karakter',
errorCode: 'weak_password',
);
}
// Reauthenticate dengan password lama
debugPrint('[ProfileService] Reauthenticating...');
final credential = EmailAuthProvider.credential(
email: user.email!,
password: oldPassword,
);
try {
await user.reauthenticateWithCredential(credential);
debugPrint('[ProfileService] ✓ Reauthentication successful');
} on FirebaseAuthException catch (e) {
debugPrint('[ProfileService] ✗ Reauthentication failed: ${e.code}');
if (e.code == 'wrong-password') {
return ServiceResult.failure(
message: 'Password lama Anda salah',
errorCode: 'wrong_password',
);
}
return ServiceResult.failure(
message: 'Gagal memverifikasi password lama',
errorCode: e.code,
);
}
// Update password
debugPrint('[ProfileService] Updating password...');
await user.updatePassword(newPassword);
debugPrint('[ProfileService] ✓ Password updated successfully');
return ServiceResult.success(
message: 'Password berhasil diubah',
);
} catch (e) {
debugPrint('[ProfileService] ✗ Failed to update password: $e');
return ServiceResult.failure(
message: 'Gagal mengubah password: $e',
errorCode: 'update_failed',
);
}
}
/// Validate email
bool isValidEmail(String email) {
return email.isNotEmpty && email.contains('@');
}
/// Check if user is authenticated
bool isUserAuthenticated() {
final user = _auth.currentUser;
final isAuth = user != null && user.uid.isNotEmpty;
debugPrint('[ProfileService] isUserAuthenticated: $isAuth');
return isAuth;
}
}

View File

@ -0,0 +1,80 @@
import 'package:flutter/foundation.dart';
import 'package:mobile_monitoring/data/services/auth_service.dart';
import 'package:mobile_monitoring/data/services/users_service.dart';
import 'package:mobile_monitoring/data/services/planting_cycles_service.dart';
import 'package:mobile_monitoring/data/models/service_result.dart';
class RegisterRequest {
final String name;
final String email;
final String password;
final String confirmPassword;
RegisterRequest({
required this.name,
required this.email,
required this.password,
required this.confirmPassword,
});
}
class RegisterService {
final AuthService _authService;
final UsersService _usersService;
final PlantingCyclesService _cyclesService;
RegisterService({
AuthService? authService,
UsersService? usersService,
PlantingCyclesService? cyclesService,
}) : _authService = authService ?? AuthService(),
_usersService = usersService ?? UsersService(),
_cyclesService = cyclesService ?? PlantingCyclesService();
Future<ServiceResult<void>> register(RegisterRequest request) async {
final name = request.name.trim();
final email = request.email.trim();
final password = request.password;
final confirmPassword = request.confirmPassword;
if (name.isEmpty) return ServiceResult.failure(message: 'Nama tidak boleh kosong');
if (!_authService.isValidEmail(email)) return ServiceResult.failure(message: 'Email tidak valid');
if (!_authService.isValidPassword(password)) return ServiceResult.failure(message: 'Password minimal 6 karakter');
if (password != confirmPassword) return ServiceResult.failure(message: 'Password tidak cocok');
debugPrint('[RegisterService] Register attempt for: $email');
final result = await _authService.registerWithEmailAndPassword(
email: email,
password: password,
name: name,
);
if (result.success) {
final user = result.data;
if (user != null) {
try {
// 1. OTOMATIS BUILD COLLECTION 'users'
await _usersService.createUserDocument(user.uid, name, email);
// 2. OTOMATIS BUILD COLLECTION 'planting_cycles'
// Fungsi addCycle di PlantingCyclesService akan membuat fase 'Semai'
await _cyclesService.addCycle(
userId: user.uid,
startDate: DateTime.now(),
);
debugPrint('[RegisterService] Berhasil men-generate semua collection awal');
} catch (e) {
debugPrint('[RegisterService] Error saat membuat collection: $e');
}
}
return ServiceResult.success(
message: 'Registrasi berhasil! Selamat datang',
);
}
return ServiceResult.failure(message: result.message);
}
}

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