feat: add dynamic feature control

This commit is contained in:
Cutiful 2025-05-04 16:28:33 +07:00
parent f05ed9641b
commit db5a346f76
48 changed files with 1061 additions and 68 deletions

View File

@ -1 +1 @@
Agrlink Vocpro Agrilink Vocpro

View File

@ -0,0 +1,123 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>

View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

View File

@ -5,6 +5,9 @@
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
</SelectionState> </SelectionState>
<SelectionState runConfigName="MainActivity">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates> </selectionStates>
</component> </component>
</project> </project>

View File

@ -11,6 +11,7 @@
<set> <set>
<option value="$PROJECT_DIR$" /> <option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" /> <option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/control_feature" />
</set> </set>
</option> </option>
<option name="resolveExternalAnnotations" value="false" /> <option name="resolveExternalAnnotations" value="false" />

View File

@ -2,16 +2,17 @@ plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.compose)
id("com.google.gms.google-services")
} }
android { android {
namespace = "com.syaroful.agrlinkvocpro" namespace = "com.syaroful.agrilinkvocpro"
compileSdk = 34 compileSdk = 35
defaultConfig { defaultConfig {
applicationId = "com.syaroful.agrlinkvocpro" applicationId = "com.syaroful.agrilinkvocpro"
minSdk = 29 minSdk = 29
targetSdk = 34 targetSdk = 35
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "1.0"
@ -37,6 +38,7 @@ android {
buildFeatures { buildFeatures {
compose = true compose = true
} }
dynamicFeatures += setOf(":control_feature")
} }
dependencies { dependencies {
@ -56,4 +58,13 @@ dependencies {
androidTestImplementation(libs.androidx.ui.test.junit4) androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest) debugImplementation(libs.androidx.ui.test.manifest)
// firebase service
implementation(platform(libs.firebase.bom))
implementation(libs.firebase.database)
// viewModel
implementation(libs.androidx.lifecycle.viewmodel.compose)
} }

View File

@ -0,0 +1,30 @@
{
"project_info": {
"project_number": "332219378943",
"firebase_url": "https://agrilink-vocpro-v2-default-rtdb.asia-southeast1.firebasedatabase.app",
"project_id": "agrilink-vocpro-v2",
"storage_bucket": "agrilink-vocpro-v2.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:332219378943:android:7e62537b16d7d1b8f830ca",
"android_client_info": {
"package_name": "com.syaroful.agrilinkvocpro"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyDIEOoCqw8QjrRnqzBdsIN0CDZmCSq52D8"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}

View File

@ -1,4 +1,4 @@
package com.syaroful.agrlinkvocpro package com.syaroful.agrilinkvocpro
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
@ -19,6 +19,6 @@ class ExampleInstrumentedTest {
fun useAppContext() { fun useAppContext() {
// Context of the app under test. // Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.syaroful.agrlinkvocpro", appContext.packageName) assertEquals("com.syaroful.agrilinkvocpro", appContext.packageName)
} }
} }

View File

@ -10,13 +10,13 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.AgrlinkVocpro" android:theme="@style/Theme.AgrilinkVocpro"
tools:targetApi="31"> tools:targetApi="31">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/Theme.AgrlinkVocpro"> android:theme="@style/Theme.AgrilinkVocpro">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

View File

@ -0,0 +1,75 @@
package com.syaroful.agrilinkvocpro
import android.content.res.Configuration
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import com.syaroful.agrilinkvocpro.core.components.AppButton
import com.syaroful.agrilinkvocpro.core.components.AppPasswordField
import com.syaroful.agrilinkvocpro.core.components.AppTextField
import com.syaroful.agrilinkvocpro.ui.pages.ControlScreen
import com.syaroful.agrilinkvocpro.ui.theme.AgrilinkVocproTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
AgrilinkVocproTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
ControlScreen(modifier = Modifier.padding(innerPadding))
}
}
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Column {
Text(
text = "Hello $name!, silahkan Login",
modifier = modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
AppTextField(
hint = "Enter Your Email",
leadingIcon = painterResource(R.drawable.icon_email),
keyboardType = KeyboardType.Email
)
AppPasswordField(
hint = "Enter your password",
keyboardType = KeyboardType.Password
)
AppButton(
label = "Login",
onClick = {}
)
}
}
@Preview(showBackground = true, name = "Light Mode")
@Preview(showBackground = true, name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun GreetingPreview() {
AgrilinkVocproTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
ControlScreen(modifier = Modifier.padding(innerPadding))
}
}
}

View File

@ -0,0 +1,52 @@
package com.syaroful.agrilinkvocpro.core.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.syaroful.agrilinkvocpro.ui.theme.MainGreen
@Composable
fun AppButton(
label: String,
onClick: () -> Unit,
){
Button(
onClick = onClick,
modifier = Modifier.fillMaxWidth().padding(16.dp),
shape = RoundedCornerShape(8.dp),
colors = ButtonDefaults.buttonColors(
containerColor = MainGreen,
contentColor = Color.White
),
contentPadding = PaddingValues(vertical = 16.dp, horizontal = 0.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) { Text(text = label, fontSize = 16.sp) }
}
}
@Preview
@Composable
fun AppButtonPreview(){
AppButton(
label = "Sign in",
onClick = {}
)
}

View File

@ -0,0 +1,90 @@
package com.syaroful.agrilinkvocpro.core.components
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
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.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.syaroful.agrilinkvocpro.R
import com.syaroful.agrilinkvocpro.ui.theme.DarkGrey
import com.syaroful.agrilinkvocpro.ui.theme.LightGrey
import com.syaroful.agrilinkvocpro.ui.theme.MainGreen
@Composable
fun AppPasswordField(
// modifier: Modifier = Modifier,
hint: String,
keyboardType: KeyboardType
) {
var text by remember { mutableStateOf("") }
OutlinedTextField(
value = text,
onValueChange = { text = it },
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
leadingIcon = {
Icon(
painter = painterResource(R.drawable.ic_lock),
tint = LightGrey,
contentDescription = "Password Icon"
)
},
trailingIcon = {
Icon(
painter = painterResource(R.drawable.ic_visible),
contentDescription = "Visible Icon",
tint = DarkGrey
)
},
keyboardOptions = KeyboardOptions(
keyboardType = keyboardType,
imeAction = ImeAction.Done
),
shape = RoundedCornerShape(8.dp),
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
focusedIndicatorColor = MainGreen,
unfocusedIndicatorColor = LightGrey
),
singleLine = true,
placeholder = {
Text(
hint,
style = TextStyle(
color = LightGrey,
fontSize = 14.sp,
)
)
}
)
}
@Preview
@Composable
fun AppPasswordPreview() {
AppPasswordField(
hint = "Enter your password",
keyboardType = KeyboardType.Password
)
}

View File

@ -0,0 +1,82 @@
package com.syaroful.agrilinkvocpro.core.components
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.syaroful.agrilinkvocpro.R
import com.syaroful.agrilinkvocpro.ui.theme.LightGrey
import com.syaroful.agrilinkvocpro.ui.theme.MainGreen
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun AppTextField(
// modifier: Modifier = Modifier,
leadingIcon: Painter? = null,
hint: String,
keyboardType: KeyboardType
) {
var text by remember { mutableStateOf("") }
OutlinedTextField(
value = text,
onValueChange = { text = it },
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.border(BorderStroke(1.dp, LightGrey), RoundedCornerShape(8.dp)),
leadingIcon = {
if (leadingIcon != null) {
Icon(
painter = leadingIcon,
contentDescription = "Email Icon",
tint = LightGrey
)
}
},
keyboardOptions = KeyboardOptions(
keyboardType = keyboardType,
imeAction = ImeAction.Done
),
shape = RoundedCornerShape(8.dp),
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
focusedIndicatorColor = MainGreen, // Warna stroke saat TextField aktif
unfocusedIndicatorColor = LightGrey // Warna stroke saat TextField tidak aktif
),
singleLine = true,
placeholder = {
Text(
hint,
style = textTheme.bodyMedium
)
}
)
}
@OptIn(ExperimentalFoundationApi::class)
@Preview
@Composable
fun TextFieldPreview() {
AppTextField(
hint = "Enter Your Email",
leadingIcon = painterResource(R.drawable.icon_email),
keyboardType = KeyboardType.Email
)
}

View File

@ -0,0 +1,63 @@
package com.syaroful.agrilinkvocpro.core.components
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedButton
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.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.syaroful.agrilinkvocpro.R
import com.syaroful.agrilinkvocpro.ui.theme.MainGreen
@Composable
fun MenuItemButton(
label: String,
icon: Painter
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
OutlinedButton(
onClick = {},
modifier = Modifier.size(48.dp),
shape = CircleShape,
border = BorderStroke(0.dp, Color.Transparent),
contentPadding = PaddingValues(0.dp),
colors = ButtonDefaults.outlinedButtonColors(containerColor = MainGreen.copy(alpha = 0.1f))
) {
Icon(
painter = icon,
modifier = Modifier.size(28.dp),
contentDescription = "icon",
tint = MainGreen,
)
}
Spacer(modifier = Modifier.height(16.dp))
Text(
label,
textAlign = TextAlign.Center,
fontSize = 12.sp,
lineHeight = 12.sp
)
}
}
@Preview
@Composable
fun MenuItemPreview(){
MenuItemButton(label = "Kontrol\nAktuator",icon = painterResource(id = R.drawable.control_actuator_icon))
}

View File

@ -0,0 +1,79 @@
package com.syaroful.agrilinkvocpro.core.components
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import com.syaroful.agrilinkvocpro.ui.theme.LightGrey
val textTheme = Typography(
displayLarge = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 57.sp,
lineHeight = 64.sp,
letterSpacing = (-0.25).sp,
),
displayMedium = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 45.sp,
lineHeight = 52.sp,
letterSpacing = 0.sp,
),
displaySmall = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 36.sp,
lineHeight = 44.sp,
letterSpacing = 0.sp,
),
headlineLarge = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 32.sp,
lineHeight = 40.sp,
letterSpacing = 0.sp,
),
headlineMedium = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 28.sp,
lineHeight = 36.sp,
letterSpacing = 0.sp,
),
headlineSmall = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 24.sp,
lineHeight = 32.sp,
letterSpacing = 0.sp,
),
titleLarge = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp,
),
titleMedium = TextStyle(
fontWeight = FontWeight.W400,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.1.sp,
),
titleSmall = TextStyle(
fontWeight = FontWeight.W400,
fontSize = 15.sp,
lineHeight = 20.sp,
letterSpacing = 0.1.sp,
),
bodyLarge = TextStyle(
fontWeight = FontWeight.W500,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp,
),
bodyMedium = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 14.sp,
lineHeight = 24.sp,
color = LightGrey,
letterSpacing = 0.5.sp,
),
)

View File

@ -0,0 +1,53 @@
package com.syaroful.agrilinkvocpro.ui.pages
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.syaroful.agrilinkvocpro.viewModel.ControlViewModel
@Composable
fun ControlScreen(
modifier: Modifier,
viewModel: ControlViewModel = viewModel()
) { val relayState by viewModel.relayState.collectAsState()
Column (
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "Kontrol Relay", style = MaterialTheme.typography.headlineMedium)
Spacer(modifier = Modifier.height(24.dp))
Switch(
checked = relayState,
onCheckedChange = { isChecked ->
viewModel.setRelayState(isChecked)
}
)
}
}
@Preview(showBackground = true)
@Composable
fun ControlScreenPreview() {
ControlScreen(modifier = Modifier)
}

View File

@ -0,0 +1,107 @@
package com.syaroful.agrilinkvocpro.ui.pages
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.Image
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.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Person
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.syaroful.agrilinkvocpro.R
import com.syaroful.agrilinkvocpro.core.components.MenuItemButton
import com.syaroful.agrilinkvocpro.core.components.textTheme
import com.syaroful.agrilinkvocpro.ui.theme.AgrilinkVocproTheme
import com.syaroful.agrilinkvocpro.ui.theme.MainGreen
@Composable
fun HomeScreen() {
AgrilinkVocproTheme {
Scaffold { padding ->
Column(
modifier = Modifier
.padding(padding)
.fillMaxWidth()
) {
Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth()) {
IconButton(
onClick = {},
modifier = Modifier.align(Alignment.CenterVertically)
) {
Icon(
imageVector = Icons.Default.Person,
contentDescription = "profile",
tint = MainGreen
)
}
}
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(20.dp)
.fillMaxWidth()
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(text = "2 Komoditas", color = MainGreen, style = textTheme.bodyMedium)
Spacer(modifier = Modifier.height(24.dp))
Text(text = "Green House Bumiaji", style = textTheme.bodyLarge)
Text(
text = "Jl. Kopral Kasdi 2, Bulukerto, Kec. Bumiaji, Kota Batu, Jawa Timur 65334 ",
style = textTheme.bodyMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
Spacer(modifier = Modifier.width(24.dp))
Image(
painter = painterResource(id = R.drawable.green_house_image),
contentDescription = "profile",
modifier = Modifier
.size(90.dp)
.clip(RoundedCornerShape(10.dp))
)
}
Spacer(modifier = Modifier.height(20.dp))
Row(modifier = Modifier.padding(horizontal = 20.dp).fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
MenuItemButton(label = "Kontrol\nAktuator",icon = painterResource(id = R.drawable.control_actuator_icon))
MenuItemButton(label = "Resep\nPertumbuhan",icon = painterResource(id = R.drawable.growth_recipe_icon))
MenuItemButton(label = "Harga\nKomoditas",icon = painterResource(id = R.drawable.commodity_price_prediction_icon))
MenuItemButton(label = "Deteksi\nPenyakit",icon = painterResource(id = R.drawable.plant_disease_detection_icon))
}
}
}
}
}
@Preview(showBackground = true, name = "Light Mode")
@Preview(showBackground = true, name = "Dark Mode", uiMode = UI_MODE_NIGHT_YES)
@Composable
fun HomeScreenPreview() {
HomeScreen()
}

View File

@ -0,0 +1,101 @@
package com.syaroful.agrilinkvocpro.ui.pages
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Arrangement
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.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.syaroful.agrilinkvocpro.R
import com.syaroful.agrilinkvocpro.core.components.AppButton
import com.syaroful.agrilinkvocpro.core.components.AppPasswordField
import com.syaroful.agrilinkvocpro.core.components.AppTextField
import com.syaroful.agrilinkvocpro.core.components.textTheme
import com.syaroful.agrilinkvocpro.ui.theme.AgrilinkVocproTheme
import com.syaroful.agrilinkvocpro.ui.theme.DarkGrey
import com.syaroful.agrilinkvocpro.ui.theme.MainGreen
@Composable
fun LoginScreen() {
Scaffold(modifier = Modifier.fillMaxSize()) { paddingValues ->
Column(
modifier = Modifier
.fillMaxWidth()
.padding(paddingValues)
) {
Image(
painter = painterResource(id = R.drawable.greenhouse_banner),
contentDescription = "Banner",
modifier = Modifier
.height(180.dp)
.align(Alignment.CenterHorizontally),
contentScale = ContentScale.Crop
)
Spacer(modifier = Modifier.height(16.dp))
Text(
modifier = Modifier.fillMaxWidth(),
text = "Login", style = textTheme.titleMedium, textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(16.dp))
Text(
modifier = Modifier.fillMaxWidth(),
text = "Halo! yuk masuk ke dalam akunmu",
style = textTheme.titleSmall.copy(color = DarkGrey),
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(16.dp))
AppTextField(
hint = "Username", keyboardType = KeyboardType.Email, leadingIcon = painterResource(
R.drawable.icon_email
)
)
AppPasswordField(
hint = "Password",
keyboardType = KeyboardType.Password
)
AppButton(
label = "Login"
) { }
Spacer(modifier = Modifier.height(16.dp))
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
Text(
text = "Belum punya akun? ",
style = textTheme.titleSmall.copy(color = DarkGrey),
textAlign = TextAlign.Center
)
Text(
text = "Daftar disini",
style = textTheme.titleSmall.copy(color = MainGreen),
textAlign = TextAlign.Center
)
}
}
}
}
@Preview(showBackground = true, name = "Light Mode")
@Preview(showBackground = true, name = "Dark Mode", uiMode = UI_MODE_NIGHT_YES)
@Composable
fun LoginScreenPreview() {
AgrilinkVocproTheme {
LoginScreen()
}
}

View File

@ -1,4 +1,4 @@
package com.syaroful.agrlinkvocpro.ui.theme package com.syaroful.agrilinkvocpro.ui.theme
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@ -9,3 +9,8 @@ val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4) val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71) val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260) val Pink40 = Color(0xFF7D5260)
val LightGrey = Color(0xFFAAB3D0)
val DarkGrey = Color(0xFF6C707E)
val MainGreen = Color(0xFF179678)

View File

@ -1,6 +1,5 @@
package com.syaroful.agrlinkvocpro.ui.theme package com.syaroful.agrilinkvocpro.ui.theme
import android.app.Activity
import android.os.Build import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -34,7 +33,7 @@ private val LightColorScheme = lightColorScheme(
) )
@Composable @Composable
fun AgrlinkVocproTheme( fun AgrilinkVocproTheme(
darkTheme: Boolean = isSystemInDarkTheme(), darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+ // Dynamic color is available on Android 12+
dynamicColor: Boolean = true, dynamicColor: Boolean = true,

View File

@ -1,4 +1,4 @@
package com.syaroful.agrlinkvocpro.ui.theme package com.syaroful.agrilinkvocpro.ui.theme
import androidx.compose.material3.Typography import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle

View File

@ -0,0 +1,58 @@
package com.syaroful.agrilinkvocpro.viewModel
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.firebase.database.DataSnapshot
import com.google.firebase.database.DatabaseError
import com.google.firebase.database.ValueEventListener
import com.google.firebase.database.ktx.database
import com.google.firebase.ktx.Firebase
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
private const val TAG = "ControlViewModel"
class ControlViewModel : ViewModel() {
private val _relayState = MutableStateFlow(false)
val relayState: StateFlow<Boolean> = _relayState
private val database = Firebase.database
private val relayRef = database.getReference("relay1/state")
init {
observeRelayState()
}
private fun observeRelayState() {
relayRef.get()
.addOnSuccessListener { snapshot ->
val value = snapshot.getValue(Int::class.java) ?: 0
_relayState.value = (value == 1)
}.addOnFailureListener{
Log.e(TAG, "Failed to fetch initial relay state", it)
}
relayRef.addValueEventListener(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
val value = snapshot.getValue(Int::class.java) ?: 0
_relayState.value = (value == 1)
}
override fun onCancelled(error: DatabaseError) {
Log.e(TAG, "Failed to observe relay state", error.toException())
}
})
}
fun setRelayState(isOn: Boolean) {
viewModelScope.launch {
relayRef.setValue(if (isOn) 1 else 0)
.addOnFailureListener {
Log.e(TAG, "Failed to set relay state", it)
}
}
}
}

View File

@ -1,47 +0,0 @@
package com.syaroful.agrlinkvocpro
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.syaroful.agrlinkvocpro.ui.theme.AgrlinkVocproTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
AgrlinkVocproTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}
}
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
AgrlinkVocproTheme {
Greeting("Android")
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 732 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 909 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 638 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 975 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 B

View File

@ -1,3 +1,4 @@
<resources> <resources>
<string name="app_name">Agrlink Vocpro</string> <string name="app_name">Agrilink Vocpro</string>
<string name="title_control_feature">Control Module</string>
</resources> </resources>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="Theme.AgrlinkVocpro" parent="android:Theme.Material.Light.NoActionBar" /> <style name="Theme.AgrilinkVocpro" parent="android:Theme.Material.Light.NoActionBar" />
</resources> </resources>

View File

@ -1,4 +1,4 @@
package com.syaroful.agrlinkvocpro package com.syaroful.agrilinkvocpro
import org.junit.Test import org.junit.Test

View File

@ -3,4 +3,6 @@ plugins {
alias(libs.plugins.android.application) apply false alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false alias(libs.plugins.kotlin.compose) apply false
id("com.google.gms.google-services") version "4.4.2" apply false
alias(libs.plugins.android.dynamic.feature) apply false
} }

View File

@ -0,0 +1 @@
/build

View File

@ -0,0 +1,38 @@
plugins {
alias(libs.plugins.android.dynamic.feature)
alias(libs.plugins.kotlin.android)
}
android {
namespace = "com.syaroful.control_feature"
compileSdk = 35
defaultConfig {
minSdk = 29
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
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"
}
}
dependencies {
implementation(project(":app"))
implementation(libs.androidx.core.ktx)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
}

View File

@ -0,0 +1,24 @@
package com.syaroful.control_feature
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.syaroful.control_feature", appContext.packageName)
}
}

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:dist="http://schemas.android.com/apk/distribution">
<dist:module
dist:instant="false"
dist:title="@string/title_control_feature">
<dist:delivery>
<dist:on-demand />
</dist:delivery>
<dist:fusing dist:include="true" />
</dist:module>
</manifest>

View File

@ -0,0 +1,17 @@
package com.syaroful.control_feature
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

View File

@ -1,5 +1,6 @@
[versions] [versions]
agp = "8.7.3" agp = "8.8.0"
firebaseBom = "33.13.0"
kotlin = "2.0.0" kotlin = "2.0.0"
coreKtx = "1.15.0" coreKtx = "1.15.0"
junit = "4.13.2" junit = "4.13.2"
@ -8,9 +9,13 @@ espressoCore = "3.6.1"
lifecycleRuntimeKtx = "2.8.7" lifecycleRuntimeKtx = "2.8.7"
activityCompose = "1.9.3" activityCompose = "1.9.3"
composeBom = "2024.04.01" composeBom = "2024.04.01"
lifecycleViewmodelCompose = "2.8.7"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" }
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" }
firebase-database = { module = "com.google.firebase:firebase-database" }
junit = { group = "junit", name = "junit", version.ref = "junit" } junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
@ -29,4 +34,5 @@ androidx-material3 = { group = "androidx.compose.material3", name = "material3"
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
android-dynamic-feature = { id = "com.android.dynamic-feature", version.ref = "agp" }

View File

@ -1,6 +1,6 @@
#Wed Jan 08 14:02:58 WIB 2025 #Wed Jan 08 14:02:58 WIB 2025
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@ -19,5 +19,6 @@ dependencyResolutionManagement {
} }
} }
rootProject.name = "Agrlink Vocpro" rootProject.name = "Agrilink Vocpro"
include(":app") include(":app")
include(":control_feature")