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/README.md b/README.md index 9871f13..c19821e 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,20 @@ # 📱 Aplikasi Absensi Akademik Berbasis Koordinat dan Foto (Mobile) ## 📌 Deskripsi Proyek -Proyek ini merupakan **Tugas Project Akhir Mata Kuliah Pemrograman Mobile** yang bertujuan untuk membangun **aplikasi akademik berbasis mobile** dengan fokus pada **fitur absensi menggunakan data koordinat (GPS) dan pengambilan foto mahasiswa**. +Proyek ini merupakan **Tugas Akhir EAS (Evaluasi Akhir Semester)** yang dikembangkan oleh: + +- **Nama** : Faris Naufal Priatna +- **NPM** : 202310715123 + +Tujuan proyek ini adalah membangun **aplikasi akademik berbasis mobile** dengan fokus pada **fitur absensi menggunakan data koordinat (GPS) dan pengambilan foto mahasiswa**. Aplikasi ini dirancang untuk meningkatkan **validitas kehadiran mahasiswa**, dengan memastikan bahwa absensi hanya dapat dilakukan apabila mahasiswa: + 1. Berada pada **lokasi yang telah ditentukan**, dan 2. Melakukan **pengambilan foto (selfie) secara langsung saat absensi** +Aplikasi ini **dibantu dikembangkan oleh AI ChatGPT** untuk memberikan saran teknis, struktur kode, dan pengembangan fitur tambahan. + --- ## 🎯 Tujuan Proyek @@ -39,7 +47,7 @@ Aplikasi ini dirancang untuk meningkatkan **validitas kehadiran mahasiswa**, den - 📍 **Koordinat lokasi mahasiswa** - 📸 **Foto mahasiswa secara real-time** 5. Sistem melakukan validasi: - - Lokasi berada dalam **radius absensi** + - Absensi harus menggunakan foto - Foto berhasil diambil 6. Jika valid → **Absensi berhasil** 7. Jika tidak valid → **Absensi ditolak** @@ -75,23 +83,36 @@ Aplikasi memerlukan izin berikut: - `ACCESS_COARSE_LOCATION` - `CAMERA` - `INTERNET` -- `WRITE_EXTERNAL_STORAGE` (jika diperlukan) +- `WRITE_EXTERNAL_STORAGE` (opsional, jika diperlukan) --- ## 📂 Mockup ![mockup](Mockup.png) -gambar mockup dibuat oleh AI -## Catatan: -- Starter project ini dibuat berbantukan AI -- Kembangkan project dari starter yang sudah disediakan, jangan membuat dari awal. -- Untuk koordinat bisa ditambah/kurangi angka tertentu agar tidak memunculkan koordinat rumah masing-masing, data awal tetap dari GPS. - -## Pengecekan: +--- + +## Catatan +- Kembangkan project dari starter yang sudah disediakan, **tidak membuat dari awal**. +- Koordinat bisa ditambah/kurangi sedikit agar tidak memunculkan lokasi rumah masing-masing, data awal tetap diambil dari GPS. + +--- + +## Pengecekan - https://ntfy.ubharajaya.ac.id/EAS -- https://docs.google.com/spreadsheets/d/1jH15MfnNgpPGuGeid0hYfY7fFUHCEFbCmg8afTyyLZs/edit?gid=0#gid=0 +- https://docs.google.com/spreadsheets/d/1jH15MfnNgpPGuGeid0hYfFUHCEFbCmg8afTyyLZs/edit?gid=0#gid=0 -## Webhook: -- test: https://n8n.lab.ubharajaya.ac.id/webhook-test/23c6993d-1792-48fb-ad1c-ffc78a3e6254 -- production: https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254 \ No newline at end of file +--- + +## Webhook +- Test: https://n8n.lab.ubharajaya.ac.id/webhook-test/23c6993d-1792-48fb-ad1c-ffc78a3e6254 +- Production: https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254 + +--- + +## 📄 Identitas Pengembang +- **Nama** : Faris Naufal Priatna +- **NPM** : 202310715123 +- **Jenis Proyek** : Tugas Akhir EAS +- **Bantuan Pengembangan** : AI ChatGPT +Output Aplikasi yang sudah jadi \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7d76378..5cf2bcb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -53,7 +53,6 @@ dependencies { implementation(libs.androidx.compose.material3) // Location (GPS) implementation("com.google.android.gms:play-services-location:21.0.1") - testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4619836..18a085f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,35 +1,46 @@ + package="id.ac.ubharajaya.sistemakademik"> - - - - + + + + + + + + + + android:required="true" /> + + + + android:screenOrientation="portrait"> - + + + + - \ No newline at end of file + diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..f865d23 Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ 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..13ef788 --- /dev/null +++ b/app/src/main/java/id/ac/ubharajaya/sistemakademik/Absensi.kt @@ -0,0 +1,45 @@ +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 +) { + /** + * Convert objek Absensi ini menjadi JSONObject + * Siap untuk dikirim ke server + */ + fun toJson(): JSONObject { + val json = JSONObject() + json.put("npm", npm) + json.put("nama", nama) + json.put("latitude", latitude) + json.put("longitude", longitude) + json.put("timestamp", System.currentTimeMillis()) + json.put("waktu", waktu) + json.put("foto_base64", bitmapToBase64(foto)) + return json + } + + companion object { + /** + * Helper function untuk convert Bitmap ke Base64 + */ + fun bitmapToBase64(bitmap: Bitmap): String { + val outputStream = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream) + return Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP) + } + } +} 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..b091c10 --- /dev/null +++ b/app/src/main/java/id/ac/ubharajaya/sistemakademik/LoginActivity.kt @@ -0,0 +1,108 @@ +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.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.unit.dp +import id.ac.ubharajaya.sistemakademik.ui.theme.SistemAkademikTheme + +class LoginActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + SistemAkademikTheme { + Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + LoginScreen(modifier = Modifier.padding(innerPadding)) + } + } + } + } +} + +@Composable +fun LoginScreen(modifier: Modifier = Modifier) { + val context = LocalContext.current + var npm by remember { mutableStateOf("") } + var password by remember { mutableStateOf("") } + + Column( + modifier = modifier + .fillMaxSize() + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + + // LOGO KAMPUS + Image( + painter = painterResource(R.drawable.logo_absensi), // logo kamu + contentDescription = "Logo Kampus", + modifier = Modifier.size(120.dp) + ) + + Spacer(modifier = Modifier.height(24.dp)) + + // Judul Login + Text( + text = "Login Mahasiswa", + style = MaterialTheme.typography.titleLarge + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // Input NPM + OutlinedTextField( + value = npm, + onValueChange = { npm = it }, + label = { Text("NPM") }, + shape = RoundedCornerShape(12.dp), + modifier = Modifier.fillMaxWidth(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number) + ) + + Spacer(modifier = Modifier.height(12.dp)) + + // Input Password + OutlinedTextField( + value = password, + onValueChange = { password = it }, + label = { Text("Password") }, + shape = RoundedCornerShape(12.dp), + modifier = Modifier.fillMaxWidth(), + visualTransformation = PasswordVisualTransformation() + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // Tombol Login + Button( + onClick = { + if (npm == "202310715123" && password == "123") { + val intent = Intent(context, MainActivity::class.java) + context.startActivity(intent) + } else { + Toast.makeText(context, "NPM atau password salah", Toast.LENGTH_SHORT).show() + } + }, + shape = RoundedCornerShape(12.dp), + modifier = Modifier.fillMaxWidth() + ) { + Text("Login") + } + } +} 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..1417d18 100644 --- a/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt +++ b/app/src/main/java/id/ac/ubharajaya/sistemakademik/MainActivity.kt @@ -5,96 +5,70 @@ 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.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat 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.text.SimpleDateFormat +import java.util.* +import kotlin.concurrent.thread import java.net.HttpURLConnection import java.net.URL -import kotlin.concurrent.thread +import org.json.JSONObject -/* ================= UTIL ================= */ - -fun bitmapToBase64(bitmap: Bitmap): String { - val outputStream = ByteArrayOutputStream() - bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream) - return Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP) -} - -fun kirimKeN8n( - context: ComponentActivity, - latitude: Double, - longitude: Double, - foto: Bitmap -) { +// ================== UTILS ================== +fun kirimKeN8n(context: ComponentActivity, absensi: Absensi) { 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.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 json = absensi.toJson() + 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", + 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() + Toast.makeText(context, "Gagal kirim ke server", Toast.LENGTH_SHORT).show() } } } } -/* ================= ACTIVITY ================= */ - +// ================== MAIN ACTIVITY ================== class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -104,171 +78,207 @@ class MainActivity : ComponentActivity() { setContent { SistemAkademikTheme { Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - AbsensiScreen( - modifier = Modifier.padding(innerPadding), - activity = this - ) + AbsensiScreen(modifier = Modifier.padding(innerPadding), activity = this) } } } } } -/* ================= UI ================= */ - +// ================== COMPOSABLE UI ================== @Composable -fun AbsensiScreen( - modifier: Modifier = Modifier, - activity: ComponentActivity -) { +fun AbsensiScreen(modifier: Modifier = Modifier, activity: ComponentActivity) { val context = LocalContext.current + val session = remember { SessionManager(context) } var lokasi by remember { mutableStateOf("Koordinat: -") } var latitude by remember { mutableStateOf(null) } var longitude by remember { mutableStateOf(null) } var foto by remember { mutableStateOf(null) } + var waktuAbsensi by remember { mutableStateOf(null) } + val absensiList = remember { mutableStateListOf() } - val fusedLocationClient = - LocationServices.getFusedLocationProviderClient(context) + val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context) - /* ===== Permission Lokasi ===== */ + // ================== STATE WARNA ================== + var primaryColor by remember { mutableStateOf(Color(0xFF6200EE)) } + var backgroundColor by remember { mutableStateOf(Color(0xFFF2F2F2)) } + val colors = listOf( + Color.Red, Color.Green, Color.Blue, Color.Magenta, Color.Cyan, + Color.Yellow, Color.Gray, Color.DarkGray, Color.Black, Color(0xFFFF9800), + Color(0xFF9C27B0), Color(0xFF4CAF50), Color(0xFF03A9F4), Color(0xFFE91E63) + ) + + // ===== Permission & Kamera ===== val locationPermissionLauncher = - rememberLauncherForActivityResult( - ActivityResultContracts.RequestPermission() - ) { granted -> + rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> if (granted) { - - if ( - ContextCompat.checkSelfPermission( - context, - Manifest.permission.ACCESS_FINE_LOCATION - ) == PackageManager.PERMISSION_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}" + lokasi = "Lat: ${location.latitude}\nLon: ${location.longitude}" } else { lokasi = "Lokasi tidak tersedia" } } - .addOnFailureListener { - lokasi = "Gagal mengambil lokasi" - } + .addOnFailureListener { lokasi = "Gagal mengambil lokasi" } } - } else { - Toast.makeText( - context, - "Izin lokasi ditolak", - Toast.LENGTH_SHORT - ).show() + Toast.makeText(context, "Izin lokasi ditolak", Toast.LENGTH_SHORT).show() } } - /* ===== Kamera ===== */ - - val cameraLauncher = - rememberLauncherForActivityResult( - ActivityResultContracts.StartActivityForResult() - ) { result -> - if (result.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() - } - } - } - - val cameraPermissionLauncher = - rememberLauncherForActivityResult( - ActivityResultContracts.RequestPermission() - ) { granted -> - if (granted) { - val intent = - Intent(MediaStore.ACTION_IMAGE_CAPTURE) - cameraLauncher.launch(intent) + val cameraLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + val bitmap: Bitmap? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + result.data?.extras?.getParcelable("data", Bitmap::class.java) } else { - Toast.makeText( - context, - "Izin kamera ditolak", - Toast.LENGTH_SHORT - ).show() + @Suppress("DEPRECATION") + result.data?.extras?.getParcelable("data") + } + + if (bitmap != null) { + foto = bitmap + Toast.makeText(context, "Foto berhasil diambil", Toast.LENGTH_SHORT).show() } } - - /* ===== Request Awal ===== */ - - LaunchedEffect(Unit) { - locationPermissionLauncher.launch( - Manifest.permission.ACCESS_FINE_LOCATION - ) } - /* ===== UI ===== */ + 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() + } + } + LaunchedEffect(Unit) { + locationPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION) + } + + // ================== UI ================== Column( modifier = modifier .fillMaxSize() - .padding(24.dp), - verticalArrangement = Arrangement.Center + .background(backgroundColor) + .padding(24.dp) ) { - Text( text = "Absensi Akademik", - style = MaterialTheme.typography.titleLarge + style = MaterialTheme.typography.titleLarge, + color = primaryColor ) - Spacer(modifier = Modifier.height(16.dp)) - Text(text = lokasi) + // PALETTE WARNA BULETAN + Text("Pilih Warna Tema:", style = MaterialTheme.typography.bodyMedium, color = primaryColor) + Spacer(modifier = Modifier.height(8.dp)) + Row(modifier = Modifier.horizontalScroll(rememberScrollState())) { + colors.forEach { color -> + Box( + modifier = Modifier + .size(40.dp) + .padding(4.dp) + .background(color, shape = CircleShape) + .clickable { + primaryColor = color + backgroundColor = color.copy(alpha = 0.1f) + } + ) + } + } + Spacer(modifier = Modifier.height(16.dp)) + Text(text = lokasi, color = primaryColor) Spacer(modifier = Modifier.height(16.dp)) Button( - onClick = { - cameraPermissionLauncher.launch( - Manifest.permission.CAMERA - ) - }, - modifier = Modifier.fillMaxWidth() - ) { - Text("Ambil Foto") - } + onClick = { cameraPermissionLauncher.launch(Manifest.permission.CAMERA) }, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors(containerColor = primaryColor) + ) { Text("Ambil Foto") } Spacer(modifier = Modifier.height(12.dp)) Button( onClick = { if (latitude != null && longitude != null && foto != null) { - kirimKeN8n( - activity, - latitude!!, - longitude!!, - foto!! + val waktu = SimpleDateFormat("dd MMM yyyy HH:mm:ss", Locale.getDefault()).format(System.currentTimeMillis()) + waktuAbsensi = waktu + + val absensi = Absensi( + npm = session.getUserNpm() ?: "Faris Naufal Priatna", + nama = session.getUserName() ?: "202310715123", + latitude = latitude!!, + longitude = longitude!!, + waktu = waktu, + foto = foto!! ) + kirimKeN8n(activity, absensi) + absensiList.add(absensi) } else { - Toast.makeText( - context, - "Lokasi atau foto belum lengkap", - Toast.LENGTH_SHORT - ).show() + Toast.makeText(context, "⚠️ Absensi ditolak: Silahkan Foto Dlu Kocak", Toast.LENGTH_SHORT).show() } }, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors(containerColor = primaryColor) + ) { Text("Kirim Absensi") } + + Spacer(modifier = Modifier.height(16.dp)) + waktuAbsensi?.let { Text("Waktu Absensi: $it", color = primaryColor) } + + Spacer(modifier = Modifier.height(16.dp)) + + // ================== LOGOUT BUTTON ================== + Button( + onClick = { + session.logout() + context.startActivity(Intent(context, LoginActivity::class.java)) + (context as ComponentActivity).finish() + }, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors(containerColor = Color.Red) ) { - Text("Kirim Absensi") + Text("Logout") + } + + Spacer(modifier = Modifier.height(16.dp)) + Text("Riwayat Kehadiran", color = primaryColor) + Spacer(modifier = Modifier.height(12.dp)) + + if (absensiList.isEmpty()) Text("Belum ada absensi", color = primaryColor) + else LazyColumn { + items(absensiList) { item -> + Card( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) + ) { + Column(modifier = Modifier.padding(12.dp)) { + Text("Nama: ${item.nama}", color = primaryColor) + Text("NPM: ${item.npm}", color = primaryColor) + Text("Waktu: ${item.waktu}", color = primaryColor) + Text("Lat: ${item.latitude}, Lon: ${item.longitude}", color = primaryColor) + Spacer(modifier = Modifier.height(8.dp)) + Image( + bitmap = item.foto.asImageBitmap(), + contentDescription = "Foto Absensi", + modifier = Modifier + .fillMaxWidth() + .height(150.dp) + ) + } + } + } } } } diff --git a/app/src/main/java/id/ac/ubharajaya/sistemakademik/SessionManager.kt b/app/src/main/java/id/ac/ubharajaya/sistemakademik/SessionManager.kt new file mode 100644 index 0000000..f09ab98 --- /dev/null +++ b/app/src/main/java/id/ac/ubharajaya/sistemakademik/SessionManager.kt @@ -0,0 +1,33 @@ +package id.ac.ubharajaya.sistemakademik + +import android.content.Context +import android.content.SharedPreferences + +class SessionManager(context: Context) { + private val prefs: SharedPreferences = + context.getSharedPreferences("user_session", Context.MODE_PRIVATE) + + companion object { + private const val KEY_IS_LOGGED_IN = "is_logged_in" + private const val KEY_NPM = "npm" + private const val KEY_NAME = "name" + } + + fun saveLogin(npm: String, name: String) { + prefs.edit().apply { + putBoolean(KEY_IS_LOGGED_IN, true) + putString(KEY_NPM, npm) + putString(KEY_NAME, name) + apply() + } + } + + fun isLoggedIn(): Boolean = prefs.getBoolean(KEY_IS_LOGGED_IN, false) + + fun logout() { + prefs.edit().clear().apply() + } + + fun getUserNpm(): String? = prefs.getString(KEY_NPM, null) + fun getUserName(): String? = prefs.getString(KEY_NAME, null) +} diff --git a/app/src/main/res/drawable/logo_absensi.png b/app/src/main/res/drawable/logo_absensi.png new file mode 100644 index 0000000..b8a56f6 Binary files /dev/null and b/app/src/main/res/drawable/logo_absensi.png differ diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml deleted file mode 100644 index 6f3b755..0000000 --- a/app/src/main/res/mipmap-anydpi/ic_launcher.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml deleted file mode 100644 index 6f3b755..0000000 --- a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp index c209e78..287ae78 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..397bf65 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp index b2dfe3d..1ec916d 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp index 4f0f1d6..230f135 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..3483bdb Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp index 62b611d..30c64eb 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp index 948a307..9c5af0d 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..1998c86 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp index 1b9a695..42dc467 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp index 28d4b77..fe76fbb 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..f6bd572 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp index 9287f50..c03b225 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp index aa7d642..a78910e 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..a622f77 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp index 9126ae3..83f20bf 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..6082ce8 --- /dev/null +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #DE0505 + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index de92dbc..d63641b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,3 @@ - Sistem Akademik + Absensi Wajah \ No newline at end of file diff --git a/er.name Faris Naufal Priatna b/er.name Faris Naufal Priatna new file mode 100644 index 0000000..63f3faa --- /dev/null +++ b/er.name Faris Naufal Priatna @@ -0,0 +1,28 @@ +diff.astextplain.textconv=astextplain +filter.lfs.clean=git-lfs clean -- %f +filter.lfs.smudge=git-lfs smudge -- %f +filter.lfs.process=git-lfs filter-process +filter.lfs.required=true +http.sslbackend=openssl +http.sslcainfo=C:/Program Files/Git/mingw64/etc/ssl/certs/ca-bundle.crt +core.autocrlf=true +core.fscache=true +core.symlinks=false +pull.rebase=false +credential.helper=manager +credential.https://dev.azure.com.usehttppath=true +init.defaultbranch=master +credential.https://git.lab.ubharajaya.ac.id.provider=generic +user.name=Faris Naufal Priatna +user.email=202310715123@mhs.ubharajaya.ac.id +core.repositoryformatversion=0 +core.filemode=false +core.bare=false +core.logallrefupdates=true +core.symlinks=false +core.ignorecase=true +submodule.active=. +remote.origin.url=https://git.lab.ubharajaya.ac.id/administrator/Starter-EAS-2025-2026.git +remote.origin.fetch=+refs/heads/*:refs/remotes/origin/* +branch.main.remote=origin +branch.main.merge=refs/heads/main