diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..b3405b3 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +My Application \ No newline at end of file diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file 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/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..639c779 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ 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..7061a0d --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,61 @@ + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..903a037 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..b3a95a5 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,73 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "com.kelompok10.ToDoList" + compileSdk = 34 + + defaultConfig { + applicationId = "com.kelompok10.ToDoList" + minSdk = 24 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary = true + } + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = "17" + } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.1" + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.appcompat:appcompat:1.6.1") + implementation("com.google.android.material:material:1.11.0") + implementation("androidx.constraintlayout:constraintlayout:2.1.4") + implementation("androidx.recyclerview:recyclerview:1.3.1") + implementation("org.jetbrains.kotlin:kotlin-stdlib:1.9.0") + + // Jetpack Compose Dependencies + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") + implementation("androidx.activity:activity-compose:1.8.2") + implementation(platform("androidx.compose:compose-bom:2023.08.00")) + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.ui:ui-graphics") + implementation("androidx.compose.ui:ui-tooling-preview") + implementation("androidx.compose.material3:material3") + implementation("androidx.compose.material:material-icons-extended") // Tambahkan ini + androidTestImplementation(platform("androidx.compose:compose-bom:2023.08.00")) + androidTestImplementation("androidx.compose.ui:ui-test-junit4") + debugImplementation("androidx.compose.ui:ui-tooling") + debugImplementation("androidx.compose.ui:ui-test-manifest") +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/kelompok10/ToDoList/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/kelompok10/ToDoList/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..41b0f97 --- /dev/null +++ b/app/src/androidTest/java/com/kelompok10/ToDoList/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.kelompok10.ToDoList + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.kelompok10.myapplication", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..94be7ec --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + \ 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..373cbf9 Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/com/kelompok10/ToDoList/MainActivity.kt b/app/src/main/java/com/kelompok10/ToDoList/MainActivity.kt new file mode 100644 index 0000000..5ca2f9e --- /dev/null +++ b/app/src/main/java/com/kelompok10/ToDoList/MainActivity.kt @@ -0,0 +1,1250 @@ +package com.kelompok10.ToDoList + +import android.Manifest +import android.app.Activity +import android.app.AlarmManager +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build +import android.os.Bundle +import android.speech.RecognizerIntent +import androidx.activity.ComponentActivity +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.compose.setContent +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.* +import androidx.compose.animation.core.tween +import androidx.compose.foundation.ExperimentalFoundationApi +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.material.icons.Icons +import androidx.compose.material.icons.filled.* +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.draw.shadow +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat +import com.kelompok10.ToDoList.ui.theme.MyApplicationTheme +import java.time.Instant +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.util.concurrent.atomic.AtomicInteger + +// ----- DATA & NAVIGATION MODELS ----- +data class Task( + val id: Int, + var text: String, + var isCompleted: Boolean = false, + val deadline: LocalDate? = null, + var completionDate: LocalDate? = null, + var alarmTime: LocalDateTime? = null +) + +enum class AppScreen { Splash, Main } +enum class MainScreenTab { Active, Completed, Statistics } + +// ----- ALARM RECEIVER ----- +class AlarmReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val taskText = intent.getStringExtra("task_text") ?: "Tugas" + val taskId = intent.getIntExtra("task_id", 0) + + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + // Intent untuk membuka app saat notifikasi diklik + val appIntent = Intent(context, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + val pendingIntent = PendingIntent.getActivity( + context, + taskId, + appIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + val notification = NotificationCompat.Builder(context, "task_alarm_channel") + .setSmallIcon(android.R.drawable.ic_dialog_info) + .setContentTitle("⏰ Pengingat Tugas") + .setContentText(taskText) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setCategory(NotificationCompat.CATEGORY_ALARM) + .setAutoCancel(true) + .setContentIntent(pendingIntent) + .setSound(android.provider.Settings.System.DEFAULT_NOTIFICATION_URI) + .setVibrate(longArrayOf(0, 1000, 500, 1000, 500, 1000, 500, 1000, 500, 1000)) // Pattern vibrasi lebih panjang + .setLights(0xFF1976D2.toInt(), 1000, 1000) + .setTimeoutAfter(30000) // Notifikasi tetap 30 detik + .build() + + notificationManager.notify(taskId, notification) + + // Putar suara alarm tambahan jika diperlukan + playAlarmSound(context) + } + + private fun playAlarmSound(context: Context) { + try { + val notification = android.media.RingtoneManager.getDefaultUri(android.media.RingtoneManager.TYPE_ALARM) + ?: android.media.RingtoneManager.getDefaultUri(android.media.RingtoneManager.TYPE_NOTIFICATION) + + val ringtone = android.media.RingtoneManager.getRingtone(context, notification) + ringtone?.play() + + // Stop otomatis setelah 30 detik (diperpanjang untuk lebih terdengar) + android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({ + ringtone?.stop() + }, 30000) // 30 detik = 30000 milliseconds + } catch (e: Exception) { + e.printStackTrace() + } + } +} + +// ----- MAIN ACTIVITY ----- +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // Create notification channel for alarms + createNotificationChannel() + + setContent { + MyApplicationTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + AppNavigationHost() + } + } + } + } + + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + "task_alarm_channel", + "Task Reminders", + NotificationManager.IMPORTANCE_HIGH + ).apply { + description = "Notifikasi pengingat tugas" + enableVibration(true) + vibrationPattern = longArrayOf(0, 1000, 500, 1000, 500, 1000, 500, 1000, 500, 1000) // Pattern lebih panjang + enableLights(true) + lightColor = 0xFF1976D2.toInt() + setSound( + android.provider.Settings.System.DEFAULT_ALARM_ALERT_URI, + android.media.AudioAttributes.Builder() + .setUsage(android.media.AudioAttributes.USAGE_ALARM) + .setContentType(android.media.AudioAttributes.CONTENT_TYPE_SONIFICATION) + .build() + ) + } + val notificationManager = getSystemService(NotificationManager::class.java) + notificationManager.createNotificationChannel(channel) + } + } +} + +// ----- TOP-LEVEL NAVIGATION ----- +@Composable +fun AppNavigationHost() { + var currentAppScreen by remember { mutableStateOf(AppScreen.Splash) } + + Crossfade(targetState = currentAppScreen, animationSpec = tween(500)) { screen -> + when (screen) { + AppScreen.Splash -> SplashScreen { currentAppScreen = AppScreen.Main } + AppScreen.Main -> TodoAppNavigator() + } + } +} + +@Composable +fun SplashScreen(onStartClick: () -> Unit) { + Box( + modifier = Modifier + .fillMaxSize() + .background( + brush = Brush.verticalGradient( + colors = listOf( + Color(0xFFE3F2FD), // Biru sangat muda + Color(0xFFBBDEFB), // Biru muda + Color(0xFF90CAF9) // Biru terang + ) + ) + ) + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Icon( + Icons.Default.TaskAlt, + contentDescription = "App Icon", + modifier = Modifier.size(120.dp), + tint = Color(0xFF1976D2) // Biru tua + ) + Spacer(Modifier.height(24.dp)) + Text( + "Selamat Datang di To-Do List", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + color = Color(0xFF1565C0) // Biru tua + ) + Text( + "Kelola tugas harianmu dan raih produktivitas tertinggi.", + style = MaterialTheme.typography.bodyLarge, + color = Color(0xFF1976D2), // Biru medium + textAlign = TextAlign.Center + ) + Spacer(Modifier.height(48.dp)) + Button( + onClick = onStartClick, + modifier = Modifier.fillMaxWidth(), + contentPadding = PaddingValues(16.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color(0xFF1976D2) // Biru medium + ) + ) { + Text("Mulai", fontSize = 18.sp) + } + } + } +} + +// ----- MAIN APP ----- +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TodoAppNavigator() { + val context = LocalContext.current + val idCounter = remember { AtomicInteger(0) } + val (tasks, setTasks) = remember { mutableStateOf(listOf()) } + var currentTab by remember { mutableStateOf(MainScreenTab.Active) } + + val activeTasks = remember(tasks) { tasks.filter { !it.isCompleted } } + val completedTasks = remember(tasks) { tasks.filter { it.isCompleted } } + + val addTask: (String, LocalDate?, LocalDateTime?) -> Task = { text, deadline, alarmTime -> + val newTask = Task(id = idCounter.getAndIncrement(), text = text, deadline = deadline, alarmTime = alarmTime) + setTasks(tasks + newTask) + newTask + } + val deleteTask = { task: Task -> + // Cancel alarm saat task dihapus + task.alarmTime?.let { + cancelAlarm(context, task) + } + setTasks(tasks.filter { it.id != task.id }) + } + val toggleTaskCompletion = { task: Task -> + // Cancel alarm saat task diselesaikan + if (!task.isCompleted && task.alarmTime != null) { + cancelAlarm(context, task) + } + val updatedTasks = tasks.map { + if (it.id == task.id) { + it.copy(isCompleted = !it.isCompleted, completionDate = if (!it.isCompleted) LocalDate.now() else null) + } else { + it + } + } + setTasks(updatedTasks) + } + val editTask: (Task, String, LocalDate?, LocalDateTime?) -> Unit = { taskToUpdate, newText, newDeadline, newAlarmTime -> + setTasks(tasks.map { if (it.id == taskToUpdate.id) it.copy(text = newText, deadline = newDeadline, alarmTime = newAlarmTime) else it }) + } + + Box( + modifier = Modifier + .fillMaxSize() + .background( + brush = Brush.verticalGradient( + colors = listOf( + Color(0xFFE1F5FE), // Biru langit sangat muda + Color(0xFFB3E5FC), // Biru langit muda + Color(0xFF81D4FA) // Biru langit cerah + ) + ) + ) + ) { + Scaffold( + topBar = { + TopAppBar( + title = { + Text( + when (currentTab) { + MainScreenTab.Active -> "Tugas Aktif" + MainScreenTab.Completed -> "Tugas Selesai" + MainScreenTab.Statistics -> "Statistik" + }, + color = Color(0xFF1565C0) + ) + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = Color(0xFFB3E5FC).copy(alpha = 0.9f) + ) + ) + }, + bottomBar = { MainNavigationBar(currentTab = currentTab, onTabChange = { currentTab = it }) }, + containerColor = Color.Transparent + ) { paddingValues -> + Crossfade(targetState = currentTab, animationSpec = tween(300)) { screen -> + when (screen) { + MainScreenTab.Active -> ActiveTasksScreen(paddingValues, activeTasks, addTask, toggleTaskCompletion, editTask, deleteTask) + MainScreenTab.Completed -> CompletedTasksScreen(paddingValues, completedTasks, toggleTaskCompletion, deleteTask) + MainScreenTab.Statistics -> StatisticsScreen(paddingValues, tasks) + } + } + } + } +} + +@Composable +fun MainNavigationBar(currentTab: MainScreenTab, onTabChange: (MainScreenTab) -> Unit) { + NavigationBar( + containerColor = Color(0xFFB3E5FC).copy(alpha = 0.95f) + ) { + NavigationBarItem( + icon = { Icon(Icons.Filled.List, null) }, + label = { Text("Aktif") }, + selected = currentTab == MainScreenTab.Active, + onClick = { onTabChange(MainScreenTab.Active) }, + colors = NavigationBarItemDefaults.colors( + selectedIconColor = Color(0xFF1976D2), + selectedTextColor = Color(0xFF1976D2), + indicatorColor = Color(0xFF81D4FA) + ) + ) + NavigationBarItem( + icon = { Icon(Icons.Filled.Check, null) }, + label = { Text("Selesai") }, + selected = currentTab == MainScreenTab.Completed, + onClick = { onTabChange(MainScreenTab.Completed) }, + colors = NavigationBarItemDefaults.colors( + selectedIconColor = Color(0xFF1976D2), + selectedTextColor = Color(0xFF1976D2), + indicatorColor = Color(0xFF81D4FA) + ) + ) + NavigationBarItem( + icon = { Icon(Icons.Filled.BarChart, null) }, + label = { Text("Statistik") }, + selected = currentTab == MainScreenTab.Statistics, + onClick = { onTabChange(MainScreenTab.Statistics) }, + colors = NavigationBarItemDefaults.colors( + selectedIconColor = Color(0xFF1976D2), + selectedTextColor = Color(0xFF1976D2), + indicatorColor = Color(0xFF81D4FA) + ) + ) + } +} + +// ----- SCREENS & DIALOGS ----- +@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) +@Composable +fun ActiveTasksScreen( + paddings: PaddingValues, + tasks: List, + onAddTask: (String, LocalDate?, LocalDateTime?) -> Task, + onToggle: (Task) -> Unit, + onEdit: (Task, String, LocalDate?, LocalDateTime?) -> Unit, + onDelete: (Task) -> Unit +) { + var text by remember { mutableStateOf("") } + var deadline by remember { mutableStateOf(null) } + var alarmTime by remember { mutableStateOf(null) } + var showDatePicker by remember { mutableStateOf(false) } + var showTimePicker by remember { mutableStateOf(false) } + var showEditDialog by remember { mutableStateOf(false) } + var taskToEdit by remember { mutableStateOf(null) } + + // --- Voice Recognition Logic --- + val context = LocalContext.current + val speechRecognizerLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult(), + onResult = { result -> + if (result.resultCode == Activity.RESULT_OK) { + val data: Intent? = result.data + val results = data?.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS) + text = results?.get(0) ?: "" + } + } + ) + + val permissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + onResult = { isGranted: Boolean -> + if (isGranted) { + val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply { + putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM) + putExtra(RecognizerIntent.EXTRA_PROMPT, "Ucapkan tugas Anda...") + } + speechRecognizerLauncher.launch(intent) + } + } + ) + // --- End of Voice Logic --- + + Column(Modifier.padding(paddings)) { + // New Input Section + Card( + modifier = Modifier.fillMaxWidth().padding(16.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 8.dp), + shape = MaterialTheme.shapes.large, + colors = CardDefaults.cardColors( + containerColor = Color.White + ) + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + "Tugas Baru", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = Color(0xFF1976D2) + ) + Spacer(Modifier.height(8.dp)) + OutlinedTextField( + value = text, + onValueChange = { text = it }, + label = { Text("Apa yang perlu dikerjakan?") }, + modifier = Modifier.fillMaxWidth(), + colors = OutlinedTextFieldDefaults.colors( + focusedTextColor = Color(0xFF212121), + unfocusedTextColor = Color(0xFF212121), + cursorColor = Color(0xFF1976D2), + focusedBorderColor = Color(0xFF1976D2), + focusedLabelColor = Color(0xFF1976D2) + ), + trailingIcon = { + IconButton(onClick = { + when (ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO)) { + PackageManager.PERMISSION_GRANTED -> { + val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply { + putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM) + putExtra(RecognizerIntent.EXTRA_PROMPT, "Ucapkan tugas Anda...") + } + speechRecognizerLauncher.launch(intent) + } + else -> { + permissionLauncher.launch(Manifest.permission.RECORD_AUDIO) + } + } + }) { + Icon(Icons.Default.Mic, contentDescription = "Input Suara") + } + } + ) + Spacer(Modifier.height(12.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column { + TextButton(onClick = { showDatePicker = true }) { + Icon(Icons.Default.Event, contentDescription = "Pilih Deadline", modifier = Modifier.size(18.dp)) + Spacer(Modifier.width(8.dp)) + Text(deadline?.format(DateTimeFormatter.ofPattern("dd MMM")) ?: "Deadline") + } + TextButton(onClick = { showTimePicker = true }) { + Icon(Icons.Default.Alarm, contentDescription = "Set Alarm", modifier = Modifier.size(18.dp)) + Spacer(Modifier.width(8.dp)) + Text( + alarmTime?.format(DateTimeFormatter.ofPattern("HH:mm, dd MMM")) ?: "Set Alarm", + fontSize = 14.sp + ) + } + } + Button( + onClick = { + if (text.isNotBlank()) { + val newTask = onAddTask(text, deadline, alarmTime) + alarmTime?.let { + scheduleAlarm(context, newTask, it) + } + text = "" + deadline = null + alarmTime = null + } + }, + enabled = text.isNotBlank(), + colors = ButtonDefaults.buttonColors( + containerColor = Color(0xFF1976D2) + ) + ) { + Text("Simpan") + } + } + } + } + + if (tasks.isEmpty()) { + EmptyState("Hebat! Tidak ada tugas yang perlu dikerjakan.", Icons.Filled.CheckCircle) + } else { + LazyColumn(contentPadding = PaddingValues(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { + items(tasks, key = { it.id }) { task -> + Box(Modifier.animateItemPlacement()) { + TaskItem(task, { onToggle(task) }, { taskToEdit = task; showEditDialog = true }, { onDelete(task) }) + } + } + } + } + } + + if (showDatePicker) { + val datePickerState = rememberDatePickerState() + DatePickerDialog( + onDismissRequest = { showDatePicker = false }, + confirmButton = { + Button( + onClick = { + datePickerState.selectedDateMillis?.let { + deadline = Instant.ofEpochMilli(it).atZone(ZoneId.systemDefault()).toLocalDate() + } + showDatePicker = false + }, + colors = ButtonDefaults.buttonColors( + containerColor = Color(0xFF1976D2) + ) + ) { Text("OK") } + }, + dismissButton = { Button(onClick = { showDatePicker = false }) { Text("Batal") } } + ) { DatePicker(state = datePickerState) } + } + + if (showTimePicker) { + val timePickerState = rememberTimePickerState() + var selectedDate by remember { mutableStateOf(deadline ?: LocalDate.now()) } + var showDatePickerInTime by remember { mutableStateOf(false) } + + AlertDialog( + onDismissRequest = { showTimePicker = false }, + title = { Text("Pilih Waktu Alarm") }, + text = { + Column { + Button( + onClick = { showDatePickerInTime = true }, + modifier = Modifier.fillMaxWidth() + ) { + Icon(Icons.Default.CalendarToday, null, modifier = Modifier.size(18.dp)) + Spacer(Modifier.width(8.dp)) + Text("Tanggal: ${selectedDate.format(DateTimeFormatter.ofPattern("dd MMM yyyy"))}") + } + Spacer(Modifier.height(16.dp)) + TimePicker(state = timePickerState) + } + }, + confirmButton = { + Button( + onClick = { + alarmTime = LocalDateTime.of( + selectedDate, + LocalTime.of(timePickerState.hour, timePickerState.minute) + ) + showTimePicker = false + }, + colors = ButtonDefaults.buttonColors( + containerColor = Color(0xFF1976D2) + ) + ) { Text("OK") } + }, + dismissButton = { Button(onClick = { showTimePicker = false }) { Text("Batal") } } + ) + + if (showDatePickerInTime) { + val datePickerState2 = rememberDatePickerState( + initialSelectedDateMillis = selectedDate.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli() + ) + DatePickerDialog( + onDismissRequest = { showDatePickerInTime = false }, + confirmButton = { + Button(onClick = { + datePickerState2.selectedDateMillis?.let { + selectedDate = Instant.ofEpochMilli(it).atZone(ZoneId.systemDefault()).toLocalDate() + } + showDatePickerInTime = false + }) { Text("OK") } + }, + dismissButton = { Button(onClick = { showDatePickerInTime = false }) { Text("Batal") } } + ) { DatePicker(state = datePickerState2) } + } + } + + if (showEditDialog) { + EditTaskDialog( + taskToEdit!!, + { showEditDialog = false }, + { newText, newDeadline, newAlarmTime -> + // Cancel old alarm if exists + taskToEdit?.alarmTime?.let { + cancelAlarm(context, taskToEdit!!) + } + onEdit(taskToEdit!!, newText, newDeadline, newAlarmTime) + // Schedule new alarm if exists + newAlarmTime?.let { + scheduleAlarm(context, taskToEdit!!.copy(text = newText, deadline = newDeadline, alarmTime = newAlarmTime), it) + } + showEditDialog = false + } + ) + } +} + +// Helper functions for alarm management +fun scheduleAlarm(context: Context, task: Task, alarmTime: LocalDateTime) { + val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + val intent = Intent(context, AlarmReceiver::class.java).apply { + putExtra("task_text", task.text) + putExtra("task_id", task.id) + } + val pendingIntent = PendingIntent.getBroadcast( + context, + task.id, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + val alarmTimeMillis = alarmTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (alarmManager.canScheduleExactAlarms()) { + alarmManager.setExact(AlarmManager.RTC_WAKEUP, alarmTimeMillis, pendingIntent) + } + } else { + alarmManager.setExact(AlarmManager.RTC_WAKEUP, alarmTimeMillis, pendingIntent) + } +} + +fun cancelAlarm(context: Context, task: Task) { + val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + val intent = Intent(context, AlarmReceiver::class.java) + val pendingIntent = PendingIntent.getBroadcast( + context, + task.id, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + alarmManager.cancel(pendingIntent) +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun CompletedTasksScreen(paddings: PaddingValues, tasks: List, onToggle: (Task) -> Unit, onDelete: (Task) -> Unit) { + if (tasks.isEmpty()) { + EmptyState("Belum ada tugas yang selesai.", Icons.Filled.Archive) + } else { + LazyColumn(Modifier.padding(paddings), contentPadding = PaddingValues(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { + items(tasks, key = { it.id }) { task -> + Box(Modifier.animateItemPlacement()) { + CompletedTaskItem(task, { onToggle(task) }, { onDelete(task) }) + } + } + } + } +} + +// ----- STATISTICS SCREEN ----- +@Composable +fun StatisticsScreen(paddings: PaddingValues, tasks: List) { + val completedTasks = tasks.filter { it.isCompleted } + val activeTasks = tasks.filter { !it.isCompleted } + + val today = LocalDate.now() + val completedToday = completedTasks.filter { it.completionDate == today } + val completedThisWeek = completedTasks.filter { + it.completionDate?.let { date -> + date.isAfter(today.minusDays(7)) && !date.isAfter(today) + } ?: false + } + + val overdueTasks = activeTasks.filter { + it.deadline?.isBefore(today) == true + } + + val completedLateTasks = completedTasks.filter { task -> + task.deadline != null && task.completionDate != null && + task.completionDate!!.isAfter(task.deadline!!) + } + + val totalLateTasks = overdueTasks.size + completedLateTasks.size + + val completedOnTime = completedTasks.filter { task -> + task.deadline != null && task.completionDate != null && + !task.completionDate!!.isAfter(task.deadline!!) + } + + val completionRate = if (tasks.isNotEmpty()) { + (completedTasks.size.toFloat() / tasks.size.toFloat() * 100).toInt() + } else 0 + + val onTimeRate = if (completedTasks.isNotEmpty()) { + (completedOnTime.size.toFloat() / completedTasks.size.toFloat() * 100).toInt() + } else 0 + + LazyColumn( + Modifier.padding(paddings), + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + // Header Card + item { + Card( + colors = CardDefaults.cardColors(containerColor = Color(0xFF1976D2)), + shape = RoundedCornerShape(16.dp) + ) { + Column( + Modifier.padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + Icons.Default.EmojiEvents, + contentDescription = null, + modifier = Modifier.size(64.dp), + tint = Color.White + ) + Spacer(Modifier.height(12.dp)) + Text( + "Produktivitas Kamu", + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + color = Color.White + ) + Text( + "${completedTasks.size} tugas selesai!", + fontSize = 16.sp, + color = Color.White.copy(alpha = 0.9f) + ) + } + } + } + + // Progress Overview + item { + Text( + "📊 Ringkasan", + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1565C0) + ) + } + + item { + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + StatCard( + title = "Hari Ini", + value = completedToday.size.toString(), + icon = Icons.Default.Today, + color = Color(0xFF4CAF50), + modifier = Modifier.weight(1f) + ) + StatCard( + title = "Minggu Ini", + value = completedThisWeek.size.toString(), + icon = Icons.Default.DateRange, + color = Color(0xFF2196F3), + modifier = Modifier.weight(1f) + ) + } + } + + item { + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + StatCard( + title = "Total Tugas", + value = tasks.size.toString(), + icon = Icons.Default.Assignment, + color = Color(0xFF9C27B0), + modifier = Modifier.weight(1f) + ) + StatCard( + title = "Terlambat", + value = totalLateTasks.toString(), + icon = Icons.Default.Warning, + color = Color(0xFFF44336), + modifier = Modifier.weight(1f) + ) + } + } + + // Progress Bars + item { + Spacer(Modifier.height(8.dp)) + Text( + "📈 Progress", + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1565C0) + ) + } + + item { + Card( + colors = CardDefaults.cardColors(containerColor = Color.White) + ) { + Column(Modifier.padding(16.dp)) { + ProgressItem( + label = "Tingkat Penyelesaian", + value = completionRate, + color = Color(0xFF4CAF50), + description = "${completedTasks.size} dari ${tasks.size} tugas" + ) + Spacer(Modifier.height(16.dp)) + ProgressItem( + label = "Ketepatan Waktu", + value = onTimeRate, + color = Color(0xFF2196F3), + description = "${completedOnTime.size} dari ${completedTasks.size} tepat waktu" + ) + } + } + } + + // Achievements + item { + Spacer(Modifier.height(8.dp)) + Text( + "🏆 Pencapaian", + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + color = Color(0xFF1565C0) + ) + } + + item { + Card( + colors = CardDefaults.cardColors(containerColor = Color.White) + ) { + Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { + AchievementItem( + title = "Pemula", + description = "Selesaikan 1 tugas", + isUnlocked = completedTasks.size >= 1, + icon = "🌱" + ) + AchievementItem( + title = "Produktif", + description = "Selesaikan 5 tugas", + isUnlocked = completedTasks.size >= 5, + icon = "⭐" + ) + AchievementItem( + title = "Rajin", + description = "Selesaikan 10 tugas", + isUnlocked = completedTasks.size >= 10, + icon = "🔥" + ) + AchievementItem( + title = "Master", + description = "Selesaikan 20 tugas", + isUnlocked = completedTasks.size >= 20, + icon = "👑" + ) + AchievementItem( + title = "Tepat Waktu", + description = "10 tugas selesai on-time", + isUnlocked = completedOnTime.size >= 10, + icon = "⏰" + ) + } + } + } + } +} + +@Composable +fun StatCard( + title: String, + value: String, + icon: androidx.compose.ui.graphics.vector.ImageVector, + color: Color, + modifier: Modifier = Modifier +) { + Card( + modifier = modifier, + colors = CardDefaults.cardColors(containerColor = Color.White), + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) + ) { + Column( + Modifier.padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + icon, + contentDescription = null, + tint = color, + modifier = Modifier.size(32.dp) + ) + Spacer(Modifier.height(8.dp)) + Text( + value, + fontSize = 28.sp, + fontWeight = FontWeight.Bold, + color = color + ) + Text( + title, + fontSize = 12.sp, + color = Color.Gray, + textAlign = TextAlign.Center + ) + } + } +} + +@Composable +fun ProgressItem(label: String, value: Int, color: Color, description: String) { + Column { + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text(label, fontWeight = FontWeight.SemiBold, color = Color(0xFF424242)) + Text("$value%", fontWeight = FontWeight.Bold, color = color, fontSize = 18.sp) + } + Spacer(Modifier.height(8.dp)) + LinearProgressIndicator( + progress = value / 100f, + modifier = Modifier + .fillMaxWidth() + .height(12.dp) + .clip(RoundedCornerShape(6.dp)), + color = color, + trackColor = color.copy(alpha = 0.2f), + ) + Spacer(Modifier.height(4.dp)) + Text(description, fontSize = 12.sp, color = Color.Gray) + } +} + +@Composable +fun AchievementItem(title: String, description: String, isUnlocked: Boolean, icon: String) { + Row( + Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .background(if (isUnlocked) Color(0xFFE3F2FD) else Color(0xFFF5F5F5)) + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + icon, + fontSize = 32.sp, + modifier = Modifier + .size(48.dp) + .background( + if (isUnlocked) Color(0xFF1976D2).copy(alpha = 0.1f) else Color.White, + RoundedCornerShape(8.dp) + ) + .padding(8.dp) + ) + Spacer(Modifier.width(12.dp)) + Column(Modifier.weight(1f)) { + Text( + title, + fontWeight = FontWeight.Bold, + color = if (isUnlocked) Color(0xFF1976D2) else Color.Gray + ) + Text( + description, + fontSize = 12.sp, + color = Color.Gray + ) + } + if (isUnlocked) { + Icon( + Icons.Default.CheckCircle, + contentDescription = "Unlocked", + tint = Color(0xFF4CAF50), + modifier = Modifier.size(24.dp) + ) + } else { + Icon( + Icons.Default.Lock, + contentDescription = "Locked", + tint = Color.Gray, + modifier = Modifier.size(24.dp) + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun EditTaskDialog(task: Task, onDismiss: () -> Unit, onSave: (String, LocalDate?, LocalDateTime?) -> Unit) { + var editedText by remember { mutableStateOf(task.text) } + var editedDeadline by remember { mutableStateOf(task.deadline) } + var editedAlarmTime by remember { mutableStateOf(task.alarmTime) } + var showDatePicker by remember { mutableStateOf(false) } + var showTimePicker by remember { mutableStateOf(false) } + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text("Edit Tugas") }, + text = { + Column { + OutlinedTextField( + editedText, + { editedText = it }, + Modifier.fillMaxWidth(), + label = { Text("Nama Tugas") }, + colors = OutlinedTextFieldDefaults.colors( + focusedTextColor = Color(0xFF212121), + unfocusedTextColor = Color(0xFF212121), + cursorColor = Color(0xFF1976D2), + focusedBorderColor = Color(0xFF1976D2), + focusedLabelColor = Color(0xFF1976D2) + ) + ) + Spacer(Modifier.height(16.dp)) + Button(onClick = { showDatePicker = true }, modifier = Modifier.fillMaxWidth()) { + Text(editedDeadline?.format(DateTimeFormatter.ofPattern("dd MMM yyyy")) ?: "Pilih Deadline") + } + Spacer(Modifier.height(8.dp)) + Button(onClick = { showTimePicker = true }, modifier = Modifier.fillMaxWidth()) { + Text(editedAlarmTime?.format(DateTimeFormatter.ofPattern("HH:mm, dd MMM")) ?: "Set Alarm") + } + } + }, + confirmButton = { + Button( + onClick = { if (editedText.isNotBlank()) onSave(editedText, editedDeadline, editedAlarmTime) }, + colors = ButtonDefaults.buttonColors( + containerColor = Color(0xFF1976D2) + ) + ) { Text("Simpan") } + }, + dismissButton = { Button(onClick = onDismiss) { Text("Batal") } } + ) + + if (showDatePicker) { + val pickerState = rememberDatePickerState(initialSelectedDateMillis = editedDeadline?.atStartOfDay(ZoneId.systemDefault())?.toInstant()?.toEpochMilli()) + DatePickerDialog( + onDismissRequest = { showDatePicker = false }, + confirmButton = { + Button( + onClick = { + pickerState.selectedDateMillis?.let { + editedDeadline = Instant.ofEpochMilli(it).atZone(ZoneId.systemDefault()).toLocalDate() + } + showDatePicker = false + }, + colors = ButtonDefaults.buttonColors( + containerColor = Color(0xFF1976D2) + ) + ) { Text("OK") } + } + ) { DatePicker(state = pickerState) } + } + + if (showTimePicker) { + val timePickerState = rememberTimePickerState( + initialHour = editedAlarmTime?.hour ?: 0, + initialMinute = editedAlarmTime?.minute ?: 0 + ) + var selectedDate by remember { mutableStateOf(editedAlarmTime?.toLocalDate() ?: editedDeadline ?: LocalDate.now()) } + var showDatePickerInTime by remember { mutableStateOf(false) } + + AlertDialog( + onDismissRequest = { showTimePicker = false }, + title = { Text("Pilih Waktu Alarm") }, + text = { + Column { + Button( + onClick = { showDatePickerInTime = true }, + modifier = Modifier.fillMaxWidth() + ) { + Icon(Icons.Default.CalendarToday, null, modifier = Modifier.size(18.dp)) + Spacer(Modifier.width(8.dp)) + Text("Tanggal: ${selectedDate.format(DateTimeFormatter.ofPattern("dd MMM yyyy"))}") + } + Spacer(Modifier.height(16.dp)) + TimePicker(state = timePickerState) + } + }, + confirmButton = { + Button( + onClick = { + editedAlarmTime = LocalDateTime.of( + selectedDate, + LocalTime.of(timePickerState.hour, timePickerState.minute) + ) + showTimePicker = false + }, + colors = ButtonDefaults.buttonColors( + containerColor = Color(0xFF1976D2) + ) + ) { Text("OK") } + }, + dismissButton = { Button(onClick = { showTimePicker = false }) { Text("Batal") } } + ) + + if (showDatePickerInTime) { + val datePickerState2 = rememberDatePickerState( + initialSelectedDateMillis = selectedDate.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli() + ) + DatePickerDialog( + onDismissRequest = { showDatePickerInTime = false }, + confirmButton = { + Button(onClick = { + datePickerState2.selectedDateMillis?.let { + selectedDate = Instant.ofEpochMilli(it).atZone(ZoneId.systemDefault()).toLocalDate() + } + showDatePickerInTime = false + }) { Text("OK") } + }, + dismissButton = { Button(onClick = { showDatePickerInTime = false }) { Text("Batal") } } + ) { DatePicker(state = datePickerState2) } + } + } +} + +// ----- UI COMPONENTS ----- +@Composable +fun TaskItem(task: Task, onToggle: () -> Unit, onEdit: () -> Unit, onDelete: () -> Unit) { + val isOverdue = task.deadline?.isBefore(LocalDate.now()) == true + val deadlineColor = if (isOverdue) MaterialTheme.colorScheme.error else Color(0xFF1976D2) + + Card( + modifier = Modifier.fillMaxWidth().shadow(2.dp, RoundedCornerShape(12.dp)), + shape = RoundedCornerShape(12.dp), + colors = CardDefaults.cardColors( + containerColor = Color.White + ) + ) { + Row(Modifier.padding(start = 8.dp).height(IntrinsicSize.Min), verticalAlignment = Alignment.CenterVertically) { + Checkbox( + checked = false, + onCheckedChange = { onToggle() }, + colors = CheckboxDefaults.colors( + checkedColor = Color(0xFF1976D2) + ) + ) + Box(Modifier.fillMaxHeight().width(4.dp).background(deadlineColor.copy(alpha = 0.5f)).clip(RoundedCornerShape(2.dp))) + Column(Modifier.padding(horizontal = 12.dp, vertical = 8.dp).weight(1f)) { + Text(task.text, fontSize = 17.sp, fontWeight = FontWeight.SemiBold, color = Color(0xFF424242)) + task.deadline?.let { + Spacer(Modifier.height(4.dp)) + Row(verticalAlignment = Alignment.CenterVertically) { + Icon(Icons.Default.Event, contentDescription = "Deadline", modifier = Modifier.size(14.dp), tint = deadlineColor) + Spacer(Modifier.width(4.dp)) + Text(it.format(DateTimeFormatter.ofPattern("dd MMM yyyy")), color = deadlineColor, fontSize = 13.sp) + if (isOverdue) { + Spacer(Modifier.width(8.dp)) + Text("TERLAMBAT!", color = MaterialTheme.colorScheme.error, fontSize = 10.sp, fontWeight = FontWeight.Bold) + } + } + } + task.alarmTime?.let { + Spacer(Modifier.height(4.dp)) + Row(verticalAlignment = Alignment.CenterVertically) { + Icon(Icons.Default.Alarm, contentDescription = "Alarm", modifier = Modifier.size(14.dp), tint = Color(0xFF1976D2)) + Spacer(Modifier.width(4.dp)) + val now = LocalDateTime.now() + val isAlarmSoon = it.isAfter(now) && it.isBefore(now.plusMinutes(1)) + Text( + it.format(DateTimeFormatter.ofPattern("HH:mm, dd MMM")), + color = if (isAlarmSoon) Color(0xFFFF6F00) else Color(0xFF1976D2), + fontSize = 13.sp, + fontWeight = if (isAlarmSoon) FontWeight.Bold else FontWeight.Medium + ) + if (isAlarmSoon) { + Spacer(Modifier.width(4.dp)) + Text("🔥", fontSize = 12.sp) + } + } + } + } + IconButton(onClick = onEdit) { Icon(Icons.Default.Edit, "Edit", tint = Color(0xFF1976D2)) } + IconButton(onClick = onDelete) { Icon(Icons.Default.Delete, "Hapus", tint = MaterialTheme.colorScheme.error) } + } + } +} + +@Composable +fun CompletedTaskItem(task: Task, onRestore: () -> Unit, onDelete: () -> Unit) { + val wasCompletedLate = task.deadline != null && task.completionDate != null && task.completionDate!!.isAfter(task.deadline) + Card( + colors = CardDefaults.cardColors( + containerColor = Color(0xFFE1F5FE).copy(alpha = 0.7f) + ) + ) { + Row(Modifier.padding(horizontal = 16.dp, vertical = 12.dp), verticalAlignment = Alignment.CenterVertically) { + Icon(Icons.Filled.CheckCircle, "Selesai", tint = Color(0xFF66BB6A)) + Spacer(Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text(task.text, textDecoration = TextDecoration.LineThrough, color = Color.Gray) + task.completionDate?.let { + Spacer(Modifier.height(4.dp)) + Text( + text = "Selesai pada: ${it.format(DateTimeFormatter.ofPattern("dd MMM yyyy"))}", + fontSize = 12.sp, + color = Color.Gray + ) + } + if (wasCompletedLate) { + Spacer(Modifier.height(4.dp)) + Text( + text = "Tugas ini terlambat diselesaikan!", + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.error + ) + } + } + IconButton(onClick = onRestore) { Icon(Icons.Default.Refresh, "Kembalikan", tint = Color(0xFF1976D2)) } + IconButton(onClick = onDelete) { Icon(Icons.Default.DeleteForever, "Hapus", tint = MaterialTheme.colorScheme.error) } + } + } +} + +@Composable +fun EmptyState(message: String, icon: androidx.compose.ui.graphics.vector.ImageVector) { + Column(Modifier.fillMaxSize().padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) { + Icon(icon, null, Modifier.size(72.dp), tint = Color(0xFF64B5F6).copy(alpha = 0.5f)) + Spacer(Modifier.height(16.dp)) + Text(message, color = Color(0xFF1565C0), textAlign = TextAlign.Center) + } +} + +// ----- PREVIEW ----- +@Preview(showBackground = true) +@Composable +fun DefaultPreview() { + MyApplicationTheme { + AppNavigationHost() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kelompok10/ToDoList/ui/theme/Color.kt b/app/src/main/java/com/kelompok10/ToDoList/ui/theme/Color.kt new file mode 100644 index 0000000..f1156ab --- /dev/null +++ b/app/src/main/java/com/kelompok10/ToDoList/ui/theme/Color.kt @@ -0,0 +1,66 @@ +package com.kelompok10.ToDoList.ui.theme + +import androidx.compose.ui.graphics.Color + +// New Cool-toned Palette +val md_theme_light_primary = Color(0xFF00668B) +val md_theme_light_onPrimary = Color(0xFFFFFFFF) +val md_theme_light_primaryContainer = Color(0xFFC3E8FF) +val md_theme_light_onPrimaryContainer = Color(0xFF001E2C) +val md_theme_light_secondary = Color(0xFF006C4C) +val md_theme_light_onSecondary = Color(0xFFFFFFFF) +val md_theme_light_secondaryContainer = Color(0xFF89F8C6) +val md_theme_light_onSecondaryContainer = Color(0xFF002114) +val md_theme_light_tertiary = Color(0xFF835400) +val md_theme_light_onTertiary = Color(0xFFFFFFFF) +val md_theme_light_tertiaryContainer = Color(0xFFFFDDB8) +val md_theme_light_onTertiaryContainer = Color(0xFF291800) +val md_theme_light_error = Color(0xFFBA1A1A) +val md_theme_light_errorContainer = Color(0xFFFFDAD6) +val md_theme_light_onError = Color(0xFFFFFFFF) +val md_theme_light_onErrorContainer = Color(0xFF410002) +val md_theme_light_background = Color(0xFFFBFCFE) +val md_theme_light_onBackground = Color(0xFF191C1E) +val md_theme_light_surface = Color(0xFFFBFCFE) +val md_theme_light_onSurface = Color(0xFF191C1E) +val md_theme_light_surfaceVariant = Color(0xFFDCE3E9) +val md_theme_light_onSurfaceVariant = Color(0xFF40484D) +val md_theme_light_outline = Color(0xFF70787D) +val md_theme_light_inverseOnSurface = Color(0xFFF0F1F3) +val md_theme_light_inverseSurface = Color(0xFF2E3133) +val md_theme_light_inversePrimary = Color(0xFF79D1FF) +val md_theme_light_shadow = Color(0xFF000000) +val md_theme_light_surfaceTint = Color(0xFF00668B) +val md_theme_light_outlineVariant = Color(0xFFC0C8CD) +val md_theme_light_scrim = Color(0xFF000000) + +val md_theme_dark_primary = Color(0xFF79D1FF) +val md_theme_dark_onPrimary = Color(0xFF00354A) +val md_theme_dark_primaryContainer = Color(0xFF004C69) +val md_theme_dark_onPrimaryContainer = Color(0xFFC3E8FF) +val md_theme_dark_secondary = Color(0xFF6DDBAB) +val md_theme_dark_onSecondary = Color(0xFF003826) +val md_theme_dark_secondaryContainer = Color(0xFF005138) +val md_theme_dark_onSecondaryContainer = Color(0xFF89F8C6) +val md_theme_dark_tertiary = Color(0xFFFFB961) +val md_theme_dark_onTertiary = Color(0xFF452B00) +val md_theme_dark_tertiaryContainer = Color(0xFF633F00) +val md_theme_dark_onTertiaryContainer = Color(0xFFFFDDB8) +val md_theme_dark_error = Color(0xFFFFB4AB) +val md_theme_dark_errorContainer = Color(0xFF93000A) +val md_theme_dark_onError = Color(0xFF690005) +val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) +val md_theme_dark_background = Color(0xFF191C1E) +val md_theme_dark_onBackground = Color(0xFFE1E2E4) +val md_theme_dark_surface = Color(0xFF191C1E) +val md_theme_dark_onSurface = Color(0xFFE1E2E4) +val md_theme_dark_surfaceVariant = Color(0xFF40484D) +val md_theme_dark_onSurfaceVariant = Color(0xFFC0C8CD) +val md_theme_dark_outline = Color(0xFF8A9297) +val md_theme_dark_inverseOnSurface = Color(0xFF191C1E) +val md_theme_dark_inverseSurface = Color(0xFFE1E2E4) +val md_theme_dark_inversePrimary = Color(0xFF00668B) +val md_theme_dark_shadow = Color(0xFF000000) +val md_theme_dark_surfaceTint = Color(0xFF79D1FF) +val md_theme_dark_outlineVariant = Color(0xFF40484D) +val md_theme_dark_scrim = Color(0xFF000000) diff --git a/app/src/main/java/com/kelompok10/ToDoList/ui/theme/Shape.kt b/app/src/main/java/com/kelompok10/ToDoList/ui/theme/Shape.kt new file mode 100644 index 0000000..5259749 --- /dev/null +++ b/app/src/main/java/com/kelompok10/ToDoList/ui/theme/Shape.kt @@ -0,0 +1,11 @@ +package com.kelompok10.ToDoList.ui.theme + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Shapes +import androidx.compose.ui.unit.dp + +val Shapes = Shapes( + small = RoundedCornerShape(4.dp), + medium = RoundedCornerShape(12.dp), + large = RoundedCornerShape(16.dp) +) diff --git a/app/src/main/java/com/kelompok10/ToDoList/ui/theme/Theme.kt b/app/src/main/java/com/kelompok10/ToDoList/ui/theme/Theme.kt new file mode 100644 index 0000000..2b5294d --- /dev/null +++ b/app/src/main/java/com/kelompok10/ToDoList/ui/theme/Theme.kt @@ -0,0 +1,100 @@ +package com.kelompok10.ToDoList.ui.theme + +import android.app.Activity +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat + +private val LightColorScheme = lightColorScheme( + primary = md_theme_light_primary, + onPrimary = md_theme_light_onPrimary, + primaryContainer = md_theme_light_primaryContainer, + onPrimaryContainer = md_theme_light_onPrimaryContainer, + secondary = md_theme_light_secondary, + onSecondary = md_theme_light_onSecondary, + secondaryContainer = md_theme_light_secondaryContainer, + onSecondaryContainer = md_theme_light_onSecondaryContainer, + tertiary = md_theme_light_tertiary, + onTertiary = md_theme_light_onTertiary, + tertiaryContainer = md_theme_light_tertiaryContainer, + onTertiaryContainer = md_theme_light_onTertiaryContainer, + error = md_theme_light_error, + errorContainer = md_theme_light_errorContainer, + onError = md_theme_light_onError, + onErrorContainer = md_theme_light_onErrorContainer, + background = md_theme_light_background, + onBackground = md_theme_light_onBackground, + surface = md_theme_light_surface, + onSurface = md_theme_light_onSurface, + surfaceVariant = md_theme_light_surfaceVariant, + onSurfaceVariant = md_theme_light_onSurfaceVariant, + outline = md_theme_light_outline, + inverseOnSurface = md_theme_light_inverseOnSurface, + inverseSurface = md_theme_light_inverseSurface, + inversePrimary = md_theme_light_inversePrimary, + surfaceTint = md_theme_light_surfaceTint, + outlineVariant = md_theme_light_outlineVariant, + scrim = md_theme_light_scrim, +) + +private val DarkColorScheme = darkColorScheme( + primary = md_theme_dark_primary, + onPrimary = md_theme_dark_onPrimary, + primaryContainer = md_theme_dark_primaryContainer, + onPrimaryContainer = md_theme_dark_onPrimaryContainer, + secondary = md_theme_dark_secondary, + onSecondary = md_theme_dark_onSecondary, + secondaryContainer = md_theme_dark_secondaryContainer, + onSecondaryContainer = md_theme_dark_onSecondaryContainer, + tertiary = md_theme_dark_tertiary, + onTertiary = md_theme_dark_onTertiary, + tertiaryContainer = md_theme_dark_tertiaryContainer, + onTertiaryContainer = md_theme_dark_onTertiaryContainer, + error = md_theme_dark_error, + errorContainer = md_theme_dark_errorContainer, + onError = md_theme_dark_onError, + onErrorContainer = md_theme_dark_onErrorContainer, + background = md_theme_dark_background, + onBackground = md_theme_dark_onBackground, + surface = md_theme_dark_surface, + onSurface = md_theme_dark_onSurface, + surfaceVariant = md_theme_dark_surfaceVariant, + onSurfaceVariant = md_theme_dark_onSurfaceVariant, + outline = md_theme_dark_outline, + inverseOnSurface = md_theme_dark_inverseOnSurface, + inverseSurface = md_theme_dark_inverseSurface, + inversePrimary = md_theme_dark_inversePrimary, + surfaceTint = md_theme_dark_surfaceTint, + outlineVariant = md_theme_dark_outlineVariant, + scrim = md_theme_dark_scrim, +) + +@Composable +fun MyApplicationTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit +) { + val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme + + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + window.statusBarColor = colorScheme.primary.toArgb() + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme + } + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + shapes = Shapes, + content = content + ) +} diff --git a/app/src/main/java/com/kelompok10/ToDoList/ui/theme/Type.kt b/app/src/main/java/com/kelompok10/ToDoList/ui/theme/Type.kt new file mode 100644 index 0000000..d8eeee6 --- /dev/null +++ b/app/src/main/java/com/kelompok10/ToDoList/ui/theme/Type.kt @@ -0,0 +1,32 @@ +package com.kelompok10.ToDoList.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ), + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) +) diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..ca3826a --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file 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..c4a603d --- /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..c4a603d --- /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-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..77f6489 Binary files /dev/null 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..a965f5e 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 new file mode 100644 index 0000000..6c36c8c Binary files /dev/null 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 new file mode 100644 index 0000000..3e81ad7 Binary files /dev/null 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..eb4d36f 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 new file mode 100644 index 0000000..9011cc9 Binary files /dev/null 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 new file mode 100644 index 0000000..a48e23b Binary files /dev/null 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..1676dfa 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 new file mode 100644 index 0000000..83c8ee7 Binary files /dev/null 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 new file mode 100644 index 0000000..028d199 Binary files /dev/null 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..a9fe4bc 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 new file mode 100644 index 0000000..db72a7d Binary files /dev/null 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 new file mode 100644 index 0000000..415d6d8 Binary files /dev/null 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..fe86b34 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 new file mode 100644 index 0000000..6dd245d Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..35a6499 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + ToDoList + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..e48770a --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +