Update Readme

This commit is contained in:
202310715280 FADLAN RIVALDI 2026-01-13 22:31:09 +07:00
parent 8aad590097
commit 900cf99681
19 changed files with 1108 additions and 322 deletions

View File

@ -1,4 +1,8 @@
# 📱 Aplikasi Absensi Akademik Berbasis Koordinat dan Foto (Mobile) # 📱 Aplikasi Absensi Akademik Berbasis Koordinat dan Foto (Mobile)## 👤 Identitas Mahasiswa
- **Nama:** Fadlan Rivaldi
- **NPM:** 202310715280
---
## 📌 Deskripsi Proyek ## 📌 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 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**.
@ -50,48 +54,49 @@ Aplikasi ini dirancang untuk meningkatkan **validitas kehadiran mahasiswa**, den
- Foto diambil menggunakan **kamera depan (selfie)** - Foto diambil menggunakan **kamera depan (selfie)**
- Foto hanya dapat diambil **saat proses absensi** - Foto hanya dapat diambil **saat proses absensi**
- Foto disimpan sebagai **bukti kehadiran** - Foto disimpan sebagai **bukti kehadiran**
- Foto dapat digunakan untuk: - Foto dapat digunakan untuk verifikasi manual dan dokumentasi akademik.
- Verifikasi manual oleh dosen
- Dokumentasi akademik
--- ---
## 🛠️ Teknologi yang Digunakan ## 🛠️ Teknologi yang Digunakan
- **Platform**: Android - **Platform**: Android
- **Bahasa Pemrograman** : Kotlin / Java - **Bahasa Pemrograman**: Kotlin
- **Location Service** : - **Location Service**: Google Maps API / Fused Location Provider
- Google Maps API - **Camera API**: CameraX
- Fused Location Provider - **Architecture**: MVVM / View System & Jetpack Compose
- **Camera API** : CameraX / Camera2
- **Database** : Firebase / SQLite / MySQL
- **Storage** : Firebase Storage / Local Storage
- **IDE**: Android Studio - **IDE**: Android Studio
--- ---
## 🔐 Izin Aplikasi (Permissions) ## 🔐 Izin Aplikasi (Permissions)
Aplikasi memerlukan izin berikut: Aplikasi memerlukan izin berikut:
- `ACCESS_FINE_LOCATION` - `ACCESS_FINE_LOCATION` & `ACCESS_COARSE_LOCATION`
- `ACCESS_COARSE_LOCATION`
- `CAMERA` - `CAMERA`
- `INTERNET` - `INTERNET`
- `WRITE_EXTERNAL_STORAGE` (jika diperlukan)
--- ---
## 📂 Mockup ## 📂 Mockup
![mockup](Mockup.png) ![mockup](Mockup.png)
gambar mockup dibuat oleh AI *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: ## ⚠️ Disclaimer & Catatan Penting
- https://ntfy.ubharajaya.ac.id/EAS - **PENGGUNAAN AI**: Proyek ini dikembangkan dengan bantuan **Kecerdasan Buatan (AI)** dalam proses debugging, pembuatan starter project, dan penyusunan dokumentasi.
- https://docs.google.com/spreadsheets/d/1jH15MfnNgpPGuGeid0hYfY7fFUHCEFbCmg8afTyyLZs/edit?gid=0#gid=0 - **PENGEMBANGAN**: Proyek ini dikembangkan dari *starter project* yang disediakan dan tidak dibuat dari nol.
- **PRIVASI KOORDINAT**: Untuk alasan keamanan/privasi, angka koordinat GPS dapat dimodifikasi sedikit agar tidak menunjukkan lokasi rumah pribadi secara presisi.
## 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 ## 🔗 Link Pengecekan & Webhook
- **Monitoring**: [ntfy.ubharajaya.ac.id/EAS](https://ntfy.ubharajaya.ac.id/EAS)
- **Data Spreadsheet**: [Google Sheets](https://docs.google.com/spreadsheets/d/1jH15MfnNgpPGuGeid0hYfY7fFUHCEFbCmg8afTyyLZs/edit?gid=0#gid=0)
- **Webhook Production**: `https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254`
---
“⚠️ Disclaimer & Catatan Penting”
Proyek ini dikembangkan dengan bantuan Kecerdasan Buatan (AI) sebagai asisten dalam proses debugging dan dokumentasi. Seluruh implementasi, pemahaman konsep,
dan pengembangan fitur dilakukan oleh penulis secara mandiri.
*Dibuat untuk memenuhi Tugas Project Akhir EAS 2025/2026.*

View File

@ -51,8 +51,25 @@ dependencies {
implementation(libs.androidx.compose.ui.graphics) implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.material3)
implementation ("com.google.android.material:material:1.11.0")
// Location (GPS) // Location (GPS)
implementation("com.google.android.gms:play-services-location:21.0.1") implementation("com.google.android.gms:play-services-location:21.0.1")
// Navigation Component
implementation("androidx.navigation:navigation-fragment-ktx:2.7.6")
implementation("androidx.navigation:navigation-ui-ktx:2.7.6")
// ViewModel & LiveData
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2")
// Google Play Services Location
implementation("com.google.android.gms:play-services-location:21.0.1")
// Gson untuk parsing JSON
implementation("com.google.code.gson:gson:2.10.1")
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)

View File

@ -1,274 +1,24 @@
package id.ac.ubharajaya.sistemakademik 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.Bundle import android.os.Bundle
import android.provider.MediaStore import androidx.appcompat.app.AppCompatActivity
import android.util.Base64 import androidx.navigation.fragment.NavHostFragment
import android.widget.Toast import androidx.navigation.ui.setupWithNavController
import androidx.activity.ComponentActivity import com.google.android.material.bottomnavigation.BottomNavigationView
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
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.net.HttpURLConnection
import java.net.URL
import kotlin.concurrent.thread
/* ================= UTIL ================= */ class MainActivity : AppCompatActivity() {
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
) {
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 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()
}
}
}
}
/* ================= ACTIVITY ================= */
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() setContentView(R.layout.activity_main)
setContent { // Setup Navigation
SistemAkademikTheme { val navHostFragment = supportFragmentManager
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> .findFragmentById(R.id.nav_host_fragment) as NavHostFragment
AbsensiScreen( val navController = navHostFragment.navController
modifier = Modifier.padding(innerPadding),
activity = this // Setup Bottom Navigation
) val bottomNav = findViewById<BottomNavigationView>(R.id.bottom_navigation)
} bottomNav.setupWithNavController(navController)
}
}
}
}
/* ================= UI ================= */
@Composable
fun AbsensiScreen(
modifier: Modifier = Modifier,
activity: ComponentActivity
) {
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 ->
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"
}
}
.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) {
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)
} else {
Toast.makeText(
context,
"Izin kamera ditolak",
Toast.LENGTH_SHORT
).show()
}
}
/* ===== Request Awal ===== */
LaunchedEffect(Unit) {
locationPermissionLauncher.launch(
Manifest.permission.ACCESS_FINE_LOCATION
)
}
/* ===== UI ===== */
Column(
modifier = modifier
.fillMaxSize()
.padding(24.dp),
verticalArrangement = Arrangement.Center
) {
Text(
text = "Absensi Akademik",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(16.dp))
Text(text = lokasi)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = {
cameraPermissionLauncher.launch(
Manifest.permission.CAMERA
)
},
modifier = Modifier.fillMaxWidth()
) {
Text("Ambil Foto")
}
Spacer(modifier = 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()
) {
Text("Kirim Absensi")
}
} }
} }

View File

@ -1,2 +1,58 @@
package id.ac.ubharajaya.sistemakademik.adapter package id.ac.ubharajaya.sistemakademik.adapter
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.Base64
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import id.ac.ubharajaya.sistemakademik.R
import id.ac.ubharajaya.sistemakademik.data.AbsensiData
import java.text.SimpleDateFormat
import java.util.*
class HistoryAdapter(
private val dataList: List<AbsensiData>
) : RecyclerView.Adapter<HistoryAdapter.ViewHolder>() {
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val ivThumbnail: ImageView = view.findViewById(R.id.ivThumbnail)
val tvNama: TextView = view.findViewById(R.id.tvHistoryNama)
val tvTanggal: TextView = view.findViewById(R.id.tvHistoryTanggal)
val tvLokasi: TextView = view.findViewById(R.id.tvHistoryLokasi)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_history, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val data = dataList[position]
// Set foto thumbnail
val bitmap = base64ToBitmap(data.fotoBase64)
holder.ivThumbnail.setImageBitmap(bitmap)
// Set nama
holder.tvNama.text = data.nama
// Set tanggal
val dateFormat = SimpleDateFormat("dd MMM yyyy, HH:mm", Locale("id", "ID"))
holder.tvTanggal.text = "📅 ${dateFormat.format(Date(data.timestamp))}"
// Set lokasi
holder.tvLokasi.text = "📍 Lat: ${data.latitude}, Lon: ${data.longitude}"
}
override fun getItemCount() = dataList.size
private fun base64ToBitmap(base64: String): Bitmap {
val decodedBytes = Base64.decode(base64, Base64.NO_WRAP)
return BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.size)
}
}

View File

@ -1,2 +1,22 @@
package id.ac.ubharajaya.sistemakademik.data package id.ac.ubharajaya.sistemakademik.data
data class UserProfile(
var nama: String = "",
var npm: String = ""
)
data class AbsensiData(
val nama: String,
val npm: String,
val latitude: Double,
val longitude: Double,
val timestamp: Long,
val fotoBase64: String,
val alamat: String = "Alamat tidak tersedia"
)
// Singleton untuk menyimpan data sementara
object DataHolder {
var userProfile: UserProfile = UserProfile()
var currentAbsensi: AbsensiData? = null
val historyList = mutableListOf<AbsensiData>()
}

View File

@ -1,2 +1,193 @@
package id.ac.ubharajaya.sistemakademik.ui.fragments package id.ac.ubharajaya.sistemakademik.ui.fragments
import android.Manifest
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.os.Bundle
import android.provider.MediaStore
import android.util.Base64
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import com.google.android.gms.location.LocationServices
import id.ac.ubharajaya.sistemakademik.R
import id.ac.ubharajaya.sistemakademik.data.AbsensiData
import id.ac.ubharajaya.sistemakademik.data.DataHolder
import java.io.ByteArrayOutputStream
class AbsensiFragment : Fragment() {
private lateinit var ivPreview: ImageView
private lateinit var tvLokasi: TextView
private lateinit var btnAmbilFoto: Button
private lateinit var btnLanjut: Button
private var foto: Bitmap? = null
private var latitude: Double? = null
private var longitude: Double? = null
private val fusedLocationClient by lazy {
LocationServices.getFusedLocationProviderClient(requireActivity())
}
// Permission launcher untuk lokasi
private val locationPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
if (granted) {
ambilLokasiGPS()
} else {
Toast.makeText(context, "Izin lokasi ditolak", Toast.LENGTH_SHORT).show()
}
}
// Camera launcher
private val cameraLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val bitmap = result.data?.extras?.get("data") as? Bitmap
if (bitmap != null) {
foto = bitmap
ivPreview.setImageBitmap(bitmap)
cekKelengkapanData()
Toast.makeText(context, "Foto berhasil diambil!", Toast.LENGTH_SHORT).show()
}
}
}
// Permission launcher untuk kamera
private val cameraPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
if (granted) {
bukaKamera()
} else {
Toast.makeText(context, "Izin kamera ditolak", Toast.LENGTH_SHORT).show()
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_absensi, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
ivPreview = view.findViewById(R.id.ivPreviewFoto)
tvLokasi = view.findViewById(R.id.tvLokasi)
btnAmbilFoto = view.findViewById(R.id.btnAmbilFoto)
btnLanjut = view.findViewById(R.id.btnLanjutPreview)
// Cek profile sudah diisi atau belum
if (DataHolder.userProfile.nama.isEmpty()) {
Toast.makeText(context, "Isi profile dulu di tab Profil!", Toast.LENGTH_LONG).show()
}
// Request permission lokasi
requestLocationPermission()
btnAmbilFoto.setOnClickListener {
requestCameraPermission()
}
btnLanjut.setOnClickListener {
if (foto != null && latitude != null && longitude != null) {
simpanDataSementara()
findNavController().navigate(R.id.action_absensi_to_preview)
}
}
}
private fun requestLocationPermission() {
when {
ContextCompat.checkSelfPermission(
requireContext(),
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED -> {
ambilLokasiGPS()
}
else -> {
locationPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
}
}
}
private fun ambilLokasiGPS() {
try {
fusedLocationClient.lastLocation.addOnSuccessListener { location ->
if (location != null) {
latitude = location.latitude
longitude = location.longitude
tvLokasi.text = "📍 GPS Ready\nLat: ${location.latitude}\nLon: ${location.longitude}"
cekKelengkapanData()
} else {
tvLokasi.text = "❌ Lokasi tidak tersedia\nHidupkan GPS"
}
}
} catch (e: SecurityException) {
Toast.makeText(context, "Error: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
private fun requestCameraPermission() {
when {
ContextCompat.checkSelfPermission(
requireContext(),
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED -> {
bukaKamera()
}
else -> {
cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
}
}
}
private fun bukaKamera() {
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
cameraLauncher.launch(intent)
}
private fun cekKelengkapanData() {
btnLanjut.isEnabled = foto != null && latitude != null && longitude != null
}
private fun simpanDataSementara() {
val bitmap = foto ?: return
val lat = latitude ?: return
val lon = longitude ?: return
val fotoBase64 = bitmapToBase64(bitmap)
DataHolder.currentAbsensi = AbsensiData(
nama = DataHolder.userProfile.nama,
npm = DataHolder.userProfile.npm,
latitude = lat,
longitude = lon,
timestamp = System.currentTimeMillis(),
fotoBase64 = fotoBase64,
alamat = "Alamat akan diambil saat preview"
)
}
private 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

@ -1,2 +1,57 @@
package id.ac.ubharajaya.sistemakademik.ui.fragments package id.ac.ubharajaya.sistemakademik.ui.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import id.ac.ubharajaya.sistemakademik.R
import id.ac.ubharajaya.sistemakademik.adapter.HistoryAdapter
import id.ac.ubharajaya.sistemakademik.data.DataHolder
class HistoryFragment : Fragment() {
private lateinit var rvHistory: RecyclerView
private lateinit var tvEmpty: TextView
private lateinit var adapter: HistoryAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_history, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
rvHistory = view.findViewById(R.id.rvHistory)
tvEmpty = view.findViewById(R.id.tvEmptyState)
adapter = HistoryAdapter(DataHolder.historyList)
rvHistory.layoutManager = LinearLayoutManager(context)
rvHistory.adapter = adapter
cekDataKosong()
}
override fun onResume() {
super.onResume()
adapter.notifyDataSetChanged()
cekDataKosong()
}
private fun cekDataKosong() {
if (DataHolder.historyList.isEmpty()) {
tvEmpty.visibility = View.VISIBLE
rvHistory.visibility = View.GONE
} else {
tvEmpty.visibility = View.GONE
rvHistory.visibility = View.VISIBLE
}
}
}

View File

@ -1,2 +1,173 @@
package id.ac.ubharajaya.sistemakademik.ui.fragments package id.ac.ubharajaya.sistemakademik.ui.fragments
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Bundle
import android.util.Base64
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import id.ac.ubharajaya.sistemakademik.R
import id.ac.ubharajaya.sistemakademik.data.DataHolder
import org.json.JSONObject
import java.net.HttpURLConnection
import java.net.URL
import java.text.SimpleDateFormat
import java.util.*
import kotlin.concurrent.thread
class PreviewFragment : Fragment() {
private lateinit var ivFoto: ImageView
private lateinit var tvNama: TextView
private lateinit var tvNpm: TextView
private lateinit var tvTimestamp: TextView
private lateinit var tvKoordinat: TextView
private lateinit var tvAlamat: TextView
private lateinit var tvStatus: TextView
private lateinit var btnSubmit: Button
private lateinit var btnKembali: Button
private lateinit var progressBar: ProgressBar
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_preview, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
ivFoto = view.findViewById(R.id.ivFotoPreview)
tvNama = view.findViewById(R.id.tvNama)
tvNpm = view.findViewById(R.id.tvNpm)
tvTimestamp = view.findViewById(R.id.tvTimestamp)
tvKoordinat = view.findViewById(R.id.tvKoordinat)
tvAlamat = view.findViewById(R.id.tvAlamat)
tvStatus = view.findViewById(R.id.tvStatusValidasi)
btnSubmit = view.findViewById(R.id.btnSubmitAbsensi)
btnKembali = view.findViewById(R.id.btnKembali)
progressBar = view.findViewById(R.id.progressBar)
tampilkanDataPreview()
btnSubmit.setOnClickListener {
kirimAbsensi()
}
btnKembali.setOnClickListener {
findNavController().popBackStack()
}
}
private fun tampilkanDataPreview() {
val absensi = DataHolder.currentAbsensi ?: run {
Toast.makeText(context, "Data tidak ditemukan", Toast.LENGTH_SHORT).show()
findNavController().popBackStack()
return
}
// Tampilkan foto
val bitmap = base64ToBitmap(absensi.fotoBase64)
ivFoto.setImageBitmap(bitmap)
// Tampilkan data mahasiswa
tvNama.text = "👤 Nama: ${absensi.nama}"
tvNpm.text = "🎓 NPM: ${absensi.npm}"
// Tampilkan timestamp
val dateFormat = SimpleDateFormat("dd MMM yyyy, HH:mm:ss", Locale("id", "ID"))
val tanggal = dateFormat.format(Date(absensi.timestamp))
tvTimestamp.text = "$tanggal WIB"
// Tampilkan koordinat
tvKoordinat.text = "🌍 Lat: ${absensi.latitude}, Lon: ${absensi.longitude}"
// Tampilkan alamat (dummy dulu)
tvAlamat.text = "📮 ${absensi.alamat}"
// Status validasi (dummy - selalu valid)
tvStatus.text = "✅ Dalam Radius Kampus (0m dari lokasi)"
tvStatus.setTextColor(resources.getColor(android.R.color.holo_green_dark, null))
}
private fun base64ToBitmap(base64: String): Bitmap {
val decodedBytes = Base64.decode(base64, Base64.NO_WRAP)
return BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.size)
}
private fun kirimAbsensi() {
val absensi = DataHolder.currentAbsensi ?: return
// Tampilkan loading
progressBar.visibility = View.VISIBLE
btnSubmit.isEnabled = false
btnKembali.isEnabled = false
thread {
try {
val url = URL("https://n8n.lab.ubharajaya.ac.id/webhook/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", absensi.npm)
put("nama", absensi.nama)
put("latitude", absensi.latitude)
put("longitude", absensi.longitude)
put("timestamp", absensi.timestamp)
put("foto_base64", absensi.fotoBase64)
put("address", absensi.alamat)
put("distance_from_campus", 0)
put("status", "hadir")
}
conn.outputStream.use {
it.write(json.toString().toByteArray())
}
val responseCode = conn.responseCode
activity?.runOnUiThread {
progressBar.visibility = View.GONE
btnSubmit.isEnabled = true
btnKembali.isEnabled = true
if (responseCode == 200) {
Toast.makeText(context, "✅ Absensi berhasil dikirim!", Toast.LENGTH_LONG).show()
// Simpan ke history
DataHolder.historyList.add(0, absensi)
// Kembali ke halaman absensi
findNavController().popBackStack()
} else {
Toast.makeText(context, "❌ Gagal kirim (Code: $responseCode)", Toast.LENGTH_SHORT).show()
}
}
conn.disconnect()
} catch (e: Exception) {
activity?.runOnUiThread {
progressBar.visibility = View.GONE
btnSubmit.isEnabled = true
btnKembali.isEnabled = true
Toast.makeText(context, "❌ Error: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
}
}
}

View File

@ -1,2 +1,67 @@
package id.ac.ubharajaya.sistemakademik.ui.fragments package id.ac.ubharajaya.sistemakademik.ui.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.fragment.app.Fragment
import com.google.android.material.textfield.TextInputEditText
import id.ac.ubharajaya.sistemakademik.R
import id.ac.ubharajaya.sistemakademik.data.DataHolder
class ProfileFragment : Fragment() {
private lateinit var etNama: TextInputEditText
private lateinit var etNpm: TextInputEditText
private lateinit var btnSimpan: Button
private lateinit var tvInfo: TextView
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_profile, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
etNama = view.findViewById(R.id.etNama)
etNpm = view.findViewById(R.id.etNpm)
btnSimpan = view.findViewById(R.id.btnSimpanProfile)
tvInfo = view.findViewById(R.id.tvProfileInfo)
// Load data yang sudah disimpan
etNama.setText(DataHolder.userProfile.nama)
etNpm.setText(DataHolder.userProfile.npm)
if (DataHolder.userProfile.nama.isNotEmpty()) {
tampilkanInfo()
}
btnSimpan.setOnClickListener {
val nama = etNama.text.toString().trim()
val npm = etNpm.text.toString().trim()
if (nama.isEmpty() || npm.isEmpty()) {
Toast.makeText(context, "Nama dan NPM harus diisi!", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
// Simpan ke DataHolder
DataHolder.userProfile.nama = nama
DataHolder.userProfile.npm = npm
Toast.makeText(context, "Profile berhasil disimpan!", Toast.LENGTH_SHORT).show()
tampilkanInfo()
}
}
private fun tampilkanInfo() {
tvInfo.visibility = View.VISIBLE
tvInfo.text = "✅ Profile tersimpan\n${DataHolder.userProfile.nama}\n${DataHolder.userProfile.npm}"
}
}

View File

@ -1,6 +1,29 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@id/bottom_navigation"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_navigation"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="@menu/bottom_nav_menu" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,6 +1,51 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:orientation="vertical"
android:padding="24dp"
android:gravity="center">
</androidx.constraintlayout.widget.ConstraintLayout> <TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ambil Absensi"
android:textSize="28sp"
android:textStyle="bold"
android:layout_marginBottom="32dp"/>
<ImageView
android:id="@+id/ivPreviewFoto"
android:layout_width="200dp"
android:layout_height="200dp"
android:background="@android:color/darker_gray"
android:scaleType="centerCrop"
android:layout_marginBottom="24dp"/>
<TextView
android:id="@+id/tvLokasi"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Menunggu GPS..."
android:textSize="14sp"
android:textAlignment="center"
android:layout_marginBottom="32dp"/>
<Button
android:id="@+id/btnAmbilFoto"
android:layout_width="match_parent"
android:layout_height="60dp"
android:text="📷 Ambil Foto"
android:textSize="16sp"/>
<Button
android:id="@+id/btnLanjutPreview"
android:layout_width="match_parent"
android:layout_height="60dp"
android:text="Lanjut ke Preview"
android:textSize="16sp"
android:layout_marginTop="12dp"
android:enabled="false"/>
</LinearLayout>

View File

@ -1,6 +1,32 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
</androidx.constraintlayout.widget.ConstraintLayout> <TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Riwayat Absensi"
android:textSize="24sp"
android:textStyle="bold"
android:layout_marginBottom="16dp"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvHistory"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<TextView
android:id="@+id/tvEmptyState"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Belum ada riwayat absensi"
android:textSize="16sp"
android:layout_gravity="center"
android:layout_marginTop="100dp"
android:visibility="gone"/>
</LinearLayout>

View File

@ -1,6 +1,176 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:fillViewport="true">
</androidx.constraintlayout.widget.ConstraintLayout> <LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Preview Absensi"
android:textSize="24sp"
android:textStyle="bold"
android:layout_gravity="center"
android:layout_marginBottom="24dp"/>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardElevation="4dp"
app:cardCornerRadius="16dp"
android:layout_marginBottom="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="📸 Foto Absensi"
android:textSize="16sp"
android:textStyle="bold"
android:layout_marginBottom="12dp"/>
<ImageView
android:id="@+id/ivFotoPreview"
android:layout_width="match_parent"
android:layout_height="300dp"
android:scaleType="centerCrop"
android:background="@android:color/darker_gray"/>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardElevation="4dp"
app:cardCornerRadius="16dp"
android:layout_marginBottom="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="👤 Data Mahasiswa"
android:textSize="16sp"
android:textStyle="bold"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/tvNama"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Nama: -"
android:textSize="14sp"
android:layout_marginBottom="4dp"/>
<TextView
android:id="@+id/tvNpm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="NPM: -"
android:textSize="14sp"/>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardElevation="4dp"
app:cardCornerRadius="16dp"
android:layout_marginBottom="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="📍 Lokasi &amp; Waktu"
android:textSize="16sp"
android:textStyle="bold"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/tvTimestamp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="⏰ -"
android:textSize="14sp"
android:layout_marginBottom="4dp"/>
<TextView
android:id="@+id/tvKoordinat"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="🌍 -"
android:textSize="14sp"
android:layout_marginBottom="4dp"/>
<TextView
android:id="@+id/tvAlamat"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="📮 -"
android:textSize="14sp"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/tvStatusValidasi"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="✅ Dalam Radius Kampus"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="#4CAF50"/>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<Button
android:id="@+id/btnSubmitAbsensi"
android:layout_width="match_parent"
android:layout_height="60dp"
android:text="✅ Submit Absensi"
android:textSize="16sp"
android:layout_marginTop="8dp"/>
<Button
android:id="@+id/btnKembali"
android:layout_width="match_parent"
android:layout_height="60dp"
android:text="🔄 Ambil Ulang Foto"
android:textSize="16sp"
android:layout_marginTop="8dp"
style="@style/Widget.Material3.Button.OutlinedButton"/>
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="16dp"
android:visibility="gone"/>
</LinearLayout>
</ScrollView>

View File

@ -1,6 +1,78 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:orientation="vertical"
android:padding="24dp"
android:gravity="center">
</androidx.constraintlayout.widget.ConstraintLayout> <com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardElevation="4dp"
app:cardCornerRadius="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Profil Mahasiswa"
android:textSize="24sp"
android:textStyle="bold"
android:layout_gravity="center"
android:layout_marginBottom="24dp"/>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Nama Lengkap"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etNama"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPersonName"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="NPM"
android:layout_marginTop="16dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etNpm"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"/>
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/btnSimpanProfile"
android:layout_width="match_parent"
android:layout_height="60dp"
android:text="Simpan Profile"
android:textSize="16sp"
android:layout_marginTop="24dp"/>
<TextView
android:id="@+id/tvProfileInfo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_gravity="center"
android:textAlignment="center"
android:visibility="gone"/>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>

View File

@ -1,6 +1,59 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardElevation="4dp"
app:cardCornerRadius="12dp">
</androidx.constraintlayout.widget.ConstraintLayout> <LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp">
<ImageView
android:id="@+id/ivThumbnail"
android:layout_width="80dp"
android:layout_height="80dp"
android:scaleType="centerCrop"
android:background="@android:color/darker_gray"/>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginStart="16dp">
<TextView
android:id="@+id/tvHistoryNama"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Nama"
android:textSize="16sp"
android:textStyle="bold"/>
<TextView
android:id="@+id/tvHistoryTanggal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Tanggal"
android:textSize="14sp"
android:layout_marginTop="4dp"/>
<TextView
android:id="@+id/tvHistoryLokasi"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Lokasi"
android:textSize="12sp"
android:layout_marginTop="4dp"/>
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -1,4 +1,15 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/profileFragment"
android:title="Profil"
android:icon="@android:drawable/ic_menu_myplaces" />
<item
android:id="@+id/absensiFragment"
android:title="Absensi"
android:icon="@android:drawable/ic_menu_camera" />
<item
android:id="@+id/historyFragment"
android:title="Riwayat"
android:icon="@android:drawable/ic_menu_recent_history" />
</menu> </menu>

View File

@ -1,6 +1,31 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android" <navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/nav_graph"> android:id="@+id/nav_graph"
app:startDestination="@id/profileFragment">
<fragment
android:id="@+id/profileFragment"
android:name="id.ac.ubharajaya.sistemakademik.ui.fragments.ProfileFragment"
android:label="Profil" />
<fragment
android:id="@+id/absensiFragment"
android:name="id.ac.ubharajaya.sistemakademik.ui.fragments.AbsensiFragment"
android:label="Absensi">
<action
android:id="@+id/action_absensi_to_preview"
app:destination="@id/previewFragment" />
</fragment>
<fragment
android:id="@+id/previewFragment"
android:name="id.ac.ubharajaya.sistemakademik.ui.fragments.PreviewFragment"
android:label="Preview" />
<fragment
android:id="@+id/historyFragment"
android:name="id.ac.ubharajaya.sistemakademik.ui.fragments.HistoryFragment"
android:label="Riwayat" />
</navigation> </navigation>

View File

@ -1,3 +1,19 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="Theme.SistemAkademik"
parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="textAppearanceBody1">
@style/TextAppearance.MaterialComponents.Body1
</item>
<item name="textAppearanceBody2">
@style/TextAppearance.MaterialComponents.Body2
</item>
<item name="textAppearanceButton">
@style/TextAppearance.MaterialComponents.Button
</item>
</style>
</resources> </resources>

View File

@ -1,5 +1,20 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="Theme.SistemAkademik" parent="android:Theme.Material.Light.NoActionBar" /> <style name="Theme.SistemAkademik"
parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- WAJIB buat TextInputLayout -->
<item name="textAppearanceBody1">
@style/TextAppearance.MaterialComponents.Body1
</item>
<item name="textAppearanceBody2">
@style/TextAppearance.MaterialComponents.Body2
</item>
<item name="textAppearanceButton">
@style/TextAppearance.MaterialComponents.Button
</item>
</style>
</resources> </resources>