Initial commit

This commit is contained in:
putraa-afk 2026-01-14 20:45:44 +07:00
parent ed435ffbc1
commit 462f9c315a
10 changed files with 665 additions and 189 deletions

13
.idea/deviceManager.xml generated Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceTable">
<option name="columnSorters">
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
</list>
</option>
</component>
</project>

View File

@ -0,0 +1,50 @@
<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="PreviewParameterProviderOnFirstParameter" 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>

8
.idea/markdown.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MarkdownSettings">
<option name="previewPanelProviderInfo">
<ProviderInfo name="Compose (experimental)" className="com.intellij.markdown.compose.preview.ComposePanelProvider" />
</option>
</component>
</project>

View File

@ -6,6 +6,7 @@
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
@ -19,17 +20,27 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.SistemAkademik">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.SistemAkademik">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<!-- 🔥 SPLASH SCREEN (LAUNCHER) -->
<activity
android:name=".SplashActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- LOGIN -->
<activity
android:name=".LoginActivity"
android:exported="true"/>
<!-- MAIN -->
<activity
android:name=".MainActivity"
android:exported="true"/>
</application>
</manifest>

View File

@ -0,0 +1,44 @@
package id.ac.ubharajaya.sistemakademik
import android.graphics.Bitmap
import android.util.Base64
import org.json.JSONObject
import java.io.ByteArrayOutputStream
/**
* Data class untuk menyimpan informasi absensi mahasiswa
*/
data class Absensi(
val npm: String,
val nama: String,
val latitude: Double,
val longitude: Double,
val waktu: String,
val foto: Bitmap? // ⬅️ HARUS nullable
) {
/**
* Convert objek Absensi menjadi JSON
*/
fun toJson(): JSONObject {
val json = JSONObject()
json.put("npm", npm)
json.put("nama", nama)
json.put("latitude", latitude)
json.put("longitude", longitude)
json.put("waktu", waktu)
json.put("timestamp", System.currentTimeMillis())
json.put("foto_base64", foto?.let { bitmapToBase64(it) })
return json
}
companion object {
fun bitmapToBase64(bitmap: Bitmap): String {
val outputStream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
return Base64.encodeToString(
outputStream.toByteArray(),
Base64.NO_WRAP
)
}
}
}

View File

@ -0,0 +1,182 @@
package id.ac.ubharajaya.sistemakademik
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.*
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.Lock
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.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
class LoginActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val typography = Typography(
titleLarge = MaterialTheme.typography.titleLarge.copy(
fontFamily = FontFamily.SansSerif,
fontWeight = FontWeight.Bold,
fontSize = 22.sp
)
)
MaterialTheme(
typography = typography,
colorScheme = if (isSystemInDarkTheme())
darkColorScheme()
else
lightColorScheme()
) {
LoginScreen()
}
}
}
}
@Composable
fun LoginScreen() {
val context = LocalContext.current
var npm by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var showCard by remember { mutableStateOf(false) }
LaunchedEffect(Unit) { showCard = true }
val buttonGradient = Brush.horizontalGradient(
listOf(Color(0xFF1565C0), Color(0xFF42A5F5))
)
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black) // 🔥 BACKGROUND HITAM
.padding(24.dp),
contentAlignment = Alignment.Center
) {
AnimatedVisibility(
visible = showCard,
enter = fadeIn() + scaleIn(initialScale = 0.9f)
) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(22.dp),
elevation = CardDefaults.cardElevation(14.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
)
) {
Column(
modifier = Modifier.padding(28.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(id = R.drawable.logo),
contentDescription = "Logo",
modifier = Modifier
.size(120.dp)
.clip(CircleShape)
)
Spacer(modifier = Modifier.height(20.dp))
Text(
"Absensi PPB",
style = MaterialTheme.typography.titleLarge
)
Text(
"Login Mahasiswa",
fontSize = 14.sp,
color = Color.Gray
)
Spacer(modifier = Modifier.height(28.dp))
OutlinedTextField(
value = npm,
onValueChange = { npm = it },
label = { Text("NPM") },
leadingIcon = {
Icon(Icons.Default.AccountCircle, contentDescription = null)
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = password,
onValueChange = { password = it },
label = { Text("Password") },
leadingIcon = {
Icon(Icons.Default.Lock, contentDescription = null)
},
visualTransformation = PasswordVisualTransformation(),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(32.dp))
Button(
onClick = {
if (npm == "202310715307" && password == "yoo23") {
context.startActivity(
Intent(context, MainActivity::class.java)
)
} else {
Toast.makeText(
context,
"NPM atau password salah",
Toast.LENGTH_SHORT
).show()
}
},
colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent),
contentPadding = PaddingValues(),
modifier = Modifier
.fillMaxWidth()
.height(48.dp)
.background(
brush = buttonGradient,
shape = RoundedCornerShape(14.dp)
)
) {
Text(
"Login",
color = Color.White,
fontWeight = FontWeight.Bold
)
}
}
}
}
}
}

View File

@ -3,110 +3,203 @@ package id.ac.ubharajaya.sistemakademik
import android.Manifest
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.util.Base64
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.Image
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.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.android.gms.location.LocationServices
import id.ac.ubharajaya.sistemakademik.ui.theme.SistemAkademikTheme
import org.json.JSONObject
import java.io.ByteArrayOutputStream
import java.net.HttpURLConnection
import java.net.URL
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.*
import kotlin.concurrent.thread
/* ================= UTIL ================= */
/* ================== KONFIG LOCKOUT ================== */
const val LOCKOUT_DURATION = 10 * 60 * 1000L // 1 menit
fun bitmapToBase64(bitmap: Bitmap): String {
val outputStream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
return Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP)
}
/* ================== REPOSITORY ================== */
class AbsensiRepository {
fun kirimAbsensi(absensi: Absensi, onResult: (Boolean) -> Unit) {
thread {
try {
val url = java.net.URL(
"https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254"
)
val conn = url.openConnection() as java.net.HttpURLConnection
conn.requestMethod = "POST"
conn.setRequestProperty("Content-Type", "application/json")
conn.doOutput = true
fun kirimKeN8n(
context: ComponentActivity,
latitude: Double,
longitude: Double,
foto: Bitmap
) {
thread {
try {
val url = URL("https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254")
// test URL val url = URL("https://n8n.lab.ubharajaya.ac.id/webhook-test/23c6993d-1792-48fb-ad1c-ffc78a3e6254")
val conn = url.openConnection() as HttpURLConnection
conn.outputStream.use {
it.write(absensi.toJson().toString().toByteArray())
}
conn.requestMethod = "POST"
conn.setRequestProperty("Content-Type", "application/json")
conn.doOutput = true
val json = JSONObject().apply {
put("npm", "12345")
put("nama","Arif R D")
put("latitude", latitude)
put("longitude", longitude)
put("timestamp", System.currentTimeMillis())
put("foto_base64", bitmapToBase64(foto))
}
conn.outputStream.use {
it.write(json.toString().toByteArray())
}
val responseCode = conn.responseCode
context.runOnUiThread {
Toast.makeText(
context,
if (responseCode == 200)
"Absensi diterima server"
else
"Absensi ditolak server",
Toast.LENGTH_SHORT
).show()
}
conn.disconnect()
} catch (_: Exception) {
context.runOnUiThread {
Toast.makeText(
context,
"Gagal kirim ke server",
Toast.LENGTH_SHORT
).show()
onResult(conn.responseCode == 200)
conn.disconnect()
} catch (e: Exception) {
onResult(false)
}
}
}
}
/* ================= ACTIVITY ================= */
/* ================== VIEWMODEL ================== */
class AbsensiViewModel(
private val repo: AbsensiRepository,
private val activity: ComponentActivity
) : ViewModel() {
var lokasi by mutableStateOf("Koordinat: -")
private set
var latitude by mutableStateOf<Double?>(null)
private set
var longitude by mutableStateOf<Double?>(null)
private set
var foto by mutableStateOf<Bitmap?>(null)
private set
var absensiList = mutableStateListOf<Absensi>()
var waktuAbsensi by mutableStateOf<String?>(null)
private set
var isLoading by mutableStateOf(false)
private set
var sisaLockout by mutableStateOf(0L)
private set
init {
startLockoutCountdown()
}
fun updateLokasi(lat: Double, lon: Double) {
latitude = lat
longitude = lon
lokasi = "Lat: $lat\nLon: $lon"
}
fun updateFoto(bitmap: Bitmap) {
foto = bitmap
}
private fun getLastAbsensiTime(): Long {
val prefs = activity.getSharedPreferences("absensi_prefs", Activity.MODE_PRIVATE)
return prefs.getLong("last_absensi_time", 0L)
}
private fun simpanWaktuAbsensi() {
val prefs = activity.getSharedPreferences("absensi_prefs", Activity.MODE_PRIVATE)
prefs.edit().putLong("last_absensi_time", System.currentTimeMillis()).apply()
}
private fun startLockoutCountdown() {
viewModelScope.launch {
while (true) {
val diff = System.currentTimeMillis() - getLastAbsensiTime()
sisaLockout =
if (diff < LOCKOUT_DURATION) LOCKOUT_DURATION - diff else 0L
delay(1000)
}
}
}
fun kirimAbsensi() {
if (sisaLockout > 0) {
Toast.makeText(
activity,
"Sudah absen, tunggu ${(sisaLockout / 1000) / 60} menit",
Toast.LENGTH_LONG
).show()
return
}
if (latitude == null || longitude == null || foto == null) {
Toast.makeText(activity, "Lokasi atau foto belum lengkap", Toast.LENGTH_SHORT).show()
return
}
val waktu = SimpleDateFormat(
"dd MMM yyyy HH:mm:ss",
Locale.getDefault()
).format(System.currentTimeMillis())
val absensi = Absensi(
npm = "202310715307",
nama = "Satrio Putra Wardani",
latitude = latitude!!,
longitude = longitude!!,
waktu = waktu,
foto = foto
)
isLoading = true
repo.kirimAbsensi(absensi) { success ->
viewModelScope.launch {
isLoading = false
if (success) {
absensiList.add(absensi)
waktuAbsensi = waktu
simpanWaktuAbsensi()
Toast.makeText(activity, "Absensi berhasil", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(activity, "Gagal kirim absensi", Toast.LENGTH_SHORT).show()
}
}
}
}
}
/* ================== MAIN ACTIVITY ================== */
@OptIn(ExperimentalMaterial3Api::class)
class MainActivity : ComponentActivity() {
private lateinit var viewModel: AbsensiViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
viewModel = AbsensiViewModel(AbsensiRepository(), this)
setContent {
SistemAkademikTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Scaffold(
topBar = { TopBarLogout(this) }
) { padding ->
AbsensiScreen(
modifier = Modifier.padding(innerPadding),
activity = this
modifier = Modifier.padding(padding),
viewModel = viewModel
)
}
}
@ -114,161 +207,174 @@ class MainActivity : ComponentActivity() {
}
}
/* ================= UI ================= */
/* ================== TOP BAR ================== */
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AbsensiScreen(
modifier: Modifier = Modifier,
activity: ComponentActivity
) {
fun TopBarLogout(activity: ComponentActivity) {
val context = LocalContext.current
TopAppBar(
title = {
Text(
" Sistem Akademik ",
style = MaterialTheme.typography.titleLarge.copy(
fontWeight = FontWeight.SemiBold,
color = Color.White
)
)
},
actions = {
TextButton(onClick = {
val intent = Intent(context, LoginActivity::class.java)
intent.flags =
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
context.startActivity(intent)
}) {
Text(
"Logout",
style = MaterialTheme.typography.labelLarge,
color = Color.Red
)
}
}
)
}
/* ================== UI ================== */
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AbsensiScreen(modifier: Modifier, viewModel: AbsensiViewModel) {
val context = LocalContext.current
var lokasi by remember { mutableStateOf("Koordinat: -") }
var latitude by remember { mutableStateOf<Double?>(null) }
var longitude by remember { mutableStateOf<Double?>(null) }
var foto by remember { mutableStateOf<Bitmap?>(null) }
val fusedLocationClient =
LocationServices.getFusedLocationProviderClient(context)
/* ===== Permission Lokasi ===== */
val locationPermissionLauncher =
rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (granted) {
if (
ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
) {
fusedLocationClient.lastLocation
.addOnSuccessListener { location ->
if (location != null) {
latitude = location.latitude
longitude = location.longitude
lokasi =
"Lat: ${location.latitude}\nLon: ${location.longitude}"
} else {
lokasi = "Lokasi tidak tersedia"
}
LocationServices.getFusedLocationProviderClient(context)
.lastLocation.addOnSuccessListener {
it?.let { loc ->
viewModel.updateLokasi(loc.latitude, loc.longitude)
}
.addOnFailureListener {
lokasi = "Gagal mengambil lokasi"
}
}
} else {
Toast.makeText(
context,
"Izin lokasi ditolak",
Toast.LENGTH_SHORT
).show()
}
}
}
/* ===== Kamera ===== */
val cameraLauncher =
rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
val bitmap =
result.data?.extras?.getParcelable("data", Bitmap::class.java)
if (bitmap != null) {
foto = bitmap
Toast.makeText(
context,
"Foto berhasil diambil",
Toast.LENGTH_SHORT
).show()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
it.data?.extras?.getParcelable("data", Bitmap::class.java)
else it.data?.extras?.getParcelable("data")
bitmap?.let { bmp -> viewModel.updateFoto(bmp) }
}
}
val cameraPermissionLauncher =
rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
if (granted) {
val intent =
Intent(MediaStore.ACTION_IMAGE_CAPTURE)
cameraLauncher.launch(intent)
} else {
Toast.makeText(
context,
"Izin kamera ditolak",
Toast.LENGTH_SHORT
).show()
}
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) {
if (it) cameraLauncher.launch(Intent(MediaStore.ACTION_IMAGE_CAPTURE))
}
/* ===== Request Awal ===== */
LaunchedEffect(Unit) {
locationPermissionLauncher.launch(
Manifest.permission.ACCESS_FINE_LOCATION
)
locationPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
}
/* ===== UI ===== */
Column(
modifier = modifier
.fillMaxSize()
.padding(24.dp),
verticalArrangement = Arrangement.Center
.background(Color.White)
.padding(24.dp)
) {
Text(
text = "Absensi Akademik",
style = MaterialTheme.typography.titleLarge
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.headlineSmall.copy(
fontWeight = FontWeight.Bold,
color = Color.Black
)
)
Spacer(modifier = Modifier.height(16.dp))
Spacer(Modifier.height(16.dp))
Text(text = lokasi)
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(6.dp)
) {
Column(Modifier.padding(16.dp)) {
Text(
"Lokasi:",
style = MaterialTheme.typography.bodyLarge.copy(
fontWeight = FontWeight.SemiBold,
color = Color.White
)
)
Text(
viewModel.lokasi,
style = MaterialTheme.typography.bodyMedium.copy(
color = Color.White
)
)
}
}
Spacer(modifier = Modifier.height(16.dp))
Spacer(Modifier.height(16.dp))
Button(
onClick = {
cameraPermissionLauncher.launch(
Manifest.permission.CAMERA
)
},
onClick = { cameraPermissionLauncher.launch(Manifest.permission.CAMERA) },
modifier = Modifier.fillMaxWidth()
) {
Text("Ambil Foto")
}
Spacer(modifier = Modifier.height(12.dp))
Spacer(Modifier.height(12.dp))
Button(
onClick = {
if (latitude != null && longitude != null && foto != null) {
kirimKeN8n(
activity,
latitude!!,
longitude!!,
foto!!
)
} else {
Toast.makeText(
context,
"Lokasi atau foto belum lengkap",
Toast.LENGTH_SHORT
).show()
}
},
modifier = Modifier.fillMaxWidth()
onClick = { viewModel.kirimAbsensi() },
modifier = Modifier.fillMaxWidth(),
enabled = !viewModel.isLoading
) {
Text("Kirim Absensi")
if (viewModel.isLoading)
CircularProgressIndicator(
modifier = Modifier.size(22.dp),
strokeWidth = 2.dp
)
else Text("Kirim Absensi")
}
Spacer(Modifier.height(24.dp))
LazyColumn {
items(viewModel.absensiList) { item ->
Card(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp)
.animateContentSize()
) {
Column(Modifier.padding(16.dp)) {
Text(
item.nama,
style = MaterialTheme.typography.bodyLarge.copy(
fontWeight = FontWeight.Bold,
color = Color.White
)
)
Text(item.npm, color = Color.White)
Text(item.waktu, color = Color.White)
item.foto?.let {
Image(
bitmap = it.asImageBitmap(),
contentDescription = null,
modifier = Modifier
.fillMaxWidth()
.height(150.dp)
.clip(RoundedCornerShape(12.dp))
)
}
}
}
}
}
}
}

View File

@ -0,0 +1,62 @@
package id.ac.ubharajaya.sistemakademik
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
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.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
class SplashActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
SplashScreen {
startActivity(Intent(this, LoginActivity::class.java))
finish()
}
}
}
}
@Composable
fun SplashScreen(onFinish: () -> Unit) {
LaunchedEffect(Unit) {
delay(2000) // 2 detik
onFinish()
}
// 🔥 GRADIENT ABU-ABU MODERN
val gradient = Brush.verticalGradient(
listOf(
Color(0xFF2C2C2C), // Dark Grey
Color(0xFF757575) // Light Grey
)
)
Box(
modifier = Modifier
.fillMaxSize()
.background(gradient),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(id = R.drawable.logo),
contentDescription = "Logo",
modifier = Modifier
.size(140.dp)
.clip(CircleShape)
)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
app/src/main/res/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB