diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml new file mode 100644 index 0000000..91f9558 --- /dev/null +++ b/.idea/deviceManager.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..f0c6ad0 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,50 @@ + + + + \ No newline at end of file diff --git a/.idea/markdown.xml b/.idea/markdown.xml new file mode 100644 index 0000000..c61ea33 --- /dev/null +++ b/.idea/markdown.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4619836..d058e92 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ + @@ -19,17 +20,27 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.SistemAkademik"> - - - - + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/id/ac/ubharajaya/sistemakademik/Absensi.kt b/app/src/main/java/id/ac/ubharajaya/sistemakademik/Absensi.kt new file mode 100644 index 0000000..bcd97fa --- /dev/null +++ b/app/src/main/java/id/ac/ubharajaya/sistemakademik/Absensi.kt @@ -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 + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/id/ac/ubharajaya/sistemakademik/LoginActivity.kt b/app/src/main/java/id/ac/ubharajaya/sistemakademik/LoginActivity.kt new file mode 100644 index 0000000..dda7d73 --- /dev/null +++ b/app/src/main/java/id/ac/ubharajaya/sistemakademik/LoginActivity.kt @@ -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 + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt b/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt index c774502..92642e4 100644 --- a/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt +++ b/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt @@ -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(null) + private set + + var longitude by mutableStateOf(null) + private set + + var foto by mutableStateOf(null) + private set + + var absensiList = mutableStateListOf() + + var waktuAbsensi by mutableStateOf(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(null) } - var longitude by remember { mutableStateOf(null) } - var foto by remember { mutableStateOf(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)) + ) + } + } + } + } } } } diff --git a/app/src/main/java/id/ac/ubharajaya/sistemakademik/SplashActivity.kt b/app/src/main/java/id/ac/ubharajaya/sistemakademik/SplashActivity.kt new file mode 100644 index 0000000..94c32ea --- /dev/null +++ b/app/src/main/java/id/ac/ubharajaya/sistemakademik/SplashActivity.kt @@ -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) + ) + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/logo.png b/app/src/main/res/drawable/logo.png new file mode 100644 index 0000000..9064055 Binary files /dev/null and b/app/src/main/res/drawable/logo.png differ diff --git a/app/src/main/res/logo.png b/app/src/main/res/logo.png new file mode 100644 index 0000000..e51154f Binary files /dev/null and b/app/src/main/res/logo.png differ