Initial commit

This commit is contained in:
Aditya 2026-08-03 12:15:55 +07:00
commit 528e4776ee
151 changed files with 12258 additions and 0 deletions

15
.gitignore vendored Normal file
View File

@ -0,0 +1,15 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties

3
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

1
.idea/.name Normal file
View File

@ -0,0 +1 @@
UI tugas akhir

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AppInsightsSettings">
<option name="tabSettings">
<map>
<entry key="Firebase Crashlytics">
<value>
<InsightsFilterSettings>
<option name="connection">
<ConnectionSetting>
<option name="appId" value="PLACEHOLDER" />
<option name="mobileSdkAppId" value="" />
<option name="projectId" value="" />
<option name="projectNumber" value="" />
</ConnectionSetting>
</option>
<option name="signal" value="SIGNAL_UNSPECIFIED" />
<option name="timeIntervalDays" value="THIRTY_DAYS" />
<option name="visibilityType" value="ALL" />
</InsightsFilterSettings>
</value>
</entry>
</map>
</option>
</component>
</project>

6
.idea/compiler.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="21" />
</component>
</project>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>

20
.idea/gradle.xml Normal file
View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings>
</option>
</component>
</project>

View File

@ -0,0 +1,47 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
</profile>
</component>

6
.idea/kotlinc.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.9.22" />
</component>
</project>

10
.idea/migrations.xml Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>

10
.idea/misc.xml Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>

1036
Backup.txt Normal file

File diff suppressed because it is too large Load Diff

1
app/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

26
app/RiwayatViewModel.kt Normal file
View File

@ -0,0 +1,26 @@
package com.adit.smartplant.viewmodel
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.adit.smartplant.model.RiwayatItem
import com.adit.smartplant.network.RetrofitClient
import kotlinx.coroutines.launch
class RiwayatViewModel : ViewModel() {
var riwayatList by mutableStateOf<List<RiwayatItem>>(emptyList())
private set
fun loadRiwayat(userId: Int) {
viewModelScope.launch {
try {
riwayatList = RetrofitClient.api.getRiwayat(userId)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}

107
app/build.gradle.kts Normal file
View File

@ -0,0 +1,107 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.ksp)
}
android {
namespace = "com.example.uitugasakhir"
compileSdk = 34
defaultConfig {
applicationId = "com.example.uitugasakhir"
minSdk = 24
targetSdk = 34
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.8"
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation("androidx.navigation:navigation-compose:2.7.7")
implementation("androidx.compose.material:material-icons-extended")
implementation("com.android.volley:volley:1.2.1")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
implementation("com.google.accompanist:accompanist-swiperefresh:0.32.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
implementation("com.airbnb.android:lottie-compose:6.4.0")
implementation("io.coil-kt:coil-compose:2.5.0")
implementation("io.coil-kt:coil-gif:2.5.0")
implementation("androidx.room:room-runtime:2.6.1")
implementation("androidx.room:room-ktx:2.6.1")
ksp("androidx.room:room-compiler:2.6.1")
implementation("org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5")
implementation("com.github.hannesa2:paho.mqtt.android:4.2.4")
implementation("androidx.localbroadcastmanager:localbroadcastmanager:1.1.0")
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
}

21
app/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,24 @@
package com.example.uitugasakhir
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.example.uitugasakhir", appContext.packageName)
}
}

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<application
android:allowBackup="true"
android:usesCleartextTraffic="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.UITugasAkhir"
tools:targetApi="31">
<service
android:name=".SensorMonitoringService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="dataSync" />
<service
android:name="info.mqtt.android.service.MqttService"
android:exported="false" />
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.UITugasAkhir">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 KiB

View File

@ -0,0 +1,878 @@
package com.example.uitugasakhir
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import com.example.app.ui.theme.Inria_Regular
import com.example.app.ui.theme.Inter
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.ui.platform.LocalContext
import android.widget.Toast
import com.android.volley.Request
import com.android.volley.toolbox.StringRequest
import com.android.volley.toolbox.Volley
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import org.json.JSONObject
import androidx.compose.runtime.LaunchedEffect
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.compose.runtime.Composable
import com.example.uitugasakhir.ViewModel.RiwayatViewModel
import com.example.uitugasakhir.ViewModel.MQTTViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import android.util.Log
import org.json.JSONArray
import com.example.uitugasakhir.ViewModel.SensorViewModel
import com.google.accompanist.swiperefresh.*
import com.example.uitugasakhir.model.JadwalItem
import com.example.uitugasakhir.ViewModel.ManualViewModel
import com.example.uitugasakhir.api.RetrofitClient
import androidx.compose.animation.core.tween
import com.example.uitugasakhir.ui_manual.SensorSection
import com.example.uitugasakhir.ui_manual.PenyiramanSection
import com.example.uitugasakhir.ui_manual.RiwayatSection
import com.example.uitugasakhir.ui_manual.RataRataSection
import com.example.uitugasakhir.ui_otomatis.JadwalSection
import com.example.uitugasakhir.ui_otomatis.JedaPenyiramanCard
import com.example.uitugasakhir.aksi.DeleteDialog
import com.example.uitugasakhir.ui.komponen.WarningPopup
import android.media.MediaPlayer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings
import com.example.uitugasakhir.ui_otomatis.SensorSectionOtomatis
import com.example.uitugasakhir.ui_otomatis.RiwayatSectionOtomatis
import com.example.uitugasakhir.ui_otomatis.AmbangBatasSection
import com.example.uitugasakhir.ViewModel.PumpViewModel
import com.example.uitugasakhir.ViewModel.MainViewModel
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import com.example.uitugasakhir.mqtt.MqttManager
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.ui.res.stringResource
import com.example.uitugasakhir.ui_otomatis.RataRataOtomatisSection
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ManualScreen(
navController: NavController,
sensorVm: SensorViewModel,
mainVm: MainViewModel
) {
// Riwayat viewModel
val viewModel = mainVm
val mainViewModel: MainViewModel = viewModel()
val mode = mainVm.mode
val isLoading = mainVm.isLoading
val context = LocalContext.current
val snackbarHostState = remember {
SnackbarHostState()
}
var showPopup by remember {
mutableStateOf(false)
}
val mqttVm: MQTTViewModel = viewModel()
val mqttState = mqttVm.state
val sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = false
)
var mutePopupUntil by remember {
mutableLongStateOf(0L)
}
var disablePopup1Minute by remember {
mutableStateOf(false)
}
var popupMessage by remember {
mutableStateOf("")
}
val mqtt = remember {
MqttManager(context)
}
val mediaPlayerManual = remember {
MediaPlayer.create(context, R.raw.peringatan)
}
DisposableEffect(Unit) {
onDispose {
mediaPlayerManual.release()
}
}
var autoStatus by remember {
mutableStateOf("Monitoring sistem")
}
LaunchedEffect(Unit) {
viewModel.initMQTT(context)
viewModel.loadRiwayat(context)
Log.d(
"LOAD_RIWAYAT",
"Size = ${viewModel.riwayatList.size}"
)
}
LaunchedEffect(Unit) {
mainViewModel.loadAverageSensor(context)
}
// Set pompa
val pumpVm: PumpViewModel = viewModel()
// ✅ FIX 1: rename supaya tidak bentrok
val manualViewModel: ManualViewModel = viewModel()
val swipeState = rememberSwipeRefreshState(manualViewModel.isLoading)
val riwayatVm: RiwayatViewModel = viewModel()
var showDeleteDialog by remember { mutableStateOf(false) }
var selectedDeleteId by remember { mutableStateOf("") }
var targetMode by remember { mutableStateOf("") }
var jeda by remember { mutableStateOf("") }
val formattedJeda = jeda.padStart(2, '0')
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
val scale by animateFloatAsState(
targetValue = if (isPressed) 0.95f else 1f,
label = "scale",
animationSpec = tween(300)
)
var selected by remember { mutableStateOf("Manual") }
var showConfirmDialog by remember { mutableStateOf(false) }
var showSettingDialog by remember { mutableStateOf(false) }
val status = sensorVm.status
//Pop up
LaunchedEffect(Unit) {
mqtt.connect(
onConnected = {
Log.d("MQTT", "CONNECTED")
},
onMessage = { topic, msg ->
try {
val json = JSONObject(msg)
// ================= ALARM =================
if (topic == "smartplant/alarm") {
val message =
json.optString("message")
if (message != "AMAN") {
val now = System.currentTimeMillis()
// CEK APAKAH POPUP SEDANG DIMUTE
if (now > mutePopupUntil) {
showPopup = true
popupMessage = message
}
} else {
showPopup = false
}
}
// ================= SENSOR =================
if (topic == "smartplant/sensor") {
val tanah =
json.optInt("tanah")
val hujan =
json.optInt("hujan")
Log.d(
"HUJAN_DEBUG",
"HUJAN = $hujan"
)
// ================= HUJAN =================
if (hujan > 80) {
autoStatus =
"Hujan terdeteksi.\nPenyiraman ditunda"
}
// ================= TANAH KERING =================
else if (tanah < 20) {
autoStatus =
"Penyiraman otomatis berjalan"
}
// ================= NORMAL =================
else {
autoStatus =
"Tanah lembab.\nPompa tidak aktif"
}
}
} catch (e: Exception) {
Log.e(
"MQTT_ERROR",
e.toString()
)
}
}
)
}
// LOAD DATA
LaunchedEffect(Unit) {
riwayatVm.loadRiwayat(1)
sensorVm.getData(context)
}
//========== Batas Akhir ============
var jedaDetik by remember {
mutableIntStateOf(0)
}
// Jadwal ui
var jadwalList by remember {
mutableStateOf(listOf<JadwalItem>())
}
val loadJadwal = remember {
{
val url = "http://kopikaosmartsystem.my.id/get_jadwal.php"
val request = StringRequest(
Request.Method.GET,
url,
{ response ->
try {
Log.d("API_RESPONSE", response)
val jsonObject = JSONObject(response)
val jsonArray = jsonObject.getJSONArray("jadwal")
val temp = mutableListOf<JadwalItem>()
for (i in 0 until jsonArray.length()) {
val obj = jsonArray.getJSONObject(i)
temp.add(
JadwalItem(
id = obj.getString("id"),
waktu = obj.getString("waktu"),
status = obj.getString("status")
)
)
}
jadwalList = temp
} catch (e: Exception) {
Log.e("JSON_ERROR", e.toString())
}
},
{ error ->
Log.e("API_ERROR", error.toString())
}
)
Volley.newRequestQueue(context).add(request)
}
}
LaunchedEffect(Unit) {
loadJadwal()
}
LaunchedEffect(status) {
if (status.isNotEmpty()) {
Toast.makeText(context, status.toString(), Toast.LENGTH_SHORT).show()
}
}
Box(
modifier = Modifier.fillMaxSize()
) {
Box(
modifier = Modifier.fillMaxSize()
) {
// HEADER HIJAU
Box(
modifier = Modifier
.fillMaxWidth()
.height(160.dp),
contentAlignment = Alignment.TopCenter
) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(0.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF2F6F4E)
)
) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(top = 22.dp),
contentAlignment = Alignment.TopCenter
) {
Image(
painter = painterResource(id = R.drawable.back),
contentDescription = "kembali",
modifier = Modifier
.size(15.dp)
.offset(x = (-160).dp, y = 1.dp)
.clickable {
navController.popBackStack()
}
)
Text(
text = "Penyiraman",
fontSize = 16.sp,
fontFamily = Inter,
fontWeight = FontWeight.Bold,
color = Color.White
)
}
}
}
// CONTENT PUTIH
Card(
modifier = Modifier
.padding(top = 64.dp)
.fillMaxSize(),
shape = RoundedCornerShape(topStart = 24.dp),
colors = CardDefaults.cardColors(
containerColor = Color.White
)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(top = 15.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// JUDUL MODE
Text(
text = stringResource(R.string.watering_mode),
fontSize = 15.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Medium,
color = Color.Black,
modifier = Modifier
.align(Alignment.Start)
.padding(horizontal = 28.dp)
)
Spacer(modifier = Modifier.height(5.dp))
Text(
text = stringResource(R.string.select_a_watering_mode),
fontSize = 12.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Normal,
color = Color.Gray,
modifier = Modifier.align(Alignment.Start).padding(horizontal = 28.dp)
)
Spacer(modifier = Modifier.height(15.dp))
// TOGGLE MANUAL OTOMATIS
Column(
modifier = Modifier
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally
) {
Card(
modifier = Modifier
.padding(horizontal = 25.dp)
.size(width = 333.dp, height = 100.dp),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFEDEDED)
),
elevation = CardDefaults.cardElevation(4.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp)
.offset(y = 23.dp)
.height(55.dp)
.clip(RoundedCornerShape(50.dp))
.background(Color(0xFFD9D9D9))
.padding(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
// ================= OTOMATIS =================
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.clip(RoundedCornerShape(20.dp))
.background(
if (selected == "Otomatis")
Color(0xFF2F6F4E)
else
Color.Transparent
)
.clickable {
if (selected != "Otomatis") {
targetMode = "Otomatis"
showConfirmDialog = true
}
},
contentAlignment = Alignment.Center
) {
Text(
"Otomatis",
color = if (selected == "Otomatis")
Color.White else Color.Black
)
}
// ================= MANUAL =================
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.clip(RoundedCornerShape(20.dp))
.background(
if (selected == "Manual")
Color(0xFF2F6F4E)
else
Color.Transparent
)
.clickable {
if (selected != "Manual") {
targetMode = "Manual"
showConfirmDialog = true
}
},
contentAlignment = Alignment.Center
) {
Text(
"Manual",
color = if (selected == "Manual")
Color.White else Color.Black
)
}
}
}
// Ikon Pengaturan
Box(
) {
if (selected == "Otomatis") {
Icon(
imageVector = Icons.Default.Settings,
contentDescription = "Pengaturan",
tint = Color.Black.copy(alpha = 0.7f), // 🔥 ganti putih biar kelihatan (background abu2)
modifier = Modifier
.align(Alignment.TopEnd)
.padding(top = 15.dp, end = 8.dp)
.offset(x = 150.dp)
.size(30.dp)
.clickable {
showSettingDialog = true
}
)
}
}
Spacer(modifier = Modifier.height(10.dp))
// Tampilan monitoring otomatis
if (selected == "Otomatis") {
SensorSectionOtomatis()
Spacer(modifier = Modifier.height(15.dp))
RataRataOtomatisSection(
snackbarHostState = snackbarHostState
)
Text(
text = stringResource(R.string.system_status),
fontSize = 15.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Medium,
color = Color.Black,
modifier = Modifier
.padding(top = 15.dp)
.padding(horizontal = 25.dp)
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(5.dp))
Text(
text = stringResource(R.string.displays_the_status_of_the_sensors_pump_and_watering_mode),
fontSize = 12.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Normal,
color = Color.Gray,
modifier = Modifier.padding(horizontal = 25.dp)
)
Card(
modifier = Modifier
.padding(top = 15.dp)
.padding(horizontal = 25.dp)
.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFEDEDED)
),
elevation = CardDefaults.cardElevation(4.dp)
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(12.dp)
.background(
when {
autoStatus.contains("Hujan") ->
Color(0xFF2196F3)
autoStatus.contains("ditunda") ->
Color(0xFF2196F3)
autoStatus.contains("berjalan") ->
Color(0xFF2F6F4E)
autoStatus.contains("Menunggu") ->
Color(0xFFFF9800)
autoStatus.contains("Jeda") ->
Color(0xFFFF9800)
autoStatus.contains("lembab") ->
Color.Gray
else ->
Color.LightGray
},
RoundedCornerShape(50)
)
)
Spacer(modifier = Modifier.width(10.dp))
Text(
text = autoStatus,
fontSize = 14.sp,
color = Color.DarkGray
)
}
}
}
Spacer(modifier = Modifier.height(15.dp))
if (selected == "Otomatis") {
RiwayatSectionOtomatis(viewModel.riwayatList)
}
}
// Pop up Konfirmasi
if (showConfirmDialog) {
AlertDialog(
onDismissRequest = {
showConfirmDialog = false
},
title = {
Text("Konfirmasi")
},
text = {
Text("Anda akan beralih ke mode $targetMode. Tekan OK untuk melanjutkan.")
},
confirmButton = {
TextButton(
onClick = {
selected = targetMode
mqttVm.setMode(targetMode)
showConfirmDialog = false
sensorVm.setMode(targetMode)
mqttVm.setMode(targetMode)
}
) {
Text("OK")
}
},
dismissButton = {
TextButton(
onClick = {
showConfirmDialog = false
}
) {
Text("Batal")
}
}
)
}
// popup pengaturan jadwal
if (showSettingDialog) {
ModalBottomSheet(
onDismissRequest = {
showSettingDialog = false
},
sheetState = sheetState,
containerColor = Color.White
) {
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.7f) // 70% tinggi layar
.padding(16.dp)
.verticalScroll(rememberScrollState())
) {
Text(
text = "Pengaturan Mode Otomatis",
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(16.dp))
JadwalSection(
jadwalList = jadwalList,
loadJadwal = { loadJadwal() },
context = context,
onDeleteClick = { id ->
selectedDeleteId = id
showDeleteDialog = true
}
)
Spacer(modifier = Modifier.height(15.dp))
JedaPenyiramanCard(
onJedaChanged = {
jedaDetik = it
}
)
Spacer(modifier = Modifier.height(15.dp))
AmbangBatasSection()
Spacer(modifier = Modifier.height(20.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
TextButton(
onClick = {
showSettingDialog = false
}
) {
Text("Batal")
}
Button(
onClick = {
selected = "Otomatis"
viewModel.setMode("Otomatis")
showSettingDialog = false
}
) {
Text("Simpan")
}
}
Spacer(modifier = Modifier.height(20.dp))
}
}
}
//Aksi tombol delete
if (showDeleteDialog) {
DeleteDialog(
showDialog = true,
selectedId = selectedDeleteId,
context = context,
onDismiss = {
showDeleteDialog = false
},
onSuccess = {
loadJadwal()
}
)
}
// KONTEN BERDASARKAN MODE
when (selected) {
"Manual" -> {
// Tampilan utama monitoring sensor
Box(
modifier = Modifier.fillMaxSize()
) {
// =================== SEMUA KONTEN UTAMA ===================
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
// Tampilan utama monitoring sensor
SensorSection()
Spacer(modifier = Modifier.height(15.dp))
RataRataSection(
snackbarHostState = snackbarHostState
)
Spacer(modifier = Modifier.height(15.dp))
// Tampilan utama penyiraman sensor
PenyiramanSection(
mode = mode,
pumpStatus = mqttState.pump,
isLoading = isLoading,
onPumpChange = { status ->
// publish MQTT
mqttVm.setPump(status == "ON")
// simpan riwayat
viewModel.onPumpChange(
context = context,
status = status,
mode = mode
)
}
)
Spacer(modifier = Modifier.height(15.dp))
// Tampilan utama riwayat sensor
RiwayatSection(
riwayatList = viewModel.riwayatList
)
}
}
}
}
}
}
}
}
WarningPopup(
show = showPopup,
message = popupMessage,
onDismiss = {
// JIKA CHECKBOX AKTIF
if (disablePopup1Minute) {
mutePopupUntil =
System.currentTimeMillis() + 60000
}
showPopup = false
},
disablePopup1Minute = disablePopup1Minute,
onDisableChange = {
disablePopup1Minute = it
}
)
}

View File

@ -0,0 +1,377 @@
package com.example.uitugasakhir.Page
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import com.example.uitugasakhir.R
import com.example.uitugasakhir.ViewModel.SensorViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.*
import androidx.compose.foundation.clickable
import android.util.Log
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NotifikasiScreen(
navController: NavController,
sensorVm: SensorViewModel
) {
val list = sensorVm.warningList
var selectedFilter by remember {
mutableStateOf("Semua")
}
val filteredList = when (selectedFilter) {
"Tanah" ->
list.filter {
it.message.contains("tanah", ignoreCase = true)
}
"Udara" ->
list.filter {
it.message.contains("udara", ignoreCase = true)
}
else -> list
}
LaunchedEffect(Unit) {
sensorVm.loadLogs()
}
val selected = sensorVm.selectedItems
LaunchedEffect(Unit) {
sensorVm.readAllNotifications()
}
LaunchedEffect(list) {
Log.d("NOTIF_DEBUG", "Jumlah notif: ${list.size}")
list.forEach {
Log.d("NOTIF_DEBUG", "Item: ${it.id} - ${it.message}")
}
}
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
Box(modifier = Modifier.fillMaxSize()) {
// =========================
// HEADER HIJAU
// =========================
Box(
modifier = Modifier
.fillMaxWidth()
.height(160.dp)
.background(Color(0xFF2F6F4E)),
contentAlignment = Alignment.TopCenter
) {
Image(
painter = painterResource(id = R.drawable.back),
contentDescription = "back",
modifier = Modifier
.size(18.dp)
.offset(x = -160.dp, y = 30.dp)
.clickable {
navController.popBackStack()
}
)
Text(
text = "Notifikasi",
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = Color.White,
modifier = Modifier.padding(top = 30.dp)
)
IconButton(
onClick = {
val deleted = list.toList()
sensorVm.deleteAllWarnings()
scope.launch {
val res = snackbarHostState.showSnackbar(
message = "Semua notifikasi dihapus",
actionLabel = "UNDO",
duration = SnackbarDuration.Short
)
if (res == SnackbarResult.ActionPerformed) {
sensorVm.restoreDeleted(deleted)
}
}
},
modifier = Modifier
.align(Alignment.TopEnd)
.padding(top = 20.dp, end = 16.dp)
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Hapus Semua",
tint = Color.White
)
}
}
// =========================
// CARD PUTIH FLOATING
// =========================
Card(
modifier = Modifier
.fillMaxSize()
.padding(top = 70.dp),
shape = RoundedCornerShape(topStart = 24.dp, topEnd = 0.dp),
colors = CardDefaults.cardColors(containerColor = Color.White)
) {
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) },
// =========================
// BOTTOM DELETE BAR
// =========================
bottomBar = {
if (selected.isNotEmpty()) {
BottomAppBar(containerColor = Color(0xFF1976D2)) {
Text(
text = "${selected.size} dipilih",
color = Color.White,
modifier = Modifier
.weight(1f)
.padding(start = 16.dp)
)
IconButton(onClick = {
val deleted = list.filter { it.id in selected }
sensorVm.deleteSelected()
scope.launch {
val res = snackbarHostState.showSnackbar(
message = "Terhapus",
actionLabel = "UNDO"
)
if (res == SnackbarResult.ActionPerformed) {
sensorVm.restoreDeleted(deleted)
}
}
}) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = null,
tint = Color.White
)
}
}
}
}
) { padding ->
Column(
modifier = Modifier
.padding(padding)
.fillMaxSize()
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
FilterChip(
selected = selectedFilter == "Semua",
onClick = {
selectedFilter = "Semua"
},
label = {
Text("Semua")
}
)
FilterChip(
selected = selectedFilter == "Tanah",
onClick = {
selectedFilter = "Tanah"
},
label = {
Text("🌱 Tanah")
}
)
FilterChip(
selected = selectedFilter == "Udara",
onClick = {
selectedFilter = "Udara"
},
label = {
Text("🌡 Udara")
}
)
}
LazyColumn(
modifier = Modifier.weight(1f)
) {
items(filteredList, key = { it.id }) { item ->
Log.d("NOTIF_ITEM", "Render item: ${item.id}")
val isSelected = selected.contains(item.id)
val bgColor by animateColorAsState(
if (isSelected) Color(0xFFE3F2FD) else Color.White
)
// =========================
// SWIPE DELETE (VERSI STABIL)
// =========================
SwipeToDismissBox(
state = rememberSwipeToDismissBoxState(
confirmValueChange = { value ->
if (value == SwipeToDismissBoxValue.EndToStart ||
value == SwipeToDismissBoxValue.StartToEnd
) {
sensorVm.deleteWarning(item)
scope.launch {
snackbarHostState.showSnackbar("Dihapus")
}
}
true
}
),
backgroundContent = {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Red)
.padding(end = 20.dp),
contentAlignment = Alignment.CenterEnd
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = null,
tint = Color.White
)
}
}
) {
// =========================
// ITEM UI
// =========================
Row(
modifier = Modifier
.fillMaxWidth()
.background(bgColor)
.pointerInput(Unit) {
detectTapGestures(
onTap = {
if (selected.isNotEmpty()) {
sensorVm.toggleSelection(item)
}
},
onLongPress = {
sensorVm.toggleSelection(item)
}
)
}
.padding(16.dp)
) {
val icon = when {
item.message.contains("tanah", true) -> "🌱"
item.message.contains("udara", true) -> "🌡"
else -> "⚠️"
}
Text(icon, fontSize = 20.sp)
Spacer(modifier = Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = item.status,
fontSize = 12.sp,
color = Color(0xFF2F6F4E),
fontWeight = FontWeight.Bold
)
Text(
text = item.message,
fontSize = 14.sp
)
Text(
text = formatTime(item.time),
fontSize = 12.sp,
color = Color.Gray
)
}
if (isSelected) {
Text("", color = Color(0xFF1976D2))
}
}
}
Divider()
}
}
}
}
}
}
}
// =========================
// FORMAT TIME
// =========================
fun formatTime(time: Long): String {
val sdf = SimpleDateFormat("HH:mm", Locale.getDefault())
return sdf.format(Date(time))
}

View File

@ -0,0 +1,324 @@
package com.example.uitugasakhir
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.runtime.Composable
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.mutableStateOf
import com.example.app.ui.theme.Inria_Regular
import com.example.app.ui.theme.Inter
import androidx.navigation.NavController
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.draw.clip
import androidx.compose.foundation.background
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
@Composable
fun OtomatisScreen(navController: NavController) {
var selected by remember { mutableStateOf("Manual") }
var selectedPump by remember { mutableStateOf("Matikan pompa") }
Box(
modifier = Modifier
.fillMaxSize()
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(160.dp),
contentAlignment = Alignment.TopCenter
) {
// Card hijau di atas
Card(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(0.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF2F6F4E)
)
) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(top = 22.dp),
contentAlignment = Alignment.TopCenter
) {
Image(
painter = painterResource(id = R.drawable.back),
contentDescription = "kembali",
modifier = Modifier
.size(15.dp)
.offset(x = -160.dp, y = 1.dp)
.clickable {
navController.popBackStack()
}
)
Text(
text = "Adityaaaa",
fontSize = 16.sp,
fontFamily = Inter,
fontWeight = FontWeight.Bold,
color = Color.White
)
}
}
}
Card(
modifier = Modifier
.padding(top = 64.dp)
.fillMaxSize(),
shape = RoundedCornerShape(topStart = 24.dp, topEnd = 0.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFFFFFFF)
)
) {
Column(
modifier = Modifier
.padding(top = 15.dp)
.fillMaxSize(),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Mode Penyiraman",
fontSize = 15.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Medium,
color = Color(0XFF6B6666).copy(alpha = 0.7f),
modifier = Modifier
.align(Alignment.Start)
.padding(horizontal = 28.dp)
)
Spacer(modifier = Modifier.height(15.dp))
Box(
modifier = Modifier
.wrapContentHeight()
.padding(bottom = 15.dp)
.padding(horizontal = 25.dp),
contentAlignment = Alignment.TopCenter
) {
Card(
modifier = Modifier
.size(width = 333.dp, height = 100.dp),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFEDEDED)
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp)
.offset(y = 23.dp)
.height(55.dp)
.clip(RoundedCornerShape(50.dp))
.background(Color(0xFFD9D9D9))
.padding(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
// OTOMATIS
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.clip(RoundedCornerShape(20.dp))
.background(
if (selected == "Otomatis") Color(0xFF2F6F4E) else Color.Transparent
)
.clickable { selected = "Otomatis"
navController.navigate("otomatis") },
contentAlignment = Alignment.Center
) {
Text(
text = "Otomatis",
color = if (selected == "Otomatis") Color.White else Color.Black
)
}
// MANUAL
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.clip(RoundedCornerShape(20.dp))
.background(
if (selected == "Manual") Color(0xFF2F6F4E) else Color.Transparent
)
.clickable { selected = "Manual"
navController.navigate("manual")},
contentAlignment = Alignment.Center
) {
Text(
text = "Manual",
color = if (selected == "Manual") Color.White else Color.Black
)
}
}
}}
Text(
text = "Pemantauan sensor",
fontSize = 15.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Medium,
color = Color(0XFF6B6666).copy(alpha = 0.7f),
modifier = Modifier
.align(Alignment.Start)
.padding(horizontal = 28.dp)
)
Spacer(modifier = Modifier.height(15.dp))
Box(
modifier = Modifier
.wrapContentHeight()
.padding(bottom = 15.dp)
.padding(horizontal = 25.dp),
contentAlignment = Alignment.TopCenter
) {
Card(
modifier = Modifier
.size(width = 333.dp, height = 100.dp),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFEDEDED)
)
) {}}
Text(
text = "Penyiraman",
fontSize = 15.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Medium,
color = Color(0XFF6B6666).copy(alpha = 0.7f),
modifier = Modifier
.align(Alignment.Start)
.padding(horizontal = 28.dp)
)
Spacer(modifier = Modifier.height(15.dp))
Box(
modifier = Modifier
.wrapContentHeight()
.padding(bottom = 15.dp)
.padding(horizontal = 25.dp),
contentAlignment = Alignment.TopCenter
) {
Card(
modifier = Modifier
.size(width = 333.dp, height = 100.dp),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFEDEDED)
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp)
.offset(y = 23.dp)
.height(55.dp)
.clip(RoundedCornerShape(50.dp))
.background(Color(0xFFD9D9D9))
.padding(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
// OTOMATIS
Box(
modifier = Modifier
.weight(1f)
.height(60.dp)
.clip(RoundedCornerShape(20.dp))
.background(
if (selectedPump == "Hidupkan pompa") Color(0xFF2F6F4E) else Color.Transparent
)
.clickable { selectedPump = "Hidupkan pompa" },
contentAlignment = Alignment.Center
) {
Text(
text = "Hidupkan pompa",
color = if (selectedPump == "Hidupkan pompa") Color.White else Color.Black
)
}
// MANUAL
Box(
modifier = Modifier
.weight(1f)
.height(60.dp)
.clip(RoundedCornerShape(20.dp))
.background(
if (selectedPump == "Matikan pompa") Color(0xFFD81F23) else Color.Transparent
)
.clickable { selectedPump = "Matikan pompa" },
contentAlignment = Alignment.Center
) {
Text(
text = "Matikan pompa",
color = if (selectedPump == "Matikan pompa") Color.White else Color.Black
)
}
}
}}
Text(
text = "Riwayat penyiraman",
fontSize = 15.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Medium,
color = Color(0XFF6B6666).copy(alpha = 0.7f),
modifier = Modifier
.align(Alignment.Start)
.padding(horizontal = 28.dp)
)
Spacer(modifier = Modifier.height(15.dp))
Box(
modifier = Modifier
.wrapContentHeight()
.padding(bottom = 15.dp)
.padding(horizontal = 25.dp),
contentAlignment = Alignment.TopCenter
) {
Card(
modifier = Modifier
.size(width = 333.dp, height = 100.dp),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFEDEDED)
)
) {}}
}
}
}
}

View File

@ -0,0 +1,162 @@
package com.example.uitugasakhir
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.runtime.Composable
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.res.painterResource
import com.example.app.ui.theme.Inria_Regular
import com.example.app.ui.theme.Inter
import androidx.navigation.NavController
import android.widget.Toast
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import com.example.uitugasakhir.pengaturan.AkunCardSection
import com.example.uitugasakhir.pengaturan.BahasaCardSection
import com.example.uitugasakhir.pengaturan.BantuanCardSection
import com.example.uitugasakhir.pengaturan.NotifikasiCardSection
import com.example.uitugasakhir.ViewModel.MainViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.uitugasakhir.ViewModel.SensorViewModel
@Composable
fun PengaturanScreen(navController: NavController) {
val context = LocalContext.current
val mainViewModel: MainViewModel = viewModel()
val sensorViewModel: SensorViewModel = viewModel()
Box(
modifier = Modifier
.fillMaxSize()
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(160.dp),
contentAlignment = Alignment.TopCenter
) {
// Card hijau di atas
Card(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(0.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF2F6F4E)
)
) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(top = 22.dp),
contentAlignment = Alignment.TopCenter
) {
Image(
painter = painterResource(id = R.drawable.back),
contentDescription = "kembali",
modifier = Modifier
.size(15.dp)
.offset(x = -160.dp, y = 1.dp)
.clickable {
navController.popBackStack()
}
)
Text(
text = stringResource(R.string.setting),
fontSize = 16.sp,
fontFamily = Inter,
fontWeight = FontWeight.Bold,
color = Color.White
)
}
}
}
Card(
modifier = Modifier
.padding(top = 64.dp)
.verticalScroll(rememberScrollState())
.fillMaxSize(),
shape = RoundedCornerShape(topStart = 24.dp, topEnd = 0.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFFFFFFF)
)
) {
Column(
modifier = Modifier
.padding(top = 15.dp)
.fillMaxSize(),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally
) {
// Tampilan akun
AkunCardSection(viewModel = mainViewModel)
Spacer(modifier = Modifier.height(15.dp))
// Tampilan Bahasa
BahasaCardSection(
viewModel = mainViewModel
)
Spacer(modifier = Modifier.height(15.dp))
// Tampilan Notifikasi
NotifikasiCardSection(
viewModel = mainViewModel,
sensorViewModel = sensorViewModel
)
Spacer(modifier = Modifier.height(15.dp))
// Tampilan Bantuan
BantuanCardSection(
viewModel = mainViewModel,
navController = navController
)
Spacer(modifier = Modifier.height(15.dp))
Text(
text = stringResource(R.string.out),
color = Color.Red,
fontWeight = FontWeight.Bold,
modifier = Modifier
.padding(28.dp)
.clickable {
Toast.makeText(context, "Berhasil logout", Toast.LENGTH_SHORT).show()
// contoh pindah ke login
navController.navigate("login") {
popUpTo(0)
}
}
)
}
}
}
}

View File

@ -0,0 +1,856 @@
package com.example.uitugasakhir
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.navigation.NavController
import androidx.compose.foundation.background
import android.content.Context
import android.os.Environment
import android.widget.Toast
import java.io.File
import java.io.FileWriter
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.draw.scale
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.uitugasakhir.model.RiwayatItem
import com.example.uitugasakhir.ViewModel.MainViewModel
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import android.content.ContentValues
import android.os.Build
import android.provider.MediaStore
import android.util.Log
import java.text.SimpleDateFormat
import java.util.Locale
import androidx.compose.material.icons.filled.Download
import com.example.app.ui.theme.Inria_Regular
@Composable
fun RiwayatScreen(navController: NavController) {
val viewModel: MainViewModel = viewModel()
//Riwayat hari ini, minggu ini dan bulan ini
val data = viewModel.riwayatList
val penyiramanData = viewModel.riwayatList.filter {
it.aksi == "Pompa Dinyalakan"
}
val dataMingguan = viewModel.getDataMingguan(penyiramanData)
val dataHarian = viewModel.getDataHarian(penyiramanData)
// Halaman tabel selanjutnya dan sebelumnya
var currentPage by remember { mutableStateOf(0) }
val pageSize = 10
var isDownloading by remember { mutableStateOf(false) }
var isSuccess by remember { mutableStateOf<Boolean?>(null) }
val context = LocalContext.current
val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
val scale by animateFloatAsState(
targetValue = if (isDownloading) 0.9f else 1f,
animationSpec = tween(200)
)
// STATE
var showFileNameDialog by remember { mutableStateOf(false) }
var fileName by remember { mutableStateOf("") }
var selectedFilter by remember { mutableStateOf("Semua") }
var selectedDateFilter by remember { mutableStateOf("Semua") }
// Total penyiraman
val sdfServer = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
val dataHariIni = penyiramanData.count {
val split = it.waktu.split(" ")
val tanggal = split.getOrNull(0) ?: return@count false
val date = sdfServer.parse(tanggal) ?: return@count false
val cal1 = java.util.Calendar.getInstance()
val cal2 = java.util.Calendar.getInstance()
cal1.time = date
cal2.time = java.util.Date()
cal1.get(java.util.Calendar.DAY_OF_YEAR) ==
cal2.get(java.util.Calendar.DAY_OF_YEAR) &&
cal1.get(java.util.Calendar.YEAR) ==
cal2.get(java.util.Calendar.YEAR)
}
val dataMingguIni = penyiramanData.count {
val split = it.waktu.split(" ")
val tanggal = split.getOrNull(0) ?: return@count false
val date = sdfServer.parse(tanggal) ?: return@count false
val cal1 = java.util.Calendar.getInstance()
val cal2 = java.util.Calendar.getInstance()
cal1.time = date
cal2.time = java.util.Date()
cal1.get(java.util.Calendar.WEEK_OF_YEAR) ==
cal2.get(java.util.Calendar.WEEK_OF_YEAR) &&
cal1.get(java.util.Calendar.YEAR) ==
cal2.get(java.util.Calendar.YEAR)
}
val dataBulanIni = penyiramanData.count {
val split = it.waktu.split(" ")
val tanggal = split.getOrNull(0) ?: return@count false
val date = sdfServer.parse(tanggal) ?: return@count false
val cal1 = java.util.Calendar.getInstance()
val cal2 = java.util.Calendar.getInstance()
cal1.time = date
cal2.time = java.util.Date()
cal1.get(java.util.Calendar.MONTH) ==
cal2.get(java.util.Calendar.MONTH) &&
cal1.get(java.util.Calendar.YEAR) ==
cal2.get(java.util.Calendar.YEAR)
}
val totalData = penyiramanData.size
// ================= FILTER FIX =================
val filteredData = data.filter { item ->
val split = item.waktu.split(" ")
val tanggal = split.getOrNull(0) ?: ""
val jam = split.getOrNull(1) ?: ""
val matchFilter =
selectedFilter == "Semua" ||
item.mode == selectedFilter ||
item.status == selectedFilter
val matchDate = when (selectedDateFilter) {
"Hari ini" -> {
val todayStr = java.text.SimpleDateFormat("dd-MM-yyyy")
.format(java.util.Date())
tanggal == todayStr
}
else -> true
}
matchFilter && matchDate
}
val totalPage = (filteredData.size + pageSize - 1) / pageSize
val pagedData = filteredData
.drop(currentPage * pageSize)
.take(pageSize)
LaunchedEffect(Unit) {
viewModel.loadRiwayat(context)
}
// ================= UI =================
Box(modifier = Modifier.fillMaxSize()) {
SnackbarHost(
hostState = snackbarHostState,
modifier = Modifier.align(Alignment.BottomCenter)
)
// HEADER
Card(
modifier = Modifier
.fillMaxWidth()
.height(160.dp),
shape = RoundedCornerShape(0.dp),
colors = CardDefaults.cardColors(Color(0xFF2F6F4E))
) {
Box(
modifier = Modifier.fillMaxSize().padding(top = 22.dp),
contentAlignment = Alignment.TopCenter
) {
Image(
painter = painterResource(id = R.drawable.back),
contentDescription = "back",
modifier = Modifier
.size(15.dp)
.offset(x = (-160).dp)
.clickable { navController.popBackStack() }
)
Text(
text = "Riwayat",
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = Color.White
)
}
}
// CONTENT
Card(
modifier = Modifier
.padding(top = 58.dp)
.fillMaxSize(),
shape = RoundedCornerShape(topStart = 24.dp),
colors = CardDefaults.cardColors(Color.White)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
.verticalScroll(rememberScrollState())
.animateContentSize()
) {
Text("Total Penyiraman",
fontSize = 15.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Medium,
color = Color.Gray,
)
Spacer(modifier = Modifier.height(15.dp))
// SUMMARY FIX
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(Color(0xFFF2F2F2)),
elevation = CardDefaults.cardElevation(4.dp)
) {
Column(modifier = Modifier.padding(12.dp)) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceAround
) {
SummaryMini("Hari", dataHariIni)
SummaryMini("Minggu", dataMingguIni)
SummaryMini("Bulan", dataBulanIni)
SummaryMini("Total", totalData)
}
}
}
Spacer(modifier = Modifier.height(15.dp))
Text("Grafik Penyiraman Minggu Ini",
fontSize = 15.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Medium,
color = Color.Gray,
)
Spacer(modifier = Modifier.height(15.dp))
Card(
modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(Color(0xFFF2F2F2)),
elevation = CardDefaults.cardElevation(4.dp)
) {
Column(Modifier.padding(16.dp)) {
Text("Minggu Ini", fontWeight = FontWeight.Normal)
Spacer(Modifier.height(12.dp))
BarChart(dataHarian)
}
}
Spacer(modifier = Modifier.height(15.dp))
Text("Grafik Penyiraman Bulan Ini",
fontSize = 15.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Medium,
color = Color.Gray,
)
Spacer(modifier = Modifier.height(15.dp))
Card(
modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(Color(0xFFF2F2F2)),
elevation = CardDefaults.cardElevation(4.dp)
) {
Column(Modifier.padding(16.dp)) {
Text("Bulan Ini", fontWeight = FontWeight.Normal)
Spacer(Modifier.height(12.dp))
BarChart(dataMingguan)
}
}
Spacer(modifier = Modifier.height(15.dp))
Text(
text = "Mode",
fontSize = 15.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Medium,
color = Color.Gray,
)
Spacer(modifier = Modifier.height(15.dp))
LazyRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(horizontal = 4.dp)
) {
items(listOf("Semua", "Otomatis", "Manual", "Berhasil", "Gagal")) { item ->
FilterChipModern(
label = item,
selected = selectedFilter == item,
onClick = { selectedFilter = item }
)
}
}
Spacer(modifier = Modifier.height(10.dp))
Text(
text = "Periode",
fontSize = 15.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Medium,
color = Color.Gray,
)
Spacer(modifier = Modifier.height(15.dp))
LazyRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(horizontal = 4.dp)
) {
items(listOf("Semua", "Hari ini", "Minggu ini", "Bulan ini")) { item ->
FilterChipModern(
label = item,
selected = selectedDateFilter == item,
onClick = { selectedDateFilter = item }
)
}
}
Spacer(modifier = Modifier.height(15.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Data: ${filteredData.size}",
fontSize = 13.sp,
color = Color.Gray
)
Button(
onClick = {
if (filteredData.isEmpty()) {
Toast.makeText(
context,
"Tidak ada data",
Toast.LENGTH_SHORT
).show()
} else {
fileName = ""
showFileNameDialog = true
}
}, enabled = !isDownloading,
modifier = Modifier
.scale(scale)
.height(42.dp),
shape = RoundedCornerShape(20.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF2F6F4E)
),
contentPadding = PaddingValues(horizontal = 14.dp)
) {
Icon(
imageVector = Icons.Default.Download,
contentDescription = "Download",
tint = Color.White,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(6.dp))
Text(
text = if (isDownloading) "Loading..." else "Unduh",
fontSize = 12.sp,
color = Color.White
)
}
}
Spacer(modifier = Modifier.height(15.dp))
// HEADER TABLE
Row(
modifier = Modifier
.fillMaxWidth()
.background(Color(0xFF2F6F4E))
.padding(8.dp)
) {
TableText("No", 0.1f)
TableText("Tanggal", 0.3f)
TableText("Jam", 0.2f)
TableText("Mode", 0.2f)
TableText("Status", 0.2f)
TableText("", 0.1f)
}
pagedData.forEachIndexed { index, item ->
val split = item.waktu.split(" ")
val tanggal = split.getOrNull(0) ?: "-"
val jam = split.getOrNull(1) ?: "-"
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
colors = CardDefaults.cardColors(Color(0xFFF8F8F8)),
shape = RoundedCornerShape(12.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
TableText("${index + 1 + (currentPage * pageSize)}", 0.1f)
TableText(tanggal, 0.25f)
TableText(jam, 0.2f)
TableText(item.mode, 0.2f)
TableText(item.status, 0.15f)
Box(
modifier = Modifier.weight(0.1f),
contentAlignment = Alignment.Center
) {
IconButton(
onClick = {
viewModel.deleteRiwayat(context, item)
}
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Hapus",
tint = Color.Red,
modifier = Modifier.size(18.dp)
)
}
}
}
}
}
Spacer(Modifier.height(10.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
Text(
"<",
modifier = Modifier
.clickable {
if (currentPage > 0) currentPage--
}
.padding(12.dp),
fontSize = 20.sp
)
Text(
"Page ${currentPage + 1} / $totalPage",
modifier = Modifier.padding(12.dp)
)
Text(
">",
modifier = Modifier
.clickable {
if (currentPage < totalPage - 1) currentPage++
}
.padding(12.dp),
fontSize = 20.sp
)
}
}
}
}
if (showFileNameDialog) {
AlertDialog(
onDismissRequest = {
showFileNameDialog = false
},
title = {
Text("Nama File")
},
text = {
OutlinedTextField(
value = fileName,
onValueChange = {
fileName = it
},
label = {
Text("Masukkan nama file")
},
singleLine = true
)
},
confirmButton = {
Button(
onClick = {
scope.launch {
isDownloading = true
val success = exportToCSV(
context,
filteredData,
if (fileName.isBlank())
"Riwayat.csv"
else
"$fileName.csv"
)
isDownloading = false
if (success) {
Toast.makeText(
context,
"Berhasil disimpan",
Toast.LENGTH_LONG
).show()
} else {
Toast.makeText(
context,
"Gagal menyimpan",
Toast.LENGTH_LONG
).show()
}
showFileNameDialog = false
}
}
) {
Text("Simpan")
}
},
dismissButton = {
TextButton(
onClick = {
showFileNameDialog = false
}
) {
Text("Batal")
}
}
)
}
}
// ================= COMPONENT FIX =================
@Composable
fun SummaryMini(label: String, value: Int) {
val isToday = label == "Hari"
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "$value",
fontWeight = FontWeight.Bold,
fontSize = 16.sp,
color = if (isToday) Color(0xFF2F6F4E) else Color.Black
)
Text(
text = label,
fontSize = 10.sp,
color = Color.Gray
)
}
}
@Composable
fun FilterChip(label: String, selected: Boolean, onClick: () -> Unit) {
Card(
modifier = Modifier.clickable { onClick() },
colors = CardDefaults.cardColors(
if (selected) Color(0xFF2F6F4E) else Color.LightGray
)
) {
Text(
text = label,
modifier = Modifier.padding(8.dp),
color = if (selected) Color.White else Color.Black
)
}
}
@Composable
fun RowScope.TableText(
text: String,
weight: Float,
color: Color = Color.Black
) {
Text(
text = text,
modifier = Modifier.weight(weight), // ✔ aman karena RowScope
fontSize = 12.sp,
color = color,
textAlign = TextAlign.Center
)
}
@Composable
fun TableText(
text: String,
modifier: Modifier = Modifier,
color: Color = Color.Black
) {
Text(
text = text,
modifier = modifier,
fontSize = 12.sp,
color = color,
textAlign = TextAlign.Center
)
}
// ================= EXPORT =================
fun exportToCSV(
context: Context,
data: List<RiwayatItem>,
fileName: String
): Boolean {
return try {
val values = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
put(MediaStore.MediaColumns.MIME_TYPE, "text/csv")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(
MediaStore.MediaColumns.RELATIVE_PATH,
Environment.DIRECTORY_DOWNLOADS
)
}
}
val collection =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
MediaStore.Downloads.EXTERNAL_CONTENT_URI
} else {
MediaStore.Files.getContentUri("external")
}
val uri = context.contentResolver.insert(
collection,
values
) ?: return false
context.contentResolver.openOutputStream(uri)?.use { output ->
output.write("No,Tanggal,Jam,Mode,Status\n".toByteArray())
data.forEachIndexed { index, item ->
val split = item.waktu.split(" ")
val tanggal = split.getOrNull(0) ?: "-"
val jam = split.getOrNull(1) ?: "-"
val row =
"${index + 1},$tanggal,$jam,${item.mode},${item.status}\n"
output.write(row.toByteArray())
}
output.flush()
}
Log.d("CSV", "Berhasil menyimpan: $fileName")
true
} catch (e: Exception) {
e.printStackTrace()
Log.e("CSV", "Gagal export", e)
false
}
}
@Composable
fun FilterChipModern(
label: String,
selected: Boolean,
onClick: () -> Unit
) {
Surface(
shape = RoundedCornerShape(20.dp),
color = if (selected) Color(0xFF2F6F4E) else Color(0xFFEDEDED),
shadowElevation = if (selected) 4.dp else 0.dp,
modifier = Modifier.clickable { onClick() }
) {
Text(
text = label,
modifier = Modifier.padding(horizontal = 14.dp, vertical = 8.dp),
fontSize = 12.sp,
color = if (selected) Color.White else Color.Black
)
}
}
@Composable
fun BarChart(data: List<Pair<String, Int>>) {
val maxValue = (data.maxOfOrNull { it.second } ?: 1).toFloat()
Row(
modifier = Modifier
.fillMaxWidth()
.height(150.dp),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.Bottom
) {
data.forEach { (label, value) ->
val heightRatio = value / maxValue
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("$value", fontSize = 10.sp)
Spacer(modifier = Modifier.height(4.dp))
Box(
modifier = Modifier
.width(20.dp)
.height((100 * heightRatio).dp)
.background(Color(0xFF2F6F4E), RoundedCornerShape(6.dp))
)
Spacer(modifier = Modifier.height(4.dp))
Text(label, fontSize = 10.sp)
}
}
}
}

View File

@ -0,0 +1,2 @@
package Page

View File

@ -0,0 +1,290 @@
package com.example.uitugasakhir
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.runtime.Composable
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.res.painterResource
import com.example.app.ui.theme.Inria_Bold
import com.example.app.ui.theme.Inria_Regular
import com.example.app.ui.theme.Inter
import androidx.navigation.NavController
import androidx.compose.runtime.*
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.runtime.*
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.foundation.gestures.detectTapGestures
import com.example.uitugasakhir.ViewModel.SensorViewModel
import com.example.uitugasakhir.ViewModel.MainViewModel
@Composable
fun DashboardScreen(
navController: NavController,
sensorVm: SensorViewModel,
mainVm: MainViewModel
) {
val unreadCount = sensorVm.unreadCount
Box(modifier = Modifier.fillMaxSize()) {
// HEADER
Box(
modifier = Modifier
.fillMaxWidth()
.height(160.dp),
contentAlignment = Alignment.TopCenter
) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(0.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF2F6F4E)
)
) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(top = 22.dp),
contentAlignment = Alignment.TopCenter
) {
Text(
text = "Beranda",
fontSize = 16.sp,
fontFamily = Inter,
fontWeight = FontWeight.Bold,
color = Color.White
)
}
}
}
// BODY
Card(
modifier = Modifier
.padding(top = 64.dp)
.fillMaxSize(),
shape = RoundedCornerShape(topStart = 24.dp),
colors = CardDefaults.cardColors(
containerColor = Color.White
)
) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(id = R.drawable.kopikakao),
contentDescription = "Logo",
modifier = Modifier.size(200.dp)
)
Text(
text = "Selamat Datang!",
fontSize = 20.sp,
fontFamily = Inria_Bold,
fontWeight = FontWeight.Bold,
modifier = Modifier
.align(Alignment.Start)
.padding(horizontal = 28.dp)
)
Spacer(modifier = Modifier.height(5.dp))
Text(
text = "Monitoring & Penyiraman",
fontSize = 15.sp,
fontFamily = Inria_Regular,
modifier = Modifier
.align(Alignment.Start)
.padding(horizontal = 28.dp)
)
Spacer(modifier = Modifier.height(5.dp))
Text(
text = "Kopi & Kakao",
fontSize = 15.sp,
fontFamily = Inria_Regular,
modifier = Modifier
.align(Alignment.Start)
.padding(horizontal = 28.dp)
)
Spacer(modifier = Modifier.height(30.dp))
// 🔥 CARD MENU (SUDAH FIX)
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 25.dp),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF2F6F4E)
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(15.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(15.dp)
) {
MenuCard(
"Monitoring &\nPenyiraman",
modifier = Modifier.weight(1f),
imageRes = R.drawable.sprinkler_irigation,
onClick = { navController.navigate("manual") }
)
MenuCard(
title = "Notifikasi",
modifier = Modifier.weight(1f),
imageRes = R.drawable.notification,
badgeCount = unreadCount,
onClick = {
navController.navigate("notifikasi")
}
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(15.dp)
) {
MenuCard(
"Riwayat",
modifier = Modifier.weight(1f),
imageRes = R.drawable.chronometer,
onClick = { navController.navigate("riwayat") }
)
MenuCard(
"Pengaturan",
modifier = Modifier.weight(1f),
imageRes = R.drawable.cogwheel,
onClick = { navController.navigate("pengaturan") }
)
}
}
}
}
}
}
}
@Composable
fun MenuCard(
title: String,
modifier: Modifier = Modifier,
badgeCount: Int = 0,
onClick: () -> Unit,
imageRes: Int
) {
var pressed by remember { mutableStateOf(false) }
val scale by animateFloatAsState(
targetValue = if (pressed) 1.1f else 1f, // zoom
animationSpec = spring(
dampingRatio = 0.5f,
stiffness = 300f
),
label = ""
)
Box(
modifier = modifier
.height(150.dp)
.width(140.dp)
.graphicsLayer {
scaleX = scale
scaleY = scale
}
.pointerInput(Unit) {
detectTapGestures(
onPress = {
pressed = true
tryAwaitRelease() // tunggu sampai dilepas
pressed = false
},
onTap = {
onClick()
}
)
}
) {
Card(
modifier = Modifier.fillMaxSize(),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFDCEFE3)
)
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
Column {
Image(
painter = painterResource(id = imageRes),
contentDescription = title,
modifier = Modifier
.size(30.dp)
.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = title,
textAlign = TextAlign.Center
)
}
}
}
// 🔴 BADGE
if (badgeCount > 0) {
Box(
modifier = Modifier
.align(Alignment.TopEnd)
.offset(x = (-6).dp, y = 6.dp)
.size(22.dp)
.background(Color.Red, RoundedCornerShape(50)),
contentAlignment = Alignment.Center
) {
Text(
text = badgeCount.toString(),
color = Color.White,
fontSize = 11.sp,
fontWeight = FontWeight.Bold
)
}
}
}
}

View File

@ -0,0 +1,45 @@
package com.example.uitugasakhir
import android.window.SplashScreen
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.graphics.Color
import androidx.compose.foundation.background
import androidx.compose.foundation.Image
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.layout.ContentScale
@Composable
fun SplashScreen() {
Column(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFF2F6F4E))
.padding(4.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Image(
painter = painterResource(id = R.drawable.logo_kopi),
contentDescription = "Logo Kopi",
modifier = Modifier
.fillMaxWidth()
.height(250.dp),
contentScale = ContentScale.Crop
)
CircularProgressIndicator(
color = Color.White,
strokeWidth = 4.dp
)
}
}

View File

@ -0,0 +1,484 @@
package com.example.uitugasakhir
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.runtime.Composable
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.*
import androidx.compose.ui.res.painterResource
import com.example.app.ui.theme.Inria_Bold
import com.example.app.ui.theme.Inria_Regular
import com.example.app.ui.theme.Inter
import androidx.navigation.NavController
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.ui.platform.LocalContext
import android.content.Context
import android.widget.Toast
import com.android.volley.toolbox.StringRequest
import com.android.volley.toolbox.Volley
import com.android.volley.Request
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.graphics.ColorFilter
import okhttp3.ResponseBody
import retrofit2.Call
import org.json.JSONObject
import retrofit2.Callback
import retrofit2.Response
import com.example.uitugasakhir.api.RetrofitClient
@Composable
fun LoginScreen(navController: NavController) {
var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var passwordVisible by remember { mutableStateOf(false) }
val context = LocalContext.current
fun loginUser(
email: String,
password: String,
context: Context,
navController: NavController
) {
val url = "http://kopikaosmartsystem.my.id/login.php"
val request = object : StringRequest(
Request.Method.POST, url,
{ response ->
try {
val jsonObject = JSONObject(response)
if (jsonObject.getString("status") == "success") {
val nama = jsonObject.getString("nama")
val emailUser = jsonObject.getString("email")
val sharedPreferences =
context.getSharedPreferences("USER_DATA", Context.MODE_PRIVATE)
val editor = sharedPreferences.edit()
editor.putString("nama", nama)
editor.putString("email", emailUser)
editor.apply()
Toast.makeText(context, "Login Berhasil", Toast.LENGTH_SHORT).show()
navController.navigate("dashboard")
} else {
Toast.makeText(
context,
jsonObject.getString("message"),
Toast.LENGTH_SHORT
).show()
}
} catch (e: Exception) {
Toast.makeText(
context,
"Response error: ${e.message}",
Toast.LENGTH_LONG
).show()
}
},
{ error ->
Toast.makeText(
context,
"Error: ${error.message}",
Toast.LENGTH_LONG
).show()
}
) {
override fun getParams(): MutableMap<String, String> {
return hashMapOf(
"email" to email,
"password" to password
)
}
}
val queue = Volley.newRequestQueue(context)
queue.add(request)
}
Box(
modifier = Modifier
.fillMaxSize()
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(160.dp),
contentAlignment = Alignment.TopCenter
) {
// Card hijau di atas
Card(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(0.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF2F6F4E)
)
) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(top = 22.dp),
contentAlignment = Alignment.TopCenter
) {
Text(
text = "Halaman Login",
fontSize = 16.sp,
fontFamily = Inter,
fontWeight = FontWeight.Bold,
color = Color.White
)
}
}
}
Card(
modifier = Modifier
.padding(top = 58.dp)
.fillMaxSize(),
shape = RoundedCornerShape(topStart = 24.dp, topEnd = 0.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFFFFFFF)
)
) {
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(id = R.drawable.kopikakao),
contentDescription = "Logo Kopi",
Modifier.size(200.dp)
)
Text(
text = "Selamat Datang!",
fontSize = 20.sp,
fontFamily = Inria_Bold,
fontWeight = FontWeight.Bold,
color = Color.Black
)
Spacer(modifier = Modifier.height(10.dp))
Text(
text = "Silahkan masukkan email dan password anda",
fontSize = 15.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Medium,
color = Color.Black.copy(alpha = 0.7f)
)
Spacer(modifier = Modifier.height(30.dp))
Box(
modifier = Modifier
.wrapContentHeight()
.padding(bottom = 50.dp)
.padding(horizontal = 40.dp),
contentAlignment = Alignment.TopCenter
) {
Card(
modifier = Modifier
.size(width = 276.dp, height = 259.dp),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF2F6F4E)
)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(20.dp),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally
) {
Card(
modifier = Modifier
.size(width = 235.dp, height = 46.dp),
shape = RoundedCornerShape(25.dp),
colors = CardDefaults.cardColors(
containerColor = Color.White
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxSize()
) {
Spacer(modifier = Modifier.width(10.dp))
Image(
painter = painterResource(id = R.drawable.mail),
contentDescription = "Email",
modifier = Modifier
.size(20.dp)
.padding(start = 5.dp),
colorFilter = ColorFilter.tint(Color.Gray)
)
Spacer(modifier = Modifier.width(10.dp))
BasicTextField(
value = email,
onValueChange = { email = it },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
decorationBox = { innerTextField ->
if (email.isEmpty()) {
Text(
"Email",
color = Color.Gray
)
}
innerTextField()
}
)
}
}
Spacer(modifier = Modifier.height(20.dp))
Card(
modifier = Modifier
.size(width = 235.dp, height = 46.dp),
shape = RoundedCornerShape(25.dp),
colors = CardDefaults.cardColors(
containerColor = Color.White
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxSize()
) {
Spacer(modifier = Modifier.width(10.dp))
Image(
painter = painterResource(id = R.drawable.password),
contentDescription = "Password",
modifier = Modifier
.size(20.dp)
.padding(start = 5.dp),
colorFilter = ColorFilter.tint(Color.Gray)
)
Spacer(modifier = Modifier.width(10.dp))
BasicTextField(
value = password,
onValueChange = { password = it },
modifier = Modifier.weight(1f),
singleLine = true,
// 🔒 Enkripsi tampilan password
visualTransformation = if (passwordVisible)
VisualTransformation.None
else
PasswordVisualTransformation(),
decorationBox = { innerTextField ->
if (password.isEmpty()) {
Text(
"Password",
color = Color.Gray
)
}
innerTextField()
}
)
// 👁️ Icon show/hide password
IconButton(onClick = {
passwordVisible = !passwordVisible
}) {
Icon(
painter = painterResource(
id = if (passwordVisible)
R.drawable.ic_eye_open // mata terbuka
else
R.drawable.ic_eye_closed // mata tertutup
),
contentDescription = "Toggle Password",
modifier = Modifier.size(18.dp),
tint = Color.Gray
)
}
}
}
Spacer(modifier = Modifier.height(20.dp))
Text(
text = "Lupa password?",
fontSize = 14.sp,
fontFamily = Inter,
color = Color.White,
modifier = Modifier
.offset(x = 70.dp)
.clickable {
navController.navigate("forgot_password")
}
)
Spacer(modifier = Modifier.height(20.dp))
Card(
onClick = { loginUser(email, password, context, navController) },
modifier = Modifier
.size(width = 235.dp, height = 46.dp),
shape = RoundedCornerShape(25.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF22C55E)
)
) {
Text(
text = "Masuk",
fontSize = 16.sp,
fontFamily = Inter,
fontWeight = FontWeight.Normal,
color = Color.White,
modifier = Modifier
.padding(top = 13.dp)
.align(Alignment.CenterHorizontally)
)
}
}
}
}
Row {
Text(
text = "Belum mempunyai akun?",
fontSize = 16.sp,
fontFamily = Inter,
fontWeight = FontWeight.Normal,
color = Color.Black.copy(alpha = 0.7f)
)
Spacer(modifier = Modifier.width(5.dp))
Text(
text = "Buat akun",
fontSize = 16.sp,
fontFamily = Inter,
fontWeight = FontWeight.Normal,
color = Color(0xFF22C55E),
modifier = Modifier.clickable {
navController.navigate("register")
}
)
}
}
}
}
}
@Composable
fun ForgotPasswordScreen(navController: NavController) {
var email by remember { mutableStateOf("") }
var message by remember { mutableStateOf("") }
var isLoading by remember { mutableStateOf(false) }
fun sendForgotPassword(email: String, callback: (String) -> Unit) {
RetrofitClient.api.forgotPassword(email.trim())
.enqueue(object : Callback<ResponseBody> {
override fun onResponse(
call: Call<ResponseBody>,
response: Response<ResponseBody>
) {
callback(response.body()?.string() ?: "No response")
}
override fun onFailure(
call: Call<ResponseBody>,
t: Throwable
) {
callback("Error: ${t.message}")
}
})
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(20.dp),
verticalArrangement = Arrangement.Center
) {
Text(
text = "Reset Password",
fontSize = 24.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(20.dp))
TextField(
value = email,
onValueChange = { email = it },
placeholder = { Text("Masukkan email") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(20.dp))
Button(
onClick = {
isLoading = true
sendForgotPassword(email) {
message = it
isLoading = false
}
},
modifier = Modifier.fillMaxWidth()
) {
Text("Kirim Link Reset")
}
Spacer(modifier = Modifier.height(20.dp))
if (isLoading) {
CircularProgressIndicator()
}
Text(message)
}
}

View File

@ -0,0 +1,460 @@
package com.example.uitugasakhir
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.runtime.Composable
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import com.example.app.ui.theme.Inria_Bold
import com.example.app.ui.theme.Inria_Regular
import com.example.app.ui.theme.Inter
import androidx.navigation.NavController
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.graphics.ColorFilter
import android.content.Context
import android.widget.Toast
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import com.android.volley.toolbox.StringRequest
import com.android.volley.toolbox.Volley
import com.android.volley.Request
@Composable
fun RegisterScreen(navController: NavController) {
var nama by remember { mutableStateOf("") }
var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var confirmPassword by remember { mutableStateOf("") }
var passwordVisible by remember { mutableStateOf(false) }
var confirmPasswordVisible by remember { mutableStateOf(false) }
val context = LocalContext.current
fun registerUser(
nama: String,
email: String,
password: String,
confirmPassword: String,
context: Context,
navController: NavController
) {
if (password != confirmPassword) {
Toast.makeText(context, "Password tidak sama", Toast.LENGTH_SHORT).show()
return
}
val url = "http://kopikaosmartsystem.my.id/register.php" // GANTI IP KAMU
val request = object : StringRequest(
Request.Method.POST, url,
{ response ->
if (response.contains("Register berhasil")) {
Toast.makeText(context, "Berhasil daftar", Toast.LENGTH_SHORT).show()
navController.navigate("login")
} else {
Toast.makeText(context, response, Toast.LENGTH_SHORT).show()
}
},
{ error ->
Toast.makeText(context, "Error: ${error.message}", Toast.LENGTH_LONG).show()
}
) {
override fun getParams(): MutableMap<String, String> {
return hashMapOf(
"nama" to nama,
"email" to email,
"password" to password
)
}
}
val queue = Volley.newRequestQueue(context)
queue.add(request)
}
Box(
modifier = Modifier
.fillMaxSize()
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(160.dp),
contentAlignment = Alignment.TopCenter
) {
// Card hijau di atas
Card(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(0.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF2F6F4E)
)
) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(top = 22.dp),
contentAlignment = Alignment.TopCenter
) {
Text(
text = "Halaman Login",
fontSize = 16.sp,
fontFamily = Inter,
fontWeight = FontWeight.Bold,
color = Color.White
)
}
}
}
Card(
modifier = Modifier
.padding(top = 58.dp)
.fillMaxSize(),
shape = RoundedCornerShape(topStart = 24.dp, topEnd = 0.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFFFFFFF)
)
) {
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(id = R.drawable.kopikakao),
contentDescription = "Logo Kopi",
Modifier.size(200.dp)
)
Text(
text = "Selamat Datang!",
fontSize = 20.sp,
fontFamily = Inria_Bold,
fontWeight = FontWeight.Bold,
color = Color.Black
)
Spacer(modifier = Modifier.height(10.dp))
Text(
text = "Silahkan masukkan email dan password anda",
fontSize = 15.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Medium,
color = Color.Black.copy(alpha = 0.7f)
)
Spacer(modifier = Modifier.height(30.dp))
Box(
modifier = Modifier
.wrapContentHeight()
.padding(bottom = 10.dp)
.padding(horizontal = 40.dp),
contentAlignment = Alignment.TopCenter
) {
Card(
modifier = Modifier
.size(width = 276.dp, height = 350.dp),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF2F6F4E)
)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(20.dp),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally
) {
Card(
modifier = Modifier
.size(width = 235.dp, height = 46.dp),
shape = RoundedCornerShape(25.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFFFFFFF)
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxSize()
) {
Spacer(modifier = Modifier.width(10.dp))
Image(
painter = painterResource(id = R.drawable.user),
contentDescription = "Nama",
modifier = Modifier
.size(20.dp)
.padding(start = 5.dp),
colorFilter = ColorFilter.tint(Color.Gray)
)
Spacer(modifier = Modifier.width(10.dp))
BasicTextField(
value = nama,
onValueChange = { nama = it },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
decorationBox = { innerTextField ->
if (nama.isEmpty()) {
Text(
"Nama",
color = Color.Gray
)
}
innerTextField()
}
)
}
}
Spacer(modifier = Modifier.height(20.dp))
Card(
modifier = Modifier
.size(width = 235.dp, height = 46.dp),
shape = RoundedCornerShape(25.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFFFFFFF)
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxSize()
) {
Spacer(modifier = Modifier.width(10.dp))
Image(
painter = painterResource(id = R.drawable.mail),
contentDescription = "Email",
modifier = Modifier
.size(20.dp)
.padding(start = 5.dp),
colorFilter = ColorFilter.tint(Color.Gray)
)
Spacer(modifier = Modifier.width(10.dp))
BasicTextField(
value = email,
onValueChange = { email = it },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
decorationBox = { innerTextField ->
if (email.isEmpty()) {
Text(
"Email",
color = Color.Gray
)
}
innerTextField()
}
)
}
}
Spacer(modifier = Modifier.height(20.dp))
Card(
modifier = Modifier
.size(width = 235.dp, height = 46.dp),
shape = RoundedCornerShape(25.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFFFFFFF)
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxSize()
) {
Spacer(modifier = Modifier.width(10.dp))
Image(
painter = painterResource(id = R.drawable.password),
contentDescription = "Password",
modifier = Modifier
.size(20.dp)
.padding(start = 5.dp),
colorFilter = ColorFilter.tint(Color.Gray)
)
Spacer(modifier = Modifier.width(10.dp))
BasicTextField(
value = password,
onValueChange = { password = it },
modifier = Modifier.weight(1f),
singleLine = true,
// 🔒 Enkripsi tampilan password
visualTransformation = if (passwordVisible)
VisualTransformation.None
else
PasswordVisualTransformation(),
decorationBox = { innerTextField ->
if (password.isEmpty()) {
Text(
"Password",
color = Color.Gray
)
}
innerTextField()
}
)
// 👁️ Icon show/hide password
IconButton(onClick = {
passwordVisible = !passwordVisible
}) {
Icon(
painter = painterResource(
id = if (passwordVisible)
R.drawable.ic_eye_open // mata terbuka
else
R.drawable.ic_eye_closed // mata tertutup
),
contentDescription = "Toggle Password",
modifier = Modifier.size(18.dp),
tint = Color.Gray
)
}
}
}
Spacer(modifier = Modifier.height(20.dp))
Card(
modifier = Modifier
.size(width = 235.dp, height = 46.dp),
shape = RoundedCornerShape(25.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFFFFFFF)
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxSize()
) {
Spacer(modifier = Modifier.width(10.dp))
Image(
painter = painterResource(id = R.drawable.password),
contentDescription = "ConfirmPassword",
modifier = Modifier
.size(20.dp)
.padding(start = 5.dp),
colorFilter = ColorFilter.tint(Color.Gray)
)
Spacer(modifier = Modifier.width(10.dp))
BasicTextField(
value = confirmPassword,
onValueChange = { confirmPassword = it },
modifier = Modifier.weight(1f),
singleLine = true,
// 🔒 Enkripsi tampilan password
visualTransformation = if (confirmPasswordVisible)
VisualTransformation.None
else
PasswordVisualTransformation(),
decorationBox = { innerTextField ->
if (confirmPassword.isEmpty()) {
Text(
"Konfirmasi password",
color = Color.Gray
)
}
innerTextField()
}
)
// 👁️ Icon show/hide password
IconButton(onClick = {
confirmPasswordVisible = !confirmPasswordVisible
}) {
Icon(
painter = painterResource(
id = if (confirmPasswordVisible)
R.drawable.ic_eye_open // mata terbuka
else
R.drawable.ic_eye_closed // mata tertutup
),
contentDescription = "Toggle Password",
modifier = Modifier.size(18.dp),
tint = Color.Gray
)
}
}
}
Spacer(modifier = Modifier.height(20.dp))
Card(
onClick = { registerUser(nama, email, password, confirmPassword, context, navController) },
modifier = Modifier
.size(width = 235.dp, height = 46.dp),
shape = RoundedCornerShape(25.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF22C55E)
)
) {
Text(
text = "Daftar",
fontSize = 16.sp,
fontFamily = Inter,
fontWeight = FontWeight.Normal,
color = Color.White,
modifier = Modifier.padding(top = 13.dp)
.align(Alignment.CenterHorizontally)
)
}
}
}
}
}
}
}
}

View File

@ -0,0 +1,257 @@
package com.example.uitugasakhir
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import com.example.app.ui.theme.Inter
import androidx.compose.foundation.layout.fillMaxHeight
import com.example.app.ui.theme.Inria_Regular
@Composable
fun PanduanScreen(navController: NavController) {
Box(modifier = Modifier.fillMaxSize()) {
Card(
modifier = Modifier
.fillMaxWidth()
.height(160.dp),
shape = RoundedCornerShape(0.dp),
colors = CardDefaults.cardColors(Color(0xFF2F6F4E))
) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(top = 22.dp),
contentAlignment = Alignment.TopCenter
) {
Image(
painter = painterResource(id = R.drawable.back),
contentDescription = "kembali",
modifier = Modifier
.size(15.dp)
.offset(x = -160.dp, y = 1.dp)
.clickable { navController.popBackStack() }
)
Text(
text = stringResource(R.string.guide),
fontSize = 16.sp,
fontFamily = Inter,
fontWeight = FontWeight.Bold,
color = Color.White
)
}
}
// CONTENT
Card(
modifier = Modifier
.padding(top = 58.dp)
.fillMaxSize(),
shape = RoundedCornerShape(topStart = 24.dp),
colors = CardDefaults.cardColors(Color.White)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
.verticalScroll(rememberScrollState())
.animateContentSize()
) {
Text(
text = stringResource(R.string.first_step),
fontSize = 15.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Medium,
color = Color.Gray,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 28.dp)
)
Spacer(modifier = Modifier.height(15.dp))
Card(
modifier = Modifier
.padding(horizontal = 25.dp)
.size(width = 333.dp, height = 100.dp),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFA7C7E7)
),
elevation = CardDefaults.cardElevation(4.dp)
){
}
Spacer(modifier = Modifier.height(15.dp))
Text(
text = stringResource(R.string.main_features),
fontSize = 15.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Medium,
color = Color.Gray,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 28.dp)
)
Spacer(modifier = Modifier.height(15.dp))
Card(
modifier = Modifier
.padding(horizontal = 25.dp)
.size(width = 333.dp, height = 100.dp),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFB7E4C7)
),
elevation = CardDefaults.cardElevation(4.dp)
){
}
Spacer(modifier = Modifier.height(15.dp))
Text(
text = stringResource(R.string.automatic_watering),
fontSize = 15.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Medium,
color = Color.Gray,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 28.dp)
)
Spacer(modifier = Modifier.height(15.dp))
Card(
modifier = Modifier
.padding(horizontal = 25.dp)
.size(width = 333.dp, height = 100.dp),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFBDE0FE)
),
elevation = CardDefaults.cardElevation(4.dp)
){
}
Spacer(modifier = Modifier.height(15.dp))
Text(
text = stringResource(R.string.manual_control),
fontSize = 15.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Medium,
color = Color.Gray,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 28.dp)
)
Spacer(modifier = Modifier.height(15.dp))
Card(
modifier = Modifier
.padding(horizontal = 25.dp)
.size(width = 333.dp, height = 100.dp),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFA3B18A)
),
elevation = CardDefaults.cardElevation(4.dp)
){
}
Spacer(modifier = Modifier.height(15.dp))
Text(
text = stringResource(R.string.notifications),
fontSize = 15.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Medium,
color = Color.Gray,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 28.dp)
)
Spacer(modifier = Modifier.height(15.dp))
Card(
modifier = Modifier
.padding(horizontal = 25.dp)
.size(width = 333.dp, height = 100.dp),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFFFD6A5)
),
elevation = CardDefaults.cardElevation(4.dp)
){
}
Spacer(modifier = Modifier.height(15.dp))
Text(
text = stringResource(R.string.history_analytics),
fontSize = 15.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Medium,
color = Color.Gray,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 28.dp)
)
Spacer(modifier = Modifier.height(15.dp))
Card(
modifier = Modifier
.padding(horizontal = 25.dp)
.size(width = 333.dp, height = 100.dp),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFFFC8DD)
),
elevation = CardDefaults.cardElevation(4.dp)
){
}
}
}
}
}

View File

@ -0,0 +1,75 @@
package com.example.uitugasakhir.aksi
import android.content.Context
import android.util.Log
import android.widget.Toast
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import com.android.volley.Request
import com.android.volley.toolbox.StringRequest
import com.android.volley.toolbox.Volley
@Composable
fun DeleteDialog(
showDialog: Boolean,
selectedId: String,
context: Context,
onDismiss: () -> Unit,
onSuccess: () -> Unit
) {
if (!showDialog) return
AlertDialog(
onDismissRequest = { onDismiss() },
title = {
Text("Konfirmasi")
},
text = {
Text("Yakin ingin menghapus jadwal ini?")
},
confirmButton = {
TextButton(
onClick = {
Log.d("DELETE", "HAPUS ID: $selectedId")
val request = object : StringRequest(
Request.Method.POST,
"http://kopikaosmartsystem.my.id/hapus_jadwal.php",
{ response ->
Log.d("API", "Response: $response")
Toast.makeText(context, "Berhasil dihapus", Toast.LENGTH_SHORT).show()
onDismiss()
onSuccess() // refresh data
},
{ error ->
Log.e("API", error.toString())
Toast.makeText(context, "Gagal hapus", Toast.LENGTH_SHORT).show()
}
) {
override fun getParams(): MutableMap<String, String> {
return hashMapOf("id" to selectedId)
}
}
Volley.newRequestQueue(context).add(request)
}
) {
Text("OK")
}
},
dismissButton = {
TextButton(
onClick = { onDismiss() }
) {
Text("Batal")
}
}
)
}

View File

@ -0,0 +1,149 @@
package com.example.uitugasakhir.api
import android.content.Context
import android.util.Log
import com.android.volley.Request
import com.android.volley.toolbox.StringRequest
import com.android.volley.toolbox.Volley
import com.example.uitugasakhir.model.AmbangBatas
import org.json.JSONObject
object ApiAmbangBatas {
// 🔥 GET DATA
fun getAmbangBatas(
context: Context,
onSuccess: (AmbangBatas) -> Unit,
onError: (String) -> Unit
) {
val url =
"http://kopikaosmartsystem.my.id/get_ambang_batas.php"
val request = StringRequest(
Request.Method.GET,
url,
{ response ->
try {
Log.d("API_GET_RAW", response)
val json = JSONObject(response)
if (json.getString("status") == "success") {
val data = AmbangBatas(
min_tanah = json.getInt("min_tanah"),
min_suhu = json.getInt("min_suhu"),
min_udara = json.getInt("min_udara"),
max_tanah = json.getInt("max_tanah"),
max_suhu = json.getInt("max_suhu"),
max_udara = json.getInt("max_udara"),
critical_tanah = json.getInt("critical_tanah")
)
onSuccess(data)
} else {
onError("Data tidak tersedia")
}
} catch (e: Exception) {
Log.e("JSON_ERROR_GET", response)
onError("Response bukan JSON (cek server/API error)")
}
},
{ error ->
Log.e("GET_ERROR", error.toString())
error.networkResponse?.let {
Log.e(
"GET_CODE",
it.statusCode.toString()
)
Log.e(
"GET_BODY",
String(it.data)
)
}
onError(error.toString())
}
)
Volley.newRequestQueue(context).add(request)
}
// 🔥 SAVE / UPDATE DATA
fun saveAmbangBatas(
context: Context,
minTanah: Int,
minSuhu: Int,
minUdara: Int,
maxTanah: Int,
maxSuhu: Int,
maxUdara: Int,
criticalTanah: Int,
onSuccess: (String) -> Unit,
onError: (String) -> Unit
) {
val url = "http://kopikaosmartsystem.my.id/save_ambang_batas.php"
val request = object : StringRequest(
Method.POST,
url,
{ response ->
try {
Log.d("API_SAVE_RAW", response)
val json = JSONObject(response)
onSuccess(json.getString("message"))
} catch (e: Exception) {
Log.e("JSON_ERROR_SAVE", response)
onError("Response bukan JSON (cek server/API error)")
}
},
{ error ->
Log.e("SAVE_ERROR", error.toString())
error.networkResponse?.let {
Log.e(
"SAVE_CODE",
it.statusCode.toString()
)
Log.e(
"SAVE_BODY",
String(it.data)
)
}
onError(error.toString())
}
) {
override fun getParams(): MutableMap<String, String> {
return hashMapOf(
"min_tanah" to minTanah.toString(),
"min_suhu" to minSuhu.toString(),
"min_udara" to minUdara.toString(),
"max_tanah" to maxTanah.toString(),
"max_suhu" to maxSuhu.toString(),
"max_udara" to maxUdara.toString(),
"critical_tanah" to criticalTanah.toString()
)
}
}
Volley.newRequestQueue(context).add(request)
}
}

View File

@ -0,0 +1,36 @@
package com.example.uitugasakhir.api
import android.content.Context
import com.android.volley.Request
import com.android.volley.toolbox.StringRequest
import com.android.volley.toolbox.Volley
object ApiDurasi {
fun saveDurasi(
context: Context,
durasi: Int
) {
val url =
"http://192.168.0.105/save_duration.php"
val request = object : StringRequest(
Method.POST,
url,
{},
{}
) {
override fun getParams(): MutableMap<String, String> {
return hashMapOf(
"durasi" to durasi.toString()
)
}
}
Volley.newRequestQueue(context)
.add(request)
}
}

View File

@ -0,0 +1,164 @@
package com.example.uitugasakhir.api
import com.example.uitugasakhir.model.SensorResponse
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.Response
import retrofit2.http.*
import com.example.uitugasakhir.model.RiwayatItem
import com.example.uitugasakhir.model.Sensor
import com.example.uitugasakhir.model.SettingResponse
import com.example.uitugasakhir.model.AmbangBatas
// jadwal
import com.example.uitugasakhir.model.JadwalItem
import com.example.uitugasakhir.model.ResponseMessage
// Jeda
import com.example.uitugasakhir.model.UnreadResponse
import com.example.uitugasakhir.model.JedaResponse
import com.example.uitugasakhir.model.WarningItem
import com.example.uitugasakhir.model.ApiResponse
import com.example.uitugasakhir.model.WarningResponse
interface ApiService {
@FormUrlEncoded
@POST("forgot_password.php")
fun forgotPassword(
@Field("email") email: String
): Call<ResponseBody>
@GET("get_setting.php")
suspend fun getSetting(
@Query("user_id") userId: Int
): Response<SettingResponse>
@FormUrlEncoded
@POST("save_setting.php")
suspend fun saveSetting(
@Field("user_id") userId: Int,
@Field("kelembapan_tanah") tanah: Int,
@Field("suhu_udara") suhu: Int,
@Field("kelembapan_udara") udara: Int
): Response<SettingResponse>
@GET("config.php")
suspend fun setConfig(
@Query("jadwal") jadwal: String,
@Query("ambang") ambang: Int,
@Query("jeda") jeda: Int
): String
@GET("get_riwayat.php")
suspend fun getRiwayat(
@Query("user_id") userId: Int
): List<RiwayatItem>
@FormUrlEncoded
@POST("save_riwayat.php")
suspend fun saveRiwayat(
@Field("aksi") aksi: String,
@Field("status") status: String,
@Field("keterangan") keterangan: String
): Response<ApiResponse>
@GET("get_sensor.php")
suspend fun getSensor(): SensorResponse
@GET("setting_notifikasi.php")
suspend fun getSettingFromApi(): SettingResponse
@GET("get_ambang_batas.php")
suspend fun getAmbangBatas(): AmbangBatas
@FormUrlEncoded
@POST("save_log.php")
suspend fun saveLog(
@Field("jenis") jenis: String,
@Field("kategori") kategori: String,
@Field("pesan") pesan: String,
@Field("level") level: String,
@Field("nilai_sensor") nilai: String?,
@Field("min_batas") min: String?,
@Field("max_batas") max: String?
): okhttp3.ResponseBody
@FormUrlEncoded
@POST("delete_log.php")
suspend fun deleteLog(
@Field("id") id: Int
): Response<ResponseBody>
@GET("get_log.php")
suspend fun getLogs(): List<WarningItem>
// jadwal
@GET("get_jadwal.php")
suspend fun getJadwal(): List<JadwalItem>
// TAMBAH jadwal
@FormUrlEncoded
@POST("tambah_jadwal.php")
suspend fun tambahJadwal(
@Field("waktu") waktu: String,
@Field("status_waktu") status: String
): ResponseMessage
// HAPUS jadwal
@FormUrlEncoded
@POST("delete_jadwal.php")
suspend fun deleteJadwal(
@Field("id") id: Int
): ResponseMessage
// Jeda
// GET jeda
@GET("get_jeda.php")
suspend fun getJeda(): Response<JedaResponse>
// UPDATE jeda
@FormUrlEncoded
@POST("set_jeda.php")
suspend fun setJeda(
@Field("jeda") jeda: String
): Response<JedaResponse>
// setting pompa
@FormUrlEncoded
@POST("set_pump.php")
suspend fun setPump(
@Field("status") status: String,
@Field("mode") mode: String
): Response<ApiResponse>
@GET("get_unread_count.php")
suspend fun getUnreadCount(): UnreadResponse
@FormUrlEncoded
@POST("read_notification.php")
suspend fun readNotification(
@Field("id") id: Int
): ResponseBody
@POST("read_all_notifications.php")
suspend fun readAllNotifications(): ResponseBody
@GET("get_warning.php")
suspend fun getWarning(): WarningResponse
@GET("acknowledge_warning.php")
suspend fun acknowledgeWarning(): Response<Unit>
@FormUrlEncoded
@POST("insert_warning.php")
suspend fun insertWarning(
@Field("pesan") pesan: String
): Response<Unit>
}

View File

@ -0,0 +1,17 @@
package com.example.uitugasakhir.api
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
object RetrofitClient {
private const val BASE_URL = "http://kopikaosmartsystem.my.id/"
val api: ApiService by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiService::class.java)
}
}

View File

@ -0,0 +1,193 @@
package com.example.uitugasakhir
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.*
import kotlinx.coroutines.delay
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.compose.animation.*
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.uitugasakhir.Page.NotifikasiScreen
import com.example.uitugasakhir.ViewModel.MainViewModel
import android.content.Intent
import androidx.compose.ui.platform.LocalContext
import com.example.uitugasakhir.ViewModel.SensorViewModel
import android.os.Build
import androidx.activity.result.contract.ActivityResultContracts
import com.example.uitugasakhir.model.AlertEvent
import android.util.Log
class MainActivity : ComponentActivity() {
private var pendingRoute: String? = null
private val requestPermissionLauncher =
registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
startSensorService()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
requestPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS)
} else {
startSensorService()
}
handleIntent(intent)
setContent {
val mainVm: MainViewModel = viewModel()
AppNavigation(
routeFromNotif = pendingRoute,
mainVm = mainVm
)
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleIntent(intent)
}
private fun handleIntent(intent: Intent) {
pendingRoute = intent.getStringExtra("route")
}
private fun startSensorService() {
val serviceIntent = Intent(this, SensorMonitoringService::class.java)
}
}
@Composable
fun AppNavigation(
routeFromNotif: String?,
mainVm: MainViewModel
) {
val context = LocalContext.current
val sensorVm: SensorViewModel = viewModel()
LaunchedEffect(Unit) {
sensorVm.loadUnreadCount()
}
var showSplash by remember { mutableStateOf(!AppSession.isSplashShown) }
val navController = rememberNavController()
var navReady by remember { mutableStateOf(false) }
val alert = mainVm.alertEvent
var showPopupState = remember { mutableStateOf<AlertEvent?>(null) }
LaunchedEffect(Unit) {
mainVm.alertEvent.collect { event ->
Log.d("ALERT_FLOW", "UI RECEIVED ALERT → ${event.title}")
SensorNotification.showNotification(
context,
event.title,
event.message
)
}
}
LaunchedEffect(Unit) {
delay(3000)
showSplash = false
AppSession.isSplashShown = true
}
if (showSplash) {
SplashScreen()
return
}
LaunchedEffect(Unit) {
navReady = true
}
LaunchedEffect(routeFromNotif, navReady) {
if (navReady) {
routeFromNotif?.let {
navController.navigate(it) {
launchSingleTop = true
}
}
}
}
NavHost(
navController = navController,
startDestination = "dashboard"
) {
composable("manual") {
ManualScreen(
navController = navController,
sensorVm = sensorVm,
mainVm = mainVm
)
}
composable("notifikasi") {
NotifikasiScreen(
navController = navController,
sensorVm = sensorVm
)
}
composable("dashboard") {
DashboardScreen(
navController = navController,
sensorVm = sensorVm,
mainVm = mainVm
)
}
composable("pengaturan") {
PengaturanScreen(navController)
}
composable("riwayat") {
RiwayatScreen(navController)
}
composable("login") {
LoginScreen(navController)
}
composable("register") {
RegisterScreen(navController)
}
composable("forgot_password") {
ForgotPasswordScreen(navController)
}
composable("panduan") {
PanduanScreen(navController)
}
}
}

View File

@ -0,0 +1,6 @@
package com.example.uitugasakhir.model
data class AlertEvent(
val title: String,
val message: String
)

View File

@ -0,0 +1,5 @@
package com.example.uitugasakhir
object AppSession {
var isSplashShown = false
}

View File

@ -0,0 +1,11 @@
package com.example.uitugasakhir.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)

View File

@ -0,0 +1,28 @@
package com.example.app.ui.theme
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import com.example.uitugasakhir.R
val Inter = FontFamily(
Font(R.font.inter_medium, FontWeight.Medium)
)
val Inria_Regular = FontFamily(
Font(R.font.inria_regular, FontWeight.Normal)
)
val Inria_Bold = FontFamily(
Font(R.font.inriasans_bold, FontWeight.Bold)
)
val Poppins_Bold = FontFamily(
Font(R.font.poppins_bold, FontWeight.Bold)
)
val Poppins_Regular = FontFamily(
Font(R.font.poppins_regular, FontWeight.Bold)
)
val Poppins_Medium = FontFamily(
Font(R.font.poppins_medium, FontWeight.Bold)
)
val Poppins_Black = FontFamily(
Font(R.font.poppins_black, FontWeight.Bold)
)

View File

@ -0,0 +1,58 @@
package com.example.uitugasakhir.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun UITugasAkhirTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}

View File

@ -0,0 +1,34 @@
package com.example.uitugasakhir.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)

View File

@ -0,0 +1,93 @@
package com.example.uitugasakhir.ui.komponen
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.layout.Row
import androidx.compose.ui.Modifier
@Composable
fun WarningPopup(
show: Boolean,
message: String,
onDismiss: () -> Unit,
disablePopup1Minute: Boolean,
onDisableChange: (Boolean) -> Unit
) {
if (show) {
AlertDialog(
containerColor = Color(0xFF222831),
onDismissRequest = onDismiss,
title = {
Text(
"🚨 PERINGATAN SISTEM",
color = Color.Red,
fontWeight = FontWeight.Bold
)
},
text = {
Column {
Text(
text = message,
color = Color.White
)
Spacer(
modifier = Modifier.height(12.dp)
)
Row(
verticalAlignment =
Alignment.CenterVertically
) {
Checkbox(
checked = disablePopup1Minute,
onCheckedChange =
onDisableChange
)
Text(
text =
"Nonaktifkan selama 1 menit",
color = Color.White
)
}
}
},
confirmButton = {
TextButton(
onClick = onDismiss
) {
Text(
"OK",
color = Color.Green
)
}
}
)
}
}

View File

@ -0,0 +1,55 @@
package com.example.uitugasakhir.komponen
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.rememberNavController
import com.example.uitugasakhir.ManualScreen
import com.example.uitugasakhir.Page.NotifikasiScreen
import com.example.uitugasakhir.ViewModel.MainViewModel
import com.example.uitugasakhir.ViewModel.SensorViewModel
import viewmodel.NotificationViewModel
import okhttp3.*
class WebSocketClient(
private val notifVm: NotificationViewModel
) {
private val client = OkHttpClient()
private var webSocket: WebSocket? = null
fun connect() {
val request = Request.Builder()
.url("ws://server-kamu/ws") // ganti nanti
.build()
val listener = object : WebSocketListener() {
override fun onMessage(webSocket: WebSocket, text: String) {
notifVm.updateFromSocket(text)
}
override fun onFailure(
webSocket: WebSocket,
t: Throwable,
response: Response?
) {
t.printStackTrace()
}
}
webSocket = client.newWebSocket(request, listener)
}
fun disconnect() {
webSocket?.close(1000, "close")
}
}

View File

@ -0,0 +1,15 @@
package com.example.uitugasakhir.model
data class MQTTState(
val tanah: Int = 0,
val suhu: Int = 0,
val udara: Int = 0,
val hujan: Int = 0,
val pump: String = "OFF",
val status: String = "Standby",
val alarm: String = "AMAN"
)

View File

@ -0,0 +1,16 @@
package com.example.uitugasakhir.ViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import androidx.compose.runtime.*
import com.example.uitugasakhir.api.RetrofitClient
import com.example.uitugasakhir.model.Sensor
class ManualViewModel : ViewModel() {
var data by mutableStateOf<Sensor?>(null)
var isLoading by mutableStateOf(false)
}

View File

@ -0,0 +1,162 @@
package com.example.uitugasakhir.model
import com.google.gson.annotations.SerializedName
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.*
import androidx.room.Database
import androidx.room.RoomDatabase
import android.content.Context
import androidx.room.Room
object DatabaseInstance {
fun get(context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"app_db"
).build()
}
}
@Entity(tableName = "riwayat")
data class RiwayatEntity(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val waktu: String,
val aksi: String,
val keterangan: String,
val jeda: Int,
val status: String,
val mode: String
)
@Dao
interface RiwayatDao {
@Query("SELECT * FROM riwayat ORDER BY id DESC")
suspend fun getAll(): List<RiwayatEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(list: List<RiwayatEntity>)
@Insert
suspend fun insert(item: RiwayatEntity)
@Query("DELETE FROM riwayat")
suspend fun clear()
}
@Database(
entities = [RiwayatEntity::class],
version = 1
)
abstract class AppDatabase : RoomDatabase() {
abstract fun riwayatDao(): RiwayatDao
}
data class RiwayatItem(
val id: Int,
val waktu: String,
val aksi: String,
val keterangan: String,
val status: String,
val jeda: Int,
val mode: String
)
data class Sensor(
@SerializedName("kelembapan_udara")
val kelembaban_udara: Float,
@SerializedName("suhu_udara")
val suhu_udara: Float,
@SerializedName("kelembapan_tanah")
val kelembaban_tanah: Float,
@SerializedName("intensitas_hujan")
val intensitas_hujan: Float,
)
// jadwal
data class JadwalItem(
val id: String,
val waktu: String,
val status: String
)
data class ResponseMessage(
val success: Boolean,
val message: String
)
data class SettingResponse(
val status: String,
val message: String? = null,
val kelembapan_tanah: Int = 0,
val suhu_udara: Int = 0,
val kelembapan_udara: Int = 0
)
data class Setting(
val kelembapan_tanah: Int,
val suhu_udara: Int,
val kelembapan_udara: Int
)
data class AmbangBatas(
val min_tanah: Int,
val min_suhu: Int,
val min_udara: Int,
val max_tanah: Int,
val max_suhu: Int,
val max_udara: Int,
val critical_tanah: Int
)
// buzzer + led dan ui sinkron
data class WarningResponse(
val id: Int,
val status_warning: Int,
val status_buzzer: Int,
val status_led: Int,
val acknowledged: Int,
val pesan: String
)
//notifikasi
data class WarningItem(
val id: Int,
val status: String,
val message: String,
val time: Long,
val mode: String
)
data class JedaResponse(
val success: Boolean,
val message: String?,
val data: JedaData?
)
data class JedaData(
val id: String? = null,
val jeda: String? = null
)
// set pompa
data class ApiResponse(
val status: String,
val pump: String? = null,
val message: String? = null
)
data class UnreadResponse(
val status: String,
val total: Int
)

View File

@ -0,0 +1,34 @@
package com.adit.smartplant.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.uitugasakhir.model.RiwayatItem
@Composable
fun RiwayatCard(
riwayatList: List<RiwayatItem>
) {
Card(
modifier = Modifier
.padding(horizontal = 25.dp)
.size(width = 333.dp, height = 214.dp)
) {
LazyColumn {
items(riwayatList) { item ->
Text(
text = "${item.waktu} ${item.aksi}",
fontSize = 14.sp
)
}
}
}
}

View File

@ -0,0 +1,26 @@
package com.example.uitugasakhir.ViewModel
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.uitugasakhir.model.RiwayatItem
import kotlinx.coroutines.launch
import com.example.uitugasakhir.api.RetrofitClient
class RiwayatViewModel : ViewModel() {
var riwayatList by mutableStateOf<List<RiwayatItem>>(emptyList())
private set
fun loadRiwayat(userId: Int) {
viewModelScope.launch {
try {
riwayatList = RetrofitClient.api.getRiwayat(userId)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}

View File

@ -0,0 +1,8 @@
package com.example.uitugasakhir.model
data class SensorResponse(
val kelembaban_udara: Int,
val suhu_udara: Int,
val kelembaban_tanah: Int,
val intensitas_hujan: Int
)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,245 @@
package com.example.uitugasakhir.mqtt
import android.content.Context
import android.util.Log
import info.mqtt.android.service.MqttAndroidClient
import org.eclipse.paho.client.mqttv3.*
import javax.net.ssl.SSLSocketFactory
class MqttManager(private val context: Context) {
// =====================================================
// MQTT CONFIG
// =====================================================
private val serverUri =
"ssl://9feefbc57c4f41f6a4fde7c48e2a35ad.s1.eu.hivemq.cloud:8883"
private val username = "mqttuser2"
private val password = "Mqtt12345"
private val clientId =
"ANDROID_${System.currentTimeMillis()}"
private val client =
MqttAndroidClient(context, serverUri, clientId)
// =====================================================
// CONNECT
// =====================================================
fun connect(
onMessage: (topic: String, message: String) -> Unit,
onConnected: (() -> Unit)? = null
) {
val options = MqttConnectOptions().apply {
userName = username
password = this@MqttManager.password.toCharArray()
isAutomaticReconnect = true
isCleanSession = false
connectionTimeout = 10
keepAliveInterval = 30
socketFactory =
SSLSocketFactory.getDefault()
}
client.setCallback(object : MqttCallback {
// =================================================
// MESSAGE
// =================================================
override fun messageArrived(
topic: String?,
message: MqttMessage?
) {
val msg = message.toString()
Log.d("MQTT_TOPIC", topic ?: "")
Log.d("MQTT_MESSAGE", msg)
onMessage(topic ?: "", msg)
}
// =================================================
// LOST
// =================================================
override fun connectionLost(cause: Throwable?) {
Log.e(
"MQTT",
"CONNECTION LOST",
cause
)
}
override fun deliveryComplete(token: IMqttDeliveryToken?) {}
})
// =====================================================
// CONNECT
// =====================================================
client.connect(
options,
null,
object : IMqttActionListener {
override fun onSuccess(asyncActionToken: IMqttToken?) {
Log.d("MQTT", "CONNECTED")
subscribeTopics()
onConnected?.invoke()
}
override fun onFailure(
asyncActionToken: IMqttToken?,
exception: Throwable?
) {
Log.e(
"MQTT",
"CONNECT FAILED",
exception
)
}
}
)
}
// =====================================================
// SUBSCRIBE
// =====================================================
private fun subscribeTopics() {
val topics = arrayOf(
"smartplant/alarm",
"smartplant/pump",
"smartplant/sensor",
"smartplant/mode",
"smartplant/manualPump",
"smartplant/riwayat"
)
topics.forEach { topic ->
client.subscribe(
topic,
1,
null,
object : IMqttActionListener {
override fun onSuccess(asyncActionToken: IMqttToken?) {
Log.d(
"MQTT_SUB",
"SUCCESS -> $topic"
)
}
override fun onFailure(
asyncActionToken: IMqttToken?,
exception: Throwable?
) {
Log.e(
"MQTT_SUB",
"FAILED -> $topic",
exception
)
}
}
)
}
}
// =====================================================
// PUBLISH
// =====================================================
fun publish(
topic: String,
payload: String
) {
try {
val message =
MqttMessage(payload.toByteArray())
message.qos = 1
message.isRetained = false
client.publish(topic, message)
Log.d(
"MQTT_PUBLISH",
"$topic -> $payload"
)
} catch (e: Exception) {
Log.e(
"MQTT",
"PUBLISH ERROR",
e
)
}
}
// =====================================================
// MODE
// =====================================================
fun setMode(mode: String) {
publish(
"smartplant/mode",
mode
)
}
// =====================================================
// MANUAL PUMP
// =====================================================
fun setPump(state: Boolean) {
val payload = if (state) "ON" else "OFF"
publish(
"smartplant/manualPump",
payload
)
}
fun setModeAuto() {
publish("smartplant/mode", "Otomatis")
}
fun setModeManual() {
publish("smartplant/mode", "Manual")
}
// =====================================================
// DISCONNECT
// =====================================================
fun disconnect() {
try {
client.unregisterResources()
client.close()
} catch (e: Exception) {
e.printStackTrace()
}
}
}

View File

View File

@ -0,0 +1,333 @@
package com.example.uitugasakhir.pengaturan
import android.content.Context
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.app.ui.theme.Inria_Regular
import androidx.compose.ui.text.TextStyle
import com.android.volley.Request
import com.android.volley.toolbox.StringRequest
import com.android.volley.toolbox.Volley
import android.widget.Toast
import androidx.compose.foundation.text.BasicTextField
import com.example.app.ui.theme.Inria_Bold
import androidx.compose.ui.res.stringResource
import com.example.uitugasakhir.R
import com.example.uitugasakhir.ViewModel.MainViewModel
@Composable
fun AkunCardSection(
viewModel: MainViewModel
) {
val context = LocalContext.current
// SharedPreferences
val sharedPreferences =
context.getSharedPreferences("USER_DATA", Context.MODE_PRIVATE)
var nama by remember {
mutableStateOf(
sharedPreferences.getString("nama", "Nama User") ?: ""
)
}
var email by remember {
mutableStateOf(
sharedPreferences.getString("email", "email@gmail.com") ?: ""
)
}
// Popup dialog
var showDialog by remember { mutableStateOf(false) }
// Input edit
var editNama by remember { mutableStateOf(nama) }
var editEmail by remember { mutableStateOf(email) }
Text(
text = stringResource(R.string.account),
fontSize = 15.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Medium,
color = Color.Gray,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 28.dp)
)
Spacer(modifier = Modifier.height(15.dp))
Card(
modifier = Modifier
.padding(horizontal = 25.dp)
.size(width = 333.dp, height = 100.dp),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFEDEDED)
),
elevation = CardDefaults.cardElevation(4.dp)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
// Nama + tombol ubah
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = nama,
fontWeight = FontWeight.Bold
)
Text(
text = stringResource(R.string.change),
color = Color(0xFF2F6F4E),
fontSize = 13.sp,
modifier = Modifier.clickable {
editNama = nama
editEmail = email
showDialog = true
}
)
}
Spacer(modifier = Modifier.height(13.dp))
HorizontalDivider(
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(13.dp))
Text(
text = email,
fontSize = 12.sp,
color = Color.Gray
)
}
}
// POPUP EDIT
if (showDialog) {
AlertDialog(
onDismissRequest = {
showDialog = false
},
title = {
Text("Edit Akun",
fontSize = 18.sp,
fontFamily = Inria_Bold,
fontWeight = FontWeight.Medium,
color = Color.Black,
)
},
text = {
Column {
Text(
"Ubah Nama",
fontSize = 15.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Medium,
color = Color.Gray,
)
Spacer(modifier = Modifier.height(5.dp))
Card(
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF2F6F4E)
),
elevation = CardDefaults.cardElevation(4.dp)
) {
BasicTextField(
value = editNama,
onValueChange = {
editNama = it
},
textStyle = TextStyle(
color = Color.White.copy(alpha = 0.5f),
fontSize = 16.sp
),
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
decorationBox = { innerTextField ->
if (editNama.isEmpty()) {
Text(
"Nama",
color = Color.Gray
)
}
innerTextField()
}
)
}
Spacer(modifier = Modifier.height(12.dp))
Text(
"Ubah Email",
fontSize = 15.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Medium,
color = Color.Gray,
)
Spacer(modifier = Modifier.height(5.dp))
Card(
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF2F6F4E)
),
elevation = CardDefaults.cardElevation(4.dp),
) {
BasicTextField(
value = editEmail,
onValueChange = {
editEmail = it
},
textStyle = TextStyle(
color = Color.White.copy(alpha = 0.5f),
fontSize = 16.sp
),
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
decorationBox = { innerTextField ->
if (editEmail.isEmpty()) {
Text(
"Email",
color = Color.Gray
)
}
innerTextField()
}
)
}
}
},
confirmButton = {
TextButton(
onClick = {
val url = "http://kopikaosmartsystem.my.id/update_profile.php"
val request = object : StringRequest(
Request.Method.POST,
url,
{ response ->
Toast.makeText(
context,
response,
Toast.LENGTH_SHORT
).show()
if (response.contains("berhasil")) {
// Simpan SharedPreferences
val editor = sharedPreferences.edit()
editor.putString("nama", editNama)
editor.putString("email", editEmail)
editor.apply()
// Update tampilan
nama = editNama
email = editEmail
showDialog = false
}
},
{ error ->
android.widget.Toast.makeText(
context,
"Error: ${error.message}",
android.widget.Toast.LENGTH_SHORT
).show()
}
) {
override fun getParams(): MutableMap<String, String> {
return hashMapOf(
// email lama untuk mencari akun
"email_lama" to email,
// data baru
"nama_baru" to editNama,
"email_baru" to editEmail
)
}
}
val queue = Volley.newRequestQueue(context)
queue.add(request)
}
) {
Text("Simpan")
}
},
dismissButton = {
TextButton(
onClick = {
showDialog = false
}
) {
Text("Batal")
}
}
)
}
}

View File

@ -0,0 +1,110 @@
package com.example.uitugasakhir.pengaturan
import android.widget.Toast
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.app.ui.theme.Inria_Regular
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.HorizontalDivider
import androidx.compose.ui.res.stringResource
import com.example.uitugasakhir.R
import com.example.uitugasakhir.ViewModel.MainViewModel
@Composable
fun BahasaCardSection(
viewModel: MainViewModel
) {
var bahasa by remember { mutableStateOf("Indonesia") }
val context = LocalContext.current
Text(
text = stringResource(R.string.language),
fontSize = 15.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Medium,
color = Color.Gray,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 28.dp)
)
Spacer(modifier = Modifier.height(15.dp))
Card(
modifier = Modifier
.padding(horizontal = 25.dp)
.size(width = 333.dp, height = 110.dp),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFEDEDED)
)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
(stringResource(R.string.indonesia)),
modifier = Modifier.clickable {
bahasa = "Indonesia"
viewModel.setLocale(context, "id")
Toast.makeText(
context,
"Bahasa Indonesia dipilih",
Toast.LENGTH_SHORT
).show()
}
)
Spacer(modifier = Modifier.height(13.dp))
HorizontalDivider(
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(13.dp))
Text(
stringResource(R.string.english),
modifier = Modifier.clickable {
bahasa = "English"
viewModel.setLocale(context, "en")
Toast.makeText(
context,
"English selected",
Toast.LENGTH_SHORT
).show()
}
)
}
}
}

View File

@ -0,0 +1,90 @@
package com.example.uitugasakhir.pengaturan
import android.widget.Toast
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.app.ui.theme.Inria_Regular
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.HorizontalDivider
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavController
import androidx.navigation.Navigation
import com.example.uitugasakhir.R
import com.example.uitugasakhir.ViewModel.MainViewModel
@Composable
fun BantuanCardSection(
navController: NavController,
viewModel: MainViewModel
) {
val context = LocalContext.current
Text(
text = stringResource(R.string.help_support),
fontSize = 15.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Medium,
color = Color.Gray,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 28.dp)
)
Spacer(modifier = Modifier.height(15.dp))
Card(
modifier = Modifier
.padding(horizontal = 25.dp)
.size(width = 333.dp, height = 100.dp),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFEDEDED)
)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = stringResource(R.string.guide),
modifier = Modifier.clickable {
navController.navigate("panduan")
}
)
Spacer(modifier = Modifier.height(13.dp))
HorizontalDivider(
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(13.dp))
Text(
text = stringResource(R.string.contact_developer),
modifier = Modifier.clickable {
Toast.makeText(context, "Hubungi developer", Toast.LENGTH_SHORT).show()
}
)
}
}
}

View File

@ -0,0 +1,134 @@
package com.example.uitugasakhir.pengaturan
import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import com.example.uitugasakhir.ViewModel.SensorViewModel
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.app.ui.theme.Inria_Regular
import com.example.uitugasakhir.R
import com.example.uitugasakhir.ViewModel.MainViewModel
@Composable
fun NotifikasiCardSection(
viewModel: MainViewModel,
sensorViewModel: SensorViewModel
) {
val notifTanah = sensorViewModel.notifTanahAktif
val notifHujan = sensorViewModel.notifHujanAktif
val buzzerAktif = sensorViewModel.buzzerAktif
val context = LocalContext.current
Text(
text = stringResource(R.string.notification),
fontSize = 15.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Medium,
color = Color.Gray,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 28.dp)
)
Spacer(modifier = Modifier.height(15.dp))
Card(
modifier = Modifier
.padding(horizontal = 25.dp)
.size(width = 333.dp, height = 140.dp),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFEDEDED)
)
) {
Column(modifier = Modifier.padding(16.dp)
.verticalScroll(rememberScrollState())) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text( text = stringResource(R.string.soil),)
Switch(
checked = notifTanah,
onCheckedChange = {
sensorViewModel.setNotifTanah(it)
Toast.makeText(
context,
if (it) "Notif tanah aktif"
else "Notif tanah dimatikan",
Toast.LENGTH_SHORT
).show()
}
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text( text = stringResource(R.string.rain),)
Switch(
checked = notifHujan,
onCheckedChange = {
sensorViewModel.setNotifHujan(it)
Toast.makeText(
context,
if (it) "Notif hujan aktif"
else "Notif hujan dimatikan",
Toast.LENGTH_SHORT
).show()
}
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text("Buzzer")
Switch(
checked = buzzerAktif,
onCheckedChange = {
sensorViewModel.setBuzzer(it)
Toast.makeText(
context,
if (it) "Buzzer aktif"
else "Buzzer dimatikan",
Toast.LENGTH_SHORT
).show()
}
)
}
}
}
}

View File

@ -0,0 +1,56 @@
package com.example.uitugasakhir
import android.app.Service
import android.content.Intent
import android.os.IBinder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class SensorMonitoringService : Service() {
private val scope = CoroutineScope(Dispatchers.IO)
private var job: Job? = null
override fun onStartCommand(
intent: Intent?,
flags: Int,
startId: Int
): Int {
SensorNotification.createChannel(this)
job = scope.launch {
while (true) {
// Ambil data sensor dari Firebase/API
// lalu cek ambang batas
val suhu = 35
if (suhu > 30) {
SensorNotification.showNotification(
this@SensorMonitoringService,
"Peringatan Suhu",
"Suhu terlalu tinggi"
)
}
delay(15000)
}
}
return START_STICKY
}
override fun onDestroy() {
super.onDestroy()
job?.cancel()
}
override fun onBind(intent: Intent?): IBinder? = null
}

View File

@ -0,0 +1,77 @@
package com.example.uitugasakhir
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import android.Manifest
import android.content.pm.PackageManager
import androidx.core.content.ContextCompat
object SensorNotification {
private const val CHANNEL_ID = "sensor_alert_channel"
private const val NOTIFICATION_ID = 1001
fun createChannel(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
"Sensor Alert",
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "Notifikasi monitoring sensor"
}
val manager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
manager.createNotificationChannel(channel)
}
}
fun showNotification(
context: Context,
title: String,
message: String
) {
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
putExtra("route", "notifikasi")
}
val pendingIntent = PendingIntent.getActivity(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentTitle(title)
.setContentText(message)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
.setOnlyAlertOnce(true)
.setContentIntent(pendingIntent) // 🔥 INI PENTING
if (ContextCompat.checkSelfPermission(
context,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
) {
NotificationManagerCompat.from(context)
.notify(NOTIFICATION_ID, builder.build())
}
}
}

View File

@ -0,0 +1,277 @@
package com.example.uitugasakhir.ui_manual
import android.content.Intent
import android.net.Uri
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.app.ui.theme.Inria_Regular
import com.example.uitugasakhir.R
import com.example.uitugasakhir.ViewModel.MainViewModel
import kotlinx.coroutines.launch
@Composable
fun RataRataSection(
snackbarHostState: SnackbarHostState
) {
val mainViewModel: MainViewModel = viewModel()
val context = LocalContext.current
val scope = rememberCoroutineScope()
val avgKU = mainViewModel.avgKU
val avgSU = mainViewModel.avgSU
val avgKT = mainViewModel.avgKT
val avgIH = mainViewModel.avgIH
Text(
text = stringResource(R.string.average_sensor_status),
fontSize = 15.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Medium,
color = Color.Black,
modifier = Modifier.padding(horizontal = 28.dp)
)
Spacer(modifier = Modifier.height(5.dp))
Text(
text = stringResource(R.string.a_summary_of_the_latest_status_from_each_sensor),
fontSize = 12.sp,
fontFamily = Inria_Regular,
color = Color.Gray,
modifier = Modifier.padding(horizontal = 28.dp)
)
Spacer(modifier = Modifier.height(15.dp))
Card(
modifier = Modifier
.padding(horizontal = 25.dp)
.fillMaxWidth()
.height(190.dp),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFEDEDED)
),
elevation = CardDefaults.cardElevation(4.dp)
) {
Column(
modifier = Modifier.fillMaxSize()
) {
LazyRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(12.dp)
) {
item {
SensorCard(
title = "💧 Udara",
value = if(avgKU > 0) "${avgKU.toInt()}%" else "--",
status = if(avgKU > 0) statusKelembaban(avgKU) else "Memuat...",
color = Color(0xFF4FC3F7)
)
}
item {
SensorCard(
title = "🌡 Suhu",
value = if(avgSU > 0) "${avgSU.toInt()}°C" else "--",
status = if(avgSU > 0) statusSuhu(avgSU) else "Memuat...",
color = Color(0xFF2E8B57)
)
}
item {
SensorCard(
title = "🌱 Tanah",
value = if(avgKT > 0) "${avgKT.toInt()}%" else "--",
status = if(avgKT > 0) statusKelembaban(avgKT) else "Memuat...",
color = Color(0xFF8B5A2B)
)
}
item {
SensorCard(
title = "🌧 Hujan",
value = if(avgIH > 0) "${avgIH.toInt()}%" else "--",
status = if(avgIH > 0){
when{
avgIH < 40 -> "Ringan"
avgIH < 70 -> "Sedang"
else -> "Deras"
}
}else{
"Memuat..."
},
color = Color(0xFF1E3A5F)
)
}
}
Spacer(
modifier = Modifier.weight(1f)
)
Card(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFE8F5E9)
)
){
Row(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
){
Text(
text = "📥",
fontSize = 26.sp
)
Spacer(
modifier = Modifier.width(10.dp)
)
Column(
modifier = Modifier.weight(1f)
){
Text(
text = "Unduh Data Monitoring",
fontSize = 13.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFF1B5E20)
)
Text(
text = "Ekspor data sensor untuk laporan",
fontSize = 11.sp,
color = Color(0xFF388E3C)
)
}
Button(
onClick = {
val intent = Intent(
Intent.ACTION_VIEW,
Uri.parse(
"http://kopikaosmartsystem.my.id/download_sensor_excel.php"
)
)
try{
context.startActivity(intent)
scope.launch {
snackbarHostState.showSnackbar(
"Membuka data sensor..."
)
}
}catch(e:Exception){
scope.launch {
snackbarHostState.showSnackbar(
"Gagal membuka data"
)
}
}
},
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF4CAF50)
)
){
Text(
text = "Unduh",
fontSize = 11.sp
)
}
}
}
}
}
}
@Composable
fun SensorCard(
title:String,
value:String,
status:String,
color:Color
){
Card(
modifier = Modifier
.width(95.dp)
.height(85.dp),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = Color.White
),
elevation = CardDefaults.cardElevation(2.dp)
){
Column(
modifier = Modifier
.fillMaxSize()
.padding(8.dp),
verticalArrangement = Arrangement.SpaceBetween
){
Text(
text = title,
fontSize = 10.sp,
fontWeight = FontWeight.SemiBold,
color = color
)
Text(
text = value,
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = Color.Black
)
Text(
text = status,
fontSize = 9.sp,
color = Color.Gray
)
}
}
}

View File

@ -0,0 +1,218 @@
package com.example.uitugasakhir.ui_manual
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.app.ui.theme.Inria_Regular
import com.example.uitugasakhir.ViewModel.SensorViewModel
import androidx.compose.ui.platform.LocalContext
import com.example.uitugasakhir.R
import androidx.compose.ui.res.stringResource
// ================= STATUS =================
fun statusKelembaban(value: Float): String {
return when {
value < 40 -> "Kering"
value < 70 -> "Normal"
else -> "Lembab"
}
}
fun statusSuhu(value: Float): String {
return when {
value < 25 -> "Dingin"
value < 32 -> "Normal"
else -> "Panas"
}
}
// ================= UI =================
@Composable
fun SensorSection(
) {
val context = LocalContext.current
val sensorVm: SensorViewModel = viewModel()
val data = sensorVm.data
// 🔥 load sekali saat pertama buka
LaunchedEffect(Unit) {
sensorVm.getData(context)
}
val ku = data?.kelembaban_udara
val su = data?.suhu_udara
val kt = data?.kelembaban_tanah
val ih = data?.intensitas_hujan
val progressKU = ku?.div(100f)
val progressSU = su?.div(50f)
val progressKT = kt?.div(100f)
val progressIH = ih?.div(100f)
Column {
Text(
text = stringResource(R.string.sensor_monitoring),
fontSize = 15.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Medium,
color = Color.Black,
modifier = Modifier.padding(horizontal = 28.dp)
)
Spacer(modifier = Modifier.height(5.dp))
Text(
text = stringResource(R.string.monitor_environmental_conditions_in_real_time),
fontSize = 12.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Normal,
color = Color.Gray,
modifier = Modifier.padding(horizontal = 28.dp)
)
Spacer(modifier = Modifier.height(15.dp))
Card(
modifier = Modifier
.padding(horizontal = 25.dp)
.fillMaxWidth(),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFEDEDED)
),
elevation = CardDefaults.cardElevation(4.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
horizontalArrangement = Arrangement.SpaceEvenly
) {
com.example.uitugasakhir.ui_otomatis.SensorCard(
title = "Kel. Udara",
value = if (ku != null) "${ku.toInt()}%" else "--",
status = if (ku != null) com.example.uitugasakhir.ui_otomatis.statusKelembaban(
ku
) else "--",
progress = progressKU ?: 0f,
color = Color(0xFF4FC3F7)
)
com.example.uitugasakhir.ui_otomatis.SensorCard(
title = "Suhu Udara",
value = if (su != null) "${su.toInt()}°C" else "--",
status = if (su != null) com.example.uitugasakhir.ui_otomatis.statusSuhu(su) else "--",
progress = progressSU ?: 0f,
color = Color(0xFF2E8B57)
)
com.example.uitugasakhir.ui_otomatis.SensorCard(
title = "Kel. Tanah",
value = if (kt != null) "${kt.toInt()}%" else "--",
status = if (kt != null) com.example.uitugasakhir.ui_otomatis.statusKelembaban(
kt
) else "--",
progress = progressKT ?: 0f,
color = Color(0xFF8B5A2B)
)
com.example.uitugasakhir.ui_otomatis.SensorCard(
title = "Int. Hujan",
value = if (ih != null) "${ih.toInt()}%" else "--",
status = if (ih != null) {
when {
ih < 40 -> "Ringan"
ih < 70 -> "Sedang"
else -> "Deras"
}
} else "--",
progress = progressIH ?: 0f,
color = Color(0xFF1E3A5F)
)
}
}
}
}
@Composable
fun SensorCard(
title: String,
value: String,
status: String,
progress: Float,
color: Color
) {
Card(
modifier = Modifier
.width(75.dp)
.height(120.dp),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = Color.White
),
elevation = CardDefaults.cardElevation(3.dp)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceEvenly
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.size(50.dp)
) {
CircularProgressIndicator(
progress = { 1f },
color = Color.LightGray,
strokeWidth = 3.dp,
modifier = Modifier.fillMaxSize()
)
CircularProgressIndicator(
progress = { progress },
color = color,
strokeWidth = 3.dp,
modifier = Modifier.fillMaxSize()
)
Text(
text = value,
fontSize = 10.sp,
fontWeight = FontWeight.Bold
)
}
Text(
text = title,
fontSize = 9.sp,
fontWeight = FontWeight.Bold
)
Text(
text = status,
fontSize = 8.sp,
color = Color.Gray
)
}
}
}

View File

@ -0,0 +1,169 @@
package com.example.uitugasakhir.ui_manual
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.animation.animateColorAsState
import android.util.Log
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import com.example.app.ui.theme.Inria_Regular
import com.example.uitugasakhir.R
@Composable
fun PenyiramanSection(
mode: String,
pumpStatus: String,
isLoading: Boolean = false,
onPumpChange: (String) -> Unit
) {
val isManual = mode == "Manual"
val isOn = pumpStatus == "ON"
val statusColor by animateColorAsState(
targetValue = if (isOn) Color(0xFF2F6F4E) else Color(0xFFD81F23),
label = ""
)
Column {
// ================= TITLE =================
Text(
text = stringResource(R.string.watering),
fontSize = 15.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Medium,
color = Color.Black,
modifier = Modifier.padding(horizontal = 28.dp)
)
Spacer(modifier = Modifier.height(5.dp))
Text(
text = stringResource(R.string.watering_is_performed_manually),
fontSize = 12.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Normal,
color = Color.Gray,
modifier = Modifier.padding(horizontal = 28.dp)
)
Spacer(modifier = Modifier.height(15.dp))
// ================= STATUS =================
Text(
text = if (isOn) "🟢 Pompa Aktif" else "🔴 Pompa Mati",
color = statusColor,
fontSize = 14.sp,
modifier = Modifier.padding(horizontal = 28.dp)
)
Spacer(modifier = Modifier.height(12.dp))
// ================= CARD =================
Card(
modifier = Modifier
.padding(horizontal = 25.dp)
.fillMaxWidth()
.height(120.dp),
shape = RoundedCornerShape(24.dp),
elevation = CardDefaults.cardElevation(4.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFF5F5F5)
)
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center
) {
// ================= SEGMENTED =================
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.height(50.dp)
.clip(RoundedCornerShape(50.dp))
.background(Color(0xFFE0E0E0))
.padding(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
// ===== ON =====
val onColor by animateColorAsState(
targetValue = if (isOn) Color(0xFF2F6F4E) else Color.Transparent,
label = ""
)
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.clip(RoundedCornerShape(50.dp))
.background(color = onColor)
.clickable(enabled = isManual && !isLoading) {
Log.d("DEBUG", "PUMP CHANGE DIPANGGIL")
onPumpChange("ON")
},
contentAlignment = Alignment.Center
) {
Text(
text = if (isLoading && isOn) "Menyalakan..." else "🌱 ON",
color = if (isOn) Color.White else Color.Black
)
}
// ===== OFF =====
val offColor by animateColorAsState(
targetValue = if (!isOn) Color(0xFFD81F23) else Color.Transparent,
label = ""
)
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.clip(RoundedCornerShape(50.dp))
.background(color = offColor)
.clickable(enabled = isManual && !isLoading) {
Log.d("BUTTON", "OFF DIKLIK")
onPumpChange("OFF")
},
contentAlignment = Alignment.Center
) {
Text(
text = if (isLoading && !isOn) "Mematikan..." else "🛑 OFF",
color = if (!isOn) Color.White else Color.Black
)
}
}
// ================= LOADING =================
if (isLoading) {
Spacer(modifier = Modifier.height(10.dp))
LinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp)
)
}
}
}
}
}

View File

@ -0,0 +1,144 @@
package com.example.uitugasakhir.ui_manual
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.app.ui.theme.Inria_Regular
import com.example.app.ui.theme.Poppins_Bold
import com.example.app.ui.theme.Poppins_Medium
import com.example.uitugasakhir.R
import com.example.uitugasakhir.model.RiwayatItem
@Composable
fun RiwayatSection(riwayatList: List<RiwayatItem>) {
Column {
Text(
text = stringResource(R.string.history),
fontSize = 15.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Medium,
color = Color.Black,
modifier = Modifier.padding(horizontal = 28.dp)
)
Spacer(modifier = Modifier.height(5.dp))
Text(
text = stringResource(R.string.manual_watering_activity_log),
fontSize = 12.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Normal,
color = Color.Gray,
modifier = Modifier.padding(horizontal = 28.dp)
)
Spacer(modifier = Modifier.height(15.dp))
Card(
modifier = Modifier
.padding(horizontal = 25.dp)
.padding(bottom = 25.dp)
.size(width = 333.dp, height = 214.dp),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFEDEDED)
),
elevation = CardDefaults.cardElevation(4.dp)
) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(vertical = 8.dp)
) {
items(riwayatList) { item ->
val bgColor = when (item.status) {
"Berjalan" -> Color(0xFFD6F5DD)
"Selesai" -> Color(0xFFFFE0E0)
"Monitoring" -> Color(0xFFFFF3CD)
"Delay" -> Color(0xFFE3F2FD)
else -> Color.LightGray
}
val textColor = when (item.status) {
"Berjalan" -> Color(0xFF2E7D32)
"Selesai" -> Color.Red
"Monitoring" -> Color(0xFFB8860B)
"Delay" -> Color(0xFF1565C0)
else -> Color.DarkGray
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = "${item.waktu}${item.mode}",
fontSize = 13.sp,
fontFamily = Poppins_Bold,
color = Color.Black.copy(alpha = 0.8f)
)
Spacer(modifier = Modifier.height(2.dp))
Text(
text = item.aksi,
fontSize = 13.sp,
fontFamily = Poppins_Bold,
color = Color.Black.copy(alpha = 0.8f)
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "${item.keterangan} | Jeda: ${item.jeda} menit",
fontSize = 11.sp,
fontFamily = Poppins_Medium,
color = Color.Gray
)
}
Box(
modifier = Modifier
.background(bgColor, RoundedCornerShape(12.dp))
.padding(horizontal = 10.dp, vertical = 5.dp)
) {
Text(
text = item.status,
fontSize = 10.sp,
fontFamily = Poppins_Medium,
color = textColor
)
}
}
HorizontalDivider(
thickness = 0.8.dp,
color = Color.Gray.copy(alpha = 0.3f)
)
}
}
}
}
}

View File

@ -0,0 +1,452 @@
package com.example.uitugasakhir.ui_otomatis
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import android.widget.Toast
import androidx.compose.ui.platform.LocalContext
import com.example.app.ui.theme.Inria_Regular
import org.eclipse.paho.client.mqttv3.*
import org.json.JSONObject
import android.util.Log
import com.example.uitugasakhir.R
import com.example.uitugasakhir.model.AmbangBatas
import com.example.uitugasakhir.api.ApiAmbangBatas
import com.example.uitugasakhir.mqtt.MqttManager
@Composable
fun AmbangBatasSection() {
val context = LocalContext.current
var popupMessage by remember { mutableStateOf("") }
val mqtt = remember {
MqttManager(context)
}
var showPopup by remember { mutableStateOf(false) }
var ledState by remember { mutableStateOf(0) }
var buzzerState by remember { mutableStateOf(0) }
// ================= STATE =================
var isLoadingSave by remember { mutableStateOf(false) }
var showDialog by remember { mutableStateOf(false) }
var currentField by remember { mutableStateOf("") }
var ambang by remember { mutableStateOf<AmbangBatas?>(null) }
var criticalTanah by remember { mutableStateOf("") }
var minTanah by remember { mutableStateOf("") }
var maxTanah by remember { mutableStateOf("") }
var minSuhu by remember { mutableStateOf("") }
var maxSuhu by remember { mutableStateOf("") }
var minUdara by remember { mutableStateOf("") }
var maxUdara by remember { mutableStateOf("") }
var isSaved by remember { mutableStateOf(false) }
// ================= LOAD DATA =================
LaunchedEffect(Unit) {
ApiAmbangBatas.getAmbangBatas(
context = context,
onSuccess = {
ambang = it
minTanah = it.min_tanah.toString()
maxTanah = it.max_tanah.toString()
minSuhu = it.min_suhu.toString()
maxSuhu = it.max_suhu.toString()
minUdara = it.min_udara.toString()
maxUdara = it.max_udara.toString()
criticalTanah = it.critical_tanah.toString()
isSaved = true
},
onError = {
Toast.makeText(
context,
it,
Toast.LENGTH_SHORT
).show()
}
)
}
// ================= TITLE =================
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Start
) {
Text(
text = "Atur ambang batas",
fontSize = 15.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Medium,
color = Color.Gray,
)
Spacer(modifier = Modifier.width(15.dp))
}
Spacer(modifier = Modifier.height(15.dp))
// ================= CARD =================
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(
Color(0xFFEDEDED)
),
elevation = CardDefaults.cardElevation(4.dp)
) {
Column {
SettingItem(
title = "Kelembapan tanah",
minValue = minTanah,
maxValue = maxTanah,
isSaved = true,
iconRes = R.drawable.decagon,
onEditClick = {
currentField = "tanah"
showDialog = true
}
)
HorizontalDivider(
modifier = Modifier.padding(horizontal = 16.dp)
)
SettingItem(
title = "Suhu udara",
minValue = minSuhu,
maxValue = maxSuhu,
isSaved = true,
iconRes = R.drawable.thermometer,
onEditClick = {
currentField = "suhu"
showDialog = true
}
)
HorizontalDivider(
modifier = Modifier.padding(horizontal = 16.dp)
)
}
}
// ================= DIALOG =================
if (showDialog) {
AlertDialog(
onDismissRequest = {
showDialog = false
},
title = {
Text("Atur Ambang Batas - $currentField")
},
text = {
Column {
// MIN
Text("Minimal (%)")
TextField(
value = when (currentField) {
"tanah" -> minTanah
"suhu" -> minSuhu
else -> minUdara
},
onValueChange = { value ->
when (currentField) {
"tanah" -> minTanah = value
"suhu" -> minSuhu = value
"udara" -> minUdara = value
}
},
singleLine = true
)
Spacer(modifier = Modifier.height(12.dp))
// MAX
Text("Maksimal (%)")
TextField(
value = when (currentField) {
"tanah" -> maxTanah
"suhu" -> maxSuhu
else -> maxUdara
},
onValueChange = { value ->
when (currentField) {
"tanah" -> maxTanah = value
"suhu" -> maxSuhu = value
"udara" -> maxUdara = value
}
},
singleLine = true
)
if (currentField == "tanah") {
Spacer(modifier = Modifier.height(12.dp))
Text("Critical (%)")
TextField(
value = criticalTanah,
onValueChange = { criticalTanah = it },
singleLine = true
)
}
}
},
confirmButton = {
TextButton(
onClick = {
// 1. SAVE DATABASE (backup)
ApiAmbangBatas.saveAmbangBatas(
context = context,
minTanah = minTanah.toIntOrNull() ?: 0,
maxTanah = maxTanah.toIntOrNull() ?: 0,
minSuhu = minSuhu.toIntOrNull() ?: 0,
maxSuhu = maxSuhu.toIntOrNull() ?: 0,
minUdara = minUdara.toIntOrNull() ?: 0,
maxUdara = maxUdara.toIntOrNull() ?: 0,
criticalTanah = maxTanah.toIntOrNull() ?: 0,
onSuccess = {
Toast.makeText(context, "Saved DB", Toast.LENGTH_SHORT).show()
},
onError = {
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
}
)
// 2. MQTT REALTIME SEND (INI YANG PENTING)
mqtt.publish(
"iot/config/update",
buildThresholdJson(
minTanah,
maxTanah,
criticalTanah,
minSuhu,
maxSuhu,
minUdara,
maxUdara
)
)
Toast.makeText(context, "Sent to ESP32", Toast.LENGTH_SHORT).show()
showDialog = false
}
) {
Text("Simpan")
}
},
dismissButton = {
TextButton(
onClick = {
showDialog = false
}
) {
Text("Batal")
}
}
)
}
}
@Composable
fun SettingItem(
title: String,
minValue: String,
maxValue: String,
isSaved: Boolean,
iconRes: Int,
onEditClick: () -> Unit
) {
Column(
modifier = Modifier.padding(top = 8.dp)
) {
// TITLE
Row(
modifier = Modifier
.padding(horizontal = 15.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Row(
modifier = Modifier.width(170.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(id = iconRes),
contentDescription = null,
modifier = Modifier.size(18.dp),
colorFilter = ColorFilter.tint(Color(0xFF6B6666))
)
Text(
text = title,
fontSize = 15.sp,
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(start = 9.dp)
)
}
}
Spacer(modifier = Modifier.height(6.dp))
// CONTENT
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 43.dp, end = 15.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(
text = "Min: $minValue%",
fontSize = 15.sp,
fontWeight = FontWeight.Normal,
color = Color.Black.copy(alpha = 0.7f)
)
Text(
text = "Max: $maxValue%",
fontSize = 15.sp,
fontWeight = FontWeight.Normal,
color = Color.Black.copy(alpha = 0.7f)
)
}
Row(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = if (isSaved)
"Tersimpan"
else
"Belum diatur",
fontSize = 10.sp,
color = Color.Gray
)
Spacer(modifier = Modifier.width(5.dp))
if (isSaved) {
Image(
painter = painterResource(id = R.drawable.checked),
contentDescription = null,
modifier = Modifier.size(13.dp)
)
}
Spacer(modifier = Modifier.width(5.dp))
Box(
modifier = Modifier
.size(30.dp)
.clickable {
onEditClick()
},
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(id = R.drawable.edit_text),
contentDescription = "edit",
modifier = Modifier.size(18.dp)
)
}
}
}
Spacer(modifier = Modifier.height(8.dp))
}
}
fun buildThresholdJson(
minTanah: String,
maxTanah: String,
criticalTanah: String,
minSuhu: String,
maxSuhu: String,
minUdara: String,
maxUdara: String
): String {
return """
{
"type": "threshold",
"tanah": {
"min": ${minTanah.toIntOrNull() ?: 0},
"max": ${maxTanah.toIntOrNull() ?: 0},
"critical": ${criticalTanah.toIntOrNull() ?: 0}
},
"suhu": {
"min": ${minSuhu.toIntOrNull() ?: 0},
"max": ${maxSuhu.toIntOrNull() ?: 0}
},
"udara": {
"min": ${minUdara.toIntOrNull() ?: 0},
"max": ${maxUdara.toIntOrNull() ?: 0}
}
}
""".trimIndent()
}

View File

@ -0,0 +1,220 @@
package com.example.uitugasakhir.ui_otomatis
import android.app.TimePickerDialog
import android.content.Context
import android.util.Log
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.android.volley.Request
import com.android.volley.toolbox.StringRequest
import com.android.volley.toolbox.Volley
import com.example.app.ui.theme.Inria_Regular
import com.example.uitugasakhir.R
import com.example.uitugasakhir.model.JadwalItem
@Composable
fun JadwalSection(
jadwalList: List<JadwalItem>,
loadJadwal: () -> Unit,
context: Context,
onDeleteClick: (String) -> Unit
) {
fun getWaktuStatus(hour: Int, minute: Int): String {
return when {
hour in 5..10 -> "Pagi"
hour in 11..14 -> "Siang"
hour in 15..18 && !(hour == 18 && minute >= 30) -> "Petang"
hour == 18 && minute >= 30 -> "Senja"
hour in 19..23 -> "Malam"
else -> "Dini Hari"
}
}
Column {
Text("Atur jadwal",
fontSize = 15.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Medium,
color = Color.Gray,)
Spacer(modifier = Modifier.height(15.dp))
Card(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 150.dp, max = 350.dp),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(Color(0xFFEDEDED))
) {
LazyColumn {
itemsIndexed(jadwalList) { index, item ->
Column{
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(verticalAlignment = Alignment.CenterVertically) {
// 🗑️ DELETE
IconButton(
onClick = {
Log.d("DELETE_CLICK", "ID: ${item.id}")
onDeleteClick(item.id)
}
) {
Icon(
painter = painterResource(id = R.drawable.trash),
contentDescription = "Hapus",
modifier = Modifier.size(18.dp)
)
}
Text("Jadwal ${index + 1}",
fontSize = 15.sp,
fontWeight = FontWeight.Normal)
}
// ✏️ EDIT
IconButton(
onClick = {
TimePickerDialog(
context,
{ _, h, m ->
val status = getWaktuStatus(h, m)
val waktu = String.format("%02d:%02d", h, m)
Log.d("EDIT_KIRIM", "ID: ${item.id}")
Log.d("EDIT_KIRIM", "Waktu: $waktu")
Log.d("EDIT_KIRIM", "Status: $status")
val request = object : StringRequest(
Request.Method.POST,
"http://kopikaosmartsystem.my.id/edit_jadwal.php",
{ response ->
Log.d("EDIT_SUCCESS", "Response: $response")
loadJadwal()
},
{ error ->
Log.e("EDIT_ERROR", "Error: ${error.message}")
Log.e("EDIT_ERROR", "Detail: ${error.toString()}")
}
) {
override fun getParams(): MutableMap<String, String> {
val params = hashMapOf(
"id" to item.id.toString(),
"waktu" to waktu,
"status" to status
)
Log.d("EDIT_PARAMS", params.toString())
return params
}
}
Volley.newRequestQueue(context).add(request)
},
0, 0, true
).show()
}
) {
Icon(
painter = painterResource(id = R.drawable.edit_text),
contentDescription = "Edit",
modifier = Modifier.size(18.dp)
)
}
}
// 🔥 tampilkan data
Text(
text = "${item.waktu} ${item.status}",
modifier = Modifier.padding(start = 48.dp)
.offset(y = -8.dp)
)
Spacer(modifier = Modifier.height(6.dp))
HorizontalDivider(
modifier = Modifier.padding(horizontal = 16.dp)
)
}
}
// TAMBAH
item {
TextButton(
onClick = {
TimePickerDialog(
context,
{ _, h, m ->
val status = getWaktuStatus(h, m)
val waktu = String.format("%02d:%02d:00", h, m)
Log.d("TAMBAH_KIRIM", "Waktu: $waktu")
Log.d("TAMBAH_KIRIM", "Status: $status")
val request = object : StringRequest(
Request.Method.POST,
"http://kopikaosmartsystem.my.id/tambah_jadwal.php",
{ response ->
Log.d("TAMBAH_SUCCESS", "Response: $response")
loadJadwal()
},
{ error ->
Log.e("TAMBAH_ERROR", "Error: ${error.message}")
Log.e("TAMBAH_ERROR", "Detail: ${error.toString()}")
}
) {
override fun getParams(): MutableMap<String, String> {
val params = hashMapOf(
"waktu" to waktu,
"status" to status
)
Log.d("TAMBAH_PARAMS", params.toString())
return params
}
}
Volley.newRequestQueue(context).add(request)
},
0, 0, true
).show()
}
) {
Text("+ Tambah Jadwal")
}
}
}
}
}
}

View File

@ -0,0 +1,216 @@
package com.example.uitugasakhir.ui_otomatis
import android.util.Log
import android.widget.Toast
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.app.ui.theme.Inria_Regular
import com.example.app.ui.theme.Poppins_Black
import com.example.uitugasakhir.R
import com.example.uitugasakhir.api.RetrofitClient
import kotlinx.coroutines.launch
@Composable
fun JedaPenyiramanCard(
onJedaChanged: (Int) -> Unit
) {
var jeda by remember { mutableStateOf("") }
var selectedUnit by remember { mutableStateOf("Detik") }
var expanded by remember { mutableStateOf(false) }
val context = LocalContext.current
val scope = rememberCoroutineScope()
LaunchedEffect(Unit) {
try {
val response = RetrofitClient.api.getJeda()
if (response.isSuccessful) {
val body = response.body()
if (body?.success == true) {
val detik = body.data?.jeda?.toIntOrNull() ?: 0
jeda = detik.toString()
onJedaChanged(detik)
selectedUnit = "Detik"
}
}
} catch (e: Exception) {
Log.e("JEDA", e.message.toString())
}
}
Column(modifier = Modifier.fillMaxWidth()) {
Row {
Text(
text = "Atur jeda penyiraman",
fontSize = 15.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Medium,
color = Color.Gray,
)
Spacer(modifier = Modifier.width(15.dp))
Image(
painter = painterResource(id = R.drawable.clock),
contentDescription = null,
modifier = Modifier.size(14.dp)
.offset(y = 3.dp),
colorFilter = ColorFilter.tint(Color(0xFF6B6666))
)
}
Spacer(modifier = Modifier.height(15.dp))
Card(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(Color(0xFFEDEDED))
) {
Column(
modifier = Modifier.padding(top = 5.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(verticalAlignment = Alignment.CenterVertically) {
BasicTextField(
value = jeda,
onValueChange = {
if (it.all { c -> c.isDigit() }) {
jeda = it.take(3)
}
},
singleLine = true,
textStyle = LocalTextStyle.current.copy(
fontSize = 28.sp,
fontWeight = FontWeight.Bold,
fontFamily = Poppins_Black
),
modifier = Modifier.width(60.dp),
decorationBox = {
if (jeda.isEmpty()) {
Text("00", fontSize = 28.sp)
}
it()
}
)
Box {
Text(
text = selectedUnit,
modifier = Modifier.clickable { expanded = true }
)
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
listOf("Detik", "Menit", "Jam").forEach {
DropdownMenuItem(
text = { Text(it) },
onClick = {
selectedUnit = it
expanded = false
}
)
}
}
}
}
Spacer(modifier = Modifier.height(4.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 10.dp)
.padding(horizontal = 20.dp),
horizontalArrangement = Arrangement.spacedBy(5.dp)
) {
// 🔴 HAPUS (set 0)
Button(
onClick = {
scope.launch {
try {
val res = RetrofitClient.api.setJeda("0")
Toast.makeText(context, res.body()?.message ?: "OK", Toast.LENGTH_SHORT).show()
jeda = ""
} catch (e: Exception) {
Log.e("JEDA", e.message.toString())
}
}
},
modifier = Modifier.weight(1f),
colors = ButtonDefaults.buttonColors(Color(0xFFD32F2F))
) {
Text("Hapus", color = Color.White)
}
// 🟢 SIMPAN
Button(
onClick = {
if (jeda.isEmpty()) return@Button
val value = jeda.toInt()
val detik = when (selectedUnit) {
"Detik" -> value
"Menit" -> value * 60
"Jam" -> value * 3600
else -> value
}
onJedaChanged(detik)
scope.launch {
try {
val res = RetrofitClient.api.setJeda(detik.toString())
Toast.makeText(
context,
res.body()?.message ?: "Berhasil",
Toast.LENGTH_SHORT
).show()
} catch (e: Exception) {
Log.e("JEDA", e.message.toString())
}
}
},
modifier = Modifier.weight(1f),
colors = ButtonDefaults.buttonColors(Color(0xFF2F6F4E))
) {
Text("Simpan", color = Color.White)
}
}
}
}
}
}

View File

@ -0,0 +1,212 @@
package com.example.uitugasakhir.ui_otomatis
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.app.ui.theme.Inria_Regular
import com.example.uitugasakhir.ViewModel.SensorViewModel
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import com.example.uitugasakhir.R
// ================= STATUS =================
fun statusKelembaban(value: Float): String {
return when {
value < 40 -> "Kering"
value < 70 -> "Normal"
else -> "Lembab"
}
}
fun statusSuhu(value: Float): String {
return when {
value < 25 -> "Dingin"
value < 32 -> "Normal"
else -> "Panas"
}
}
// ================= UI =================
@Composable
fun SensorSectionOtomatis(
viewModel: SensorViewModel = viewModel()
) {
val context = LocalContext.current
val data = viewModel.data
// 🔥 load sekali saat pertama buka
LaunchedEffect(Unit) {
viewModel.getData(context)
}
val ku = data?.kelembaban_udara
val su = data?.suhu_udara
val kt = data?.kelembaban_tanah
val ih = data?.intensitas_hujan
val progressKU = ku?.div(100f)
val progressSU = su?.div(50f)
val progressKT = kt?.div(100f)
val progressIH = ih?.div(100f)
Column {
Text(
text = stringResource(R.string.sensor_monitoring),
fontSize = 15.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Medium,
color = Color.Black,
modifier = Modifier.padding(horizontal = 28.dp)
)
Spacer(modifier = Modifier.height(5.dp))
Text(
text = stringResource(R.string.monitor_environmental_conditions_in_real_time),
fontSize = 12.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Normal,
color = Color.Gray,
modifier = Modifier.padding(horizontal = 28.dp)
)
Spacer(modifier = Modifier.height(15.dp))
Card(
modifier = Modifier
.padding(horizontal = 25.dp)
.fillMaxWidth(),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFEDEDED)
),
elevation = CardDefaults.cardElevation(4.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
horizontalArrangement = Arrangement.SpaceEvenly
) {
SensorCard(
title = "Kel. Udara",
value = if (ku != null) "${ku.toInt()}%" else "--",
status = if (ku != null) statusKelembaban(ku) else "--",
progress = progressKU ?: 0f,
color = Color(0xFF4FC3F7)
)
SensorCard(
title = "Suhu Udara",
value = if (su != null) "${su.toInt()}°C" else "--",
status = if (su != null) statusSuhu(su) else "--",
progress = progressSU ?: 0f,
color = Color(0xFF2E8B57)
)
SensorCard(
title = "Kel. Tanah",
value = if (kt != null) "${kt.toInt()}%" else "--",
status = if (kt != null) statusKelembaban(kt) else "--",
progress = progressKT ?: 0f,
color = Color(0xFF8B5A2B)
)
SensorCard(
title = "Int. Hujan",
value = if (ih != null) "${ih.toInt()}%" else "--",
status = if (ih != null) {
when {
ih < 40 -> "Ringan"
ih < 70 -> "Sedang"
else -> "Deras"
}
} else "--",
progress = progressIH ?: 0f,
color = Color(0xFF1E3A5F)
)
}
}
}
}
@Composable
fun SensorCard(
title: String,
value: String,
status: String,
progress: Float,
color: Color
) {
Card(
modifier = Modifier
.width(75.dp)
.height(120.dp),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = Color.White
),
elevation = CardDefaults.cardElevation(3.dp)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceEvenly
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.size(50.dp)
) {
CircularProgressIndicator(
progress = { 1f },
color = Color.LightGray,
strokeWidth = 3.dp,
modifier = Modifier.fillMaxSize()
)
CircularProgressIndicator(
progress = { progress },
color = color,
strokeWidth = 3.dp,
modifier = Modifier.fillMaxSize()
)
Text(
text = value,
fontSize = 10.sp,
fontWeight = FontWeight.Bold
)
}
Text(
text = title,
fontSize = 9.sp,
fontWeight = FontWeight.Bold
)
Text(
text = status,
fontSize = 8.sp,
color = Color.Gray
)
}
}
}

View File

@ -0,0 +1,277 @@
package com.example.uitugasakhir.ui_otomatis
import android.content.Intent
import android.net.Uri
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.app.ui.theme.Inria_Regular
import com.example.uitugasakhir.R
import com.example.uitugasakhir.ViewModel.MainViewModel
import kotlinx.coroutines.launch
@Composable
fun RataRataOtomatisSection(
snackbarHostState: SnackbarHostState
) {
val mainViewModel: MainViewModel = viewModel()
val context = LocalContext.current
val scope = rememberCoroutineScope()
val avgKU = mainViewModel.avgKU
val avgSU = mainViewModel.avgSU
val avgKT = mainViewModel.avgKT
val avgIH = mainViewModel.avgIH
Text(
text = stringResource(R.string.average_sensor_status),
fontSize = 15.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Medium,
color = Color.Black,
modifier = Modifier.padding(horizontal = 28.dp)
)
Spacer(modifier = Modifier.height(5.dp))
Text(
text = stringResource(R.string.a_summary_of_the_latest_status_from_each_sensor),
fontSize = 12.sp,
fontFamily = Inria_Regular,
color = Color.Gray,
modifier = Modifier.padding(horizontal = 28.dp)
)
Spacer(modifier = Modifier.height(15.dp))
Card(
modifier = Modifier
.padding(horizontal = 25.dp)
.fillMaxWidth()
.height(190.dp),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFEDEDED)
),
elevation = CardDefaults.cardElevation(4.dp)
) {
Column(
modifier = Modifier.fillMaxSize()
) {
LazyRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(12.dp)
) {
item {
SensorCard(
title = "💧 Udara",
value = if(avgKU > 0) "${avgKU.toInt()}%" else "--",
status = if(avgKU > 0) statusKelembaban(avgKU) else "Memuat...",
color = Color(0xFF4FC3F7)
)
}
item {
SensorCard(
title = "🌡 Suhu",
value = if(avgSU > 0) "${avgSU.toInt()}°C" else "--",
status = if(avgSU > 0) statusSuhu(avgSU) else "Memuat...",
color = Color(0xFF2E8B57)
)
}
item {
SensorCard(
title = "🌱 Tanah",
value = if(avgKT > 0) "${avgKT.toInt()}%" else "--",
status = if(avgKT > 0) statusKelembaban(avgKT) else "Memuat...",
color = Color(0xFF8B5A2B)
)
}
item {
SensorCard(
title = "🌧 Hujan",
value = if(avgIH > 0) "${avgIH.toInt()}%" else "--",
status = if(avgIH > 0){
when{
avgIH < 40 -> "Ringan"
avgIH < 70 -> "Sedang"
else -> "Deras"
}
}else{
"Memuat..."
},
color = Color(0xFF1E3A5F)
)
}
}
Spacer(
modifier = Modifier.weight(1f)
)
Card(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFE8F5E9)
)
){
Row(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
){
Text(
text = "📥",
fontSize = 26.sp
)
Spacer(
modifier = Modifier.width(10.dp)
)
Column(
modifier = Modifier.weight(1f)
){
Text(
text = "Unduh Data Monitoring",
fontSize = 13.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFF1B5E20)
)
Text(
text = "Ekspor data sensor untuk laporan",
fontSize = 11.sp,
color = Color(0xFF388E3C)
)
}
Button(
onClick = {
val intent = Intent(
Intent.ACTION_VIEW,
Uri.parse(
"http://kopikaosmartsystem.my.id/download_sensor_excel.php"
)
)
try{
context.startActivity(intent)
scope.launch {
snackbarHostState.showSnackbar(
"Membuka data sensor..."
)
}
}catch(e:Exception){
scope.launch {
snackbarHostState.showSnackbar(
"Gagal membuka data"
)
}
}
},
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF4CAF50)
)
){
Text(
text = "Unduh",
fontSize = 11.sp
)
}
}
}
}
}
}
@Composable
fun SensorCard(
title:String,
value:String,
status:String,
color:Color
){
Card(
modifier = Modifier
.width(95.dp)
.height(85.dp),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = Color.White
),
elevation = CardDefaults.cardElevation(2.dp)
){
Column(
modifier = Modifier
.fillMaxSize()
.padding(8.dp),
verticalArrangement = Arrangement.SpaceBetween
){
Text(
text = title,
fontSize = 10.sp,
fontWeight = FontWeight.SemiBold,
color = color
)
Text(
text = value,
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = Color.Black
)
Text(
text = status,
fontSize = 9.sp,
color = Color.Gray
)
}
}
}

View File

@ -0,0 +1,157 @@
package com.example.uitugasakhir.ui_otomatis
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.app.ui.theme.Poppins_Bold
import com.example.app.ui.theme.Poppins_Medium
import com.example.uitugasakhir.model.RiwayatItem
import android.util.Log
import com.example.app.ui.theme.Inria_Regular
import androidx.compose.runtime.*
import androidx.compose.ui.res.stringResource
import com.example.uitugasakhir.R
@Composable
fun RiwayatSectionOtomatis(riwayatList: List<RiwayatItem>) {
Column {
val riwayatOtomatis = riwayatList.filter {
it.mode.trim().equals("Otomatis", ignoreCase = true)
}
LaunchedEffect(Unit) {
Log.d("FILTER", "Total = ${riwayatList.size}")
val hasil = riwayatList.filter {
it.mode.trim().equals("Otomatis", true)
}
Log.d("FILTER", "Otomatis = ${hasil.size}")
hasil.forEach {
Log.d("FILTER", "mode=${it.mode}")
}
}
Text(
text = stringResource(R.string.history),
fontSize = 15.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Medium,
color = Color.Black,
modifier = Modifier.padding(horizontal = 28.dp)
)
Spacer(modifier = Modifier.height(5.dp))
Text(
text = stringResource(R.string.manual_watering_activity_log),
fontSize = 12.sp,
fontFamily = Inria_Regular,
fontWeight = FontWeight.Normal,
color = Color.Gray,
modifier = Modifier.padding(horizontal = 28.dp)
)
Spacer(modifier = Modifier.height(15.dp))
Card(
modifier = Modifier
.padding(horizontal = 25.dp)
.padding(bottom = 25.dp)
.size(width = 333.dp, height = 214.dp),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFEDEDED)
),
elevation = CardDefaults.cardElevation(4.dp)
) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(vertical = 8.dp)
) {
items(riwayatOtomatis) { item ->
Log.d("MODE_DEBUG", "MODE = ${item.mode}")
val bgColor = when (item.status) {
"Berjalan" -> Color(0xFFD6F5DD)
"Selesai" -> Color(0xFFFFE0E0)
"Monitoring" -> Color(0xFFFFF3CD)
"Delay" -> Color(0xFFE3F2FD)
else -> Color.LightGray
}
val textColor = when (item.status) {
"Berjalan" -> Color(0xFF2E7D32)
"Selesai" -> Color.Red
"Monitoring" -> Color(0xFFB8860B)
"Delay" -> Color(0xFF1565C0)
else -> Color.DarkGray
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = "${item.waktu} ${item.aksi}",
fontSize = 13.sp,
fontFamily = Poppins_Bold,
color = Color.Black.copy(alpha = 0.8f)
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "${item.keterangan} | Jeda: ${item.jeda} menit",
fontSize = 11.sp,
fontFamily = Poppins_Medium,
color = Color.Gray
)
}
Box(
modifier = Modifier
.background(bgColor, RoundedCornerShape(12.dp))
.padding(horizontal = 10.dp, vertical = 5.dp)
) {
Text(
text = item.status,
fontSize = 10.sp,
fontFamily = Poppins_Medium,
color = textColor
)
}
}
HorizontalDivider(
thickness = 0.8.dp,
color = Color.Gray.copy(alpha = 0.3f)
)
}
}
}
}
}

View File

@ -0,0 +1,34 @@
package com.example.uitugasakhir.utils
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import androidx.core.app.NotificationCompat
fun showNotification(context: Context, title: String, message: String) {
val channelId = "alert_channel"
val manager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// ✅ hanya untuk API 26 ke atas
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
channelId,
"Alert",
NotificationManager.IMPORTANCE_HIGH
)
manager.createNotificationChannel(channel)
}
val builder = NotificationCompat.Builder(context, channelId)
.setSmallIcon(android.R.drawable.ic_dialog_alert)
.setContentTitle(title)
.setContentText(message)
.setPriority(NotificationCompat.PRIORITY_HIGH)
manager.notify(System.currentTimeMillis().toInt(), builder.build())
}

View File

@ -0,0 +1,103 @@
package com.example.uitugasakhir.ViewModel
import android.app.Application
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.AndroidViewModel
import com.example.uitugasakhir.mqtt.MqttManager
import org.json.JSONObject
data class MQTTState(
val pump: String = "OFF",
val mode: String = "Manual",
val alarm: String = "AMAN"
)
class MQTTViewModel(
application: Application
) : AndroidViewModel(application) {
private val mqtt = MqttManager(application)
var state by mutableStateOf(MQTTState())
private set
init {
mqtt.connect(
onMessage = { topic, message ->
Log.d("MQTT_TOPIC", topic)
Log.d("MQTT_MESSAGE", message)
when (topic) {
// =========================
// PUMP REALTIME
// =========================
"smartplant/pump" -> {
state = state.copy(
pump = message
)
}
// =========================
// ALARM REALTIME
// =========================
"smartplant/alarm" -> {
state = state.copy(
alarm = message
)
}
// =========================
// MODE REALTIME
// =========================
"smartplant/mode" -> {
state = state.copy(
mode = message
)
}
}
}
)
}
// =========================
// CONTROL PUMP
// =========================
fun setPump(status: Boolean) {
val msg = if (status) "ON" else "OFF"
mqtt.publish(
"smartplant/manualPump",
msg
)
state = state.copy(
pump = msg
)
}
// =========================
// CONTROL MODE
// =========================
fun setMode(mode: String) {
mqtt.publish(
"smartplant/mode",
mode
)
state = state.copy(
mode = mode
)
}
}

View File

@ -0,0 +1,204 @@
package viewmodel
import android.content.Context
import androidx.compose.runtime.*
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.uitugasakhir.api.RetrofitClient
import com.example.uitugasakhir.model.Sensor
import com.example.uitugasakhir.model.AmbangBatas
import com.example.uitugasakhir.utils.showNotification
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import com.example.uitugasakhir.api.ApiAmbangBatas.getAmbangBatas
class NotificationViewModel : ViewModel() {
// =========================
// DATA
// =========================
var sensor by mutableStateOf<Sensor?>(null)
private set
var ambang by mutableStateOf<AmbangBatas?>(null)
private set
// =========================
// UI STATE
// =========================
var warningMessage by mutableStateOf("Aman")
private set
var isWarningActive by mutableStateOf(false)
private set
private var lastPopupTime = 0L
// =========================
// LOAD DATA LOOP
// =========================
fun loadData(context: Context) {
viewModelScope.launch {
var notifTanah = false
var notifSuhu = false
var notifUdara = false
while (true) {
try {
val sensorResponse = RetrofitClient.api.getSensor()
val ambangResponse = RetrofitClient.api.getAmbangBatas()
val sensorData = Sensor(
kelembaban_udara = sensorResponse.kelembaban_udara.toFloat(),
suhu_udara = sensorResponse.suhu_udara.toFloat(),
kelembaban_tanah = sensorResponse.kelembaban_tanah.toFloat(),
intensitas_hujan = sensorResponse.intensitas_hujan.toFloat()
)
val ambangData = AmbangBatas(
min_tanah = ambangResponse.min_tanah,
max_tanah = ambangResponse.max_tanah,
critical_tanah = ambangResponse.critical_tanah,
min_suhu = ambangResponse.min_suhu,
max_suhu = ambangResponse.max_suhu,
min_udara = ambangResponse.min_udara,
max_udara = ambangResponse.max_udara
)
sensor = sensorData
ambang = ambangData
val warnings = mutableListOf<String>()
// =========================
// TANAH
// =========================
if (sensorData.kelembaban_tanah < ambangData.min_tanah) {
warnings.add("🌱 Tanah terlalu kering")
if (!notifTanah) {
showNotification(
context,
"Tanah Kering",
"Kelembapan tanah di bawah batas minimum"
)
notifTanah = true
}
} else if (sensorData.kelembaban_tanah > ambangData.max_tanah) {
warnings.add("💧 Tanah terlalu basah")
if (!notifTanah) {
showNotification(
context,
"Tanah Basah",
"Kelembapan tanah melebihi batas maksimum"
)
notifTanah = true
}
} else {
notifTanah = false
}
// =========================
// SUHU
// =========================
if (sensorData.suhu_udara < ambangData.min_suhu) {
warnings.add("❄️ Suhu terlalu dingin")
if (!notifSuhu) {
showNotification(
context,
"Suhu Rendah",
"Suhu udara di bawah batas minimum"
)
notifSuhu = true
}
} else if (sensorData.suhu_udara > ambangData.max_suhu) {
warnings.add("🔥 Suhu terlalu tinggi")
if (!notifSuhu) {
showNotification(
context,
"Suhu Tinggi",
"Suhu udara melebihi batas maksimum"
)
notifSuhu = true
}
} else {
notifSuhu = false
}
// =========================
// UDARA
// =========================
if (sensorData.kelembaban_udara < ambangData.min_udara) {
warnings.add("🌬 Udara terlalu kering")
if (!notifUdara) {
showNotification(
context,
"Udara Kering",
"Kelembapan udara di bawah batas minimum"
)
notifUdara = true
}
} else if (sensorData.kelembaban_udara > ambangData.max_udara) {
warnings.add("🌫 Udara terlalu lembab")
if (!notifUdara) {
showNotification(
context,
"Udara Lembab",
"Kelembapan udara melebihi batas maksimum"
)
notifUdara = true
}
} else {
notifUdara = false
}
// =========================
// UPDATE POPUP
// =========================
val now = System.currentTimeMillis()
if (warnings.isEmpty()) {
warningMessage = "Aman"
isWarningActive = false
} else {
warningMessage = warnings.joinToString("\n")
if (now - lastPopupTime > 5000) {
isWarningActive = true
lastPopupTime = now
}
}
} catch (e: Exception) {
e.printStackTrace()
}
delay(5000)
}
}
}
fun clearWarning() {
isWarningActive = false
}
fun updateFromSocket(message: String) {
warningMessage = message
isWarningActive = true
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
android:height="108dp"
android:width="108dp"
android:viewportHeight="108"
android:viewportWidth="108"
xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z"/>
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
</vector>

View File

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 756 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 B

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