Penyesuaian Migrasi (import Library), Fix Bug Aplikasi Crash, Menambahkan Fitur edit dan hapus pada kategori

This commit is contained in:
202310715297 RAIHAN ARIQ MUZAKKI 2025-12-13 15:45:43 +07:00
parent 63b10a3e1c
commit 3f84068d72
20 changed files with 285 additions and 198 deletions

View File

@ -4,10 +4,10 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-12-12T17:42:15.072692700Z">
<DropdownSelection timestamp="2025-12-13T07:41:36.634314200Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\dendi\.android\avd\Medium_Phone.avd" />
<DeviceId pluginId="PhysicalDevice" identifier="serial=RR8T103A6JZ" />
</handle>
</Target>
</DropdownSelection>

View File

@ -1,5 +1,6 @@
package com.example.notesai
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
@ -11,12 +12,10 @@ 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.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import java.util.UUID
import androidx.compose.ui.platform.LocalContext
@ -31,37 +30,13 @@ import com.example.notesai.presentation.screens.ai.AIHelperScreen
import com.example.notesai.presentation.screens.archive.ArchiveScreen
import com.example.notesai.presentation.screens.main.MainScreen
import com.example.notesai.presentation.screens.note.EditableFullScreenNoteView
import com.example.notesai.presentation.screens.starred.components.StarredNotesScreen
import com.example.notesai.presentation.screens.trash.components.TrashScreen
import com.example.notesai.presentation.screens.starred.StarredNotesScreen
import com.example.notesai.presentation.screens.trash.TrashScreen
import com.example.notesai.data.model.Note
import com.example.notesai.data.model.Category
import com.example.notesai.util.updateWhere
import kotlinx.coroutines.delay
// Data Classes
data class Category(
val id: String = UUID.randomUUID().toString(),
val name: String,
val gradientStart: Long,
val gradientEnd: Long,
val timestamp: Long = System.currentTimeMillis()
)
data class Note(
val id: String = UUID.randomUUID().toString(),
val categoryId: String,
val title: String,
val content: String,
val timestamp: Long = System.currentTimeMillis(),
val isArchived: Boolean = false,
val isDeleted: Boolean = false,
val isPinned: Boolean = false
)
data class ChatMessage(
val id: String = UUID.randomUUID().toString(),
val message: String,
val isUser: Boolean,
val timestamp: Long = System.currentTimeMillis()
)
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -300,6 +275,19 @@ fun NotesApp() {
categories = categories.filter { it.id != category.id }
notes = notes.filter { it.categoryId != category.id }
selectedCategory = null
},
onCategoryEdit = { category, newName, newGradientStart, newGradientEnd ->
categories = categories.updateWhere(
predicate = { it.id == category.id },
transform = {
it.copy(
name = newName,
gradientStart = newGradientStart,
gradientEnd = newGradientEnd,
timestamp = System.currentTimeMillis()
)
}
)
}
)
"starred" -> StarredNotesScreen(

View File

@ -1,5 +1,5 @@
package com.example.notesai.config
object APIKey {
const val GEMINI_API_KEY = "MY_GEMINI_KEY"
const val GEMINI_API_KEY = "AIzaSyBzC64RXsNtSERlts_FSd8HXKEpkLdT7-8"
}

View File

@ -9,8 +9,8 @@ import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.example.notesai.Category
import com.example.notesai.Note
import com.example.notesai.data.model.Note
import com.example.notesai.data.model.Category
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map

View File

@ -1,9 +0,0 @@
// File: data/local/PreferencesKeys.kt
package com.example.notesai.data.local
import androidx.datastore.preferences.core.stringPreferencesKey
object PreferencesKeys {
val CATEGORIES_KEY = stringPreferencesKey("categories")
val NOTES_KEY = stringPreferencesKey("notes")
}

View File

@ -1,67 +0,0 @@
// File: data/model/SerializableModels.kt
package com.example.notesai.data.model
import android.annotation.SuppressLint
import kotlinx.serialization.Serializable
@SuppressLint("UnsafeOptInUsageError")
@Serializable
data class SerializableCategory(
val id: String,
val name: String,
val gradientStart: Long,
val gradientEnd: Long,
val timestamp: Long
)
@SuppressLint("UnsafeOptInUsageError")
@Serializable
data class SerializableNote(
val id: String,
val categoryId: String,
val title: String,
val content: String,
val timestamp: Long,
val isArchived: Boolean,
val isDeleted: Boolean,
val isPinned: Boolean
)
// Extension functions untuk konversi
fun Category.toSerializable() = SerializableCategory(
id = id,
name = name,
gradientStart = gradientStart,
gradientEnd = gradientEnd,
timestamp = timestamp
)
fun SerializableCategory.toCategory() = Category(
id = id,
name = name,
gradientStart = gradientStart,
gradientEnd = gradientEnd,
timestamp = timestamp
)
fun Note.toSerializable() = SerializableNote(
id = id,
categoryId = categoryId,
title = title,
content = content,
timestamp = timestamp,
isArchived = isArchived,
isDeleted = isDeleted,
isPinned = isPinned
)
fun SerializableNote.toNote() = Note(
id = id,
categoryId = categoryId,
title = title,
content = content,
timestamp = timestamp,
isArchived = isArchived,
isDeleted = isDeleted,
isPinned = isPinned
)

View File

@ -25,7 +25,7 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.example.notesai.Note
import com.example.notesai.data.model.Note
@Composable
fun NoteDialog(

View File

@ -53,9 +53,9 @@ import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.example.notesai.Category
import com.example.notesai.ChatMessage
import com.example.notesai.Note
import com.example.notesai.data.model.Note
import com.example.notesai.data.model.ChatMessage
import com.example.notesai.data.model.Category
import com.example.notesai.config.APIKey
import com.example.notesai.presentation.screens.ai.components.ChatBubble
import com.example.notesai.presentation.screens.ai.components.CompactStatItem

View File

@ -23,7 +23,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.notesai.ChatMessage
import com.example.notesai.data.model.ChatMessage
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale

View File

@ -8,8 +8,8 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Archive
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import com.example.notesai.Category
import com.example.notesai.Note
import com.example.notesai.data.model.Note
import com.example.notesai.data.model.Category
import com.example.notesai.presentation.components.EmptyState
import com.example.notesai.presentation.screens.archive.components.ArchiveNoteCard

View File

@ -24,7 +24,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.example.notesai.Note
import com.example.notesai.data.model.Note
@Composable
fun ArchiveNoteCard(

View File

@ -1,10 +1,7 @@
// File: presentation/screens/main/MainScreen.kt
package com.example.notesai.presentation.screens.main
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
import androidx.compose.foundation.lazy.staggeredgrid.items
@ -15,12 +12,12 @@ import androidx.compose.material.icons.filled.Search
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.example.notesai.Category
import com.example.notesai.Note
import com.example.notesai.data.model.Category
import com.example.notesai.data.model.Note
import com.example.notesai.presentation.components.EmptyState
import com.example.notesai.presentation.screens.main.components.CategoryCard
import com.example.notesai.presentation.screens.main.components.NoteCard
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun MainScreen(
categories: List<Category>,
@ -30,7 +27,8 @@ fun MainScreen(
onCategoryClick: (Category) -> Unit,
onNoteClick: (Note) -> Unit,
onPinToggle: (Note) -> Unit,
onCategoryDelete: (Category) -> Unit
onCategoryDelete: (Category) -> Unit,
onCategoryEdit: (Category, String, Long, Long) -> Unit // Parameter baru
) {
Column(modifier = Modifier.fillMaxSize()) {
if (selectedCategory == null) {
@ -68,10 +66,15 @@ fun MainScreen(
items(filteredCategories) { category ->
CategoryCard(
category = category,
noteCount = notes.count { it.categoryId == category.id && !it.isDeleted && !it.isArchived },
noteCount = notes.count {
it.categoryId == category.id &&
!it.isDeleted &&
!it.isArchived
},
onClick = { onCategoryClick(category) },
onDelete = {
onCategoryDelete(category)
onDelete = { onCategoryDelete(category) },
onEdit = { name, gradientStart, gradientEnd ->
onCategoryEdit(category, name, gradientStart, gradientEnd)
}
)
}
@ -108,7 +111,7 @@ fun MainScreen(
NoteCard(
note = note,
onClick = { onNoteClick(note) },
onPinClick = { onPinToggle(note) },
onPinClick = { onPinToggle(note) }
)
}
}
@ -116,8 +119,3 @@ fun MainScreen(
}
}
}
@Composable
fun NoteCard(note: Note, onClick: () -> Unit, onPinClick: () -> Unit) {
TODO("Not yet implemented")
}

View File

@ -1,58 +1,44 @@
package com.example.notesai.presentation.screens.main.components
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Folder
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
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.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.example.notesai.Category
import com.example.notesai.data.model.Category
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun CategoryCard(
category: Category,
noteCount: Int,
onClick: () -> Unit,
onDelete: () -> Unit = {}
onDelete: () -> Unit = {},
onEdit: (String, Long, Long) -> Unit = { _, _, _ -> }
) {
var showDeleteConfirm by remember { mutableStateOf(false) }
var showEditDialog by remember { mutableStateOf(false) }
var showMenu by remember { mutableStateOf(false) }
// Delete confirmation dialog
// Delete Confirmation Dialog
if (showDeleteConfirm) {
AlertDialog(
onDismissRequest = { showDeleteConfirm = false },
title = { Text("Hapus Kategori?", color = Color.White) },
title = { Text("Pindahkan ke Sampah?", color = Color.White) },
text = {
Text("Kategori '${category.name}' dan semua catatan di dalamnya akan dihapus. Tindakan ini tidak dapat dibatalkan.", color = Color.White)
Text(
"Kategori '${category.name}' dan semua catatan di dalamnya akan dipindahkan ke sampah.",
color = Color.White
)
},
confirmButton = {
Button(
@ -81,6 +67,18 @@ fun CategoryCard(
)
}
// Edit Dialog
if (showEditDialog) {
EditCategoryDialog(
category = category,
onDismiss = { showEditDialog = false },
onSave = { name, gradientStart, gradientEnd ->
onEdit(name, gradientStart, gradientEnd)
showEditDialog = false
}
)
}
Card(
modifier = Modifier
.fillMaxWidth()
@ -124,22 +122,199 @@ fun CategoryCard(
)
}
// Delete button di top-right corner
IconButton(
onClick = {
showDeleteConfirm = true
},
modifier = Modifier
.align(Alignment.TopEnd)
.size(40.dp)
// Menu Button (Titik Tiga)
Box(
modifier = Modifier.align(Alignment.TopEnd)
) {
Icon(
Icons.Default.Close,
contentDescription = "Hapus kategori",
tint = Color.White.copy(0.7f),
modifier = Modifier.size(20.dp)
)
IconButton(
onClick = { showMenu = true }
) {
Icon(
Icons.Default.MoreVert,
contentDescription = "Menu",
tint = Color.White.copy(0.9f)
)
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false },
modifier = Modifier.background(Color(0xFF1E293B))
) {
DropdownMenuItem(
text = {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(
Icons.Default.Edit,
contentDescription = null,
tint = Color(0xFF6366F1),
modifier = Modifier.size(20.dp)
)
Text("Edit Kategori", color = Color.White)
}
},
onClick = {
showMenu = false
showEditDialog = true
}
)
DropdownMenuItem(
text = {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(
Icons.Default.Delete,
contentDescription = null,
tint = Color(0xFFEF4444),
modifier = Modifier.size(20.dp)
)
Text("Pindah ke Sampah", color = Color.White)
}
},
onClick = {
showMenu = false
showDeleteConfirm = true
}
)
}
}
}
}
}
@Composable
fun EditCategoryDialog(
category: Category,
onDismiss: () -> Unit,
onSave: (String, Long, Long) -> Unit
) {
val gradients = listOf(
Pair(0xFF6366F1L, 0xFFA855F7L),
Pair(0xFFEC4899L, 0xFFF59E0BL),
Pair(0xFF8B5CF6L, 0xFFEC4899L),
Pair(0xFF06B6D4L, 0xFF3B82F6L),
Pair(0xFF10B981L, 0xFF059669L),
Pair(0xFFF59E0BL, 0xFFEF4444L),
Pair(0xFF6366F1L, 0xFF8B5CF6L),
Pair(0xFFEF4444L, 0xFFDC2626L)
)
var name by remember { mutableStateOf(category.name) }
var selectedGradient by remember {
mutableStateOf(
gradients.indexOfFirst {
it.first == category.gradientStart && it.second == category.gradientEnd
}.takeIf { it >= 0 } ?: 0
)
}
AlertDialog(
onDismissRequest = onDismiss,
containerColor = Color(0xFF1E293B),
title = {
Text(
"Edit Kategori",
color = Color.White,
fontWeight = FontWeight.Bold
)
},
text = {
Column {
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Nama Kategori", color = Color(0xFF94A3B8)) },
modifier = Modifier.fillMaxWidth(),
colors = TextFieldDefaults.colors(
focusedTextColor = Color.White,
unfocusedTextColor = Color.White,
focusedContainerColor = Color(0xFF334155),
unfocusedContainerColor = Color(0xFF334155),
cursorColor = Color(0xFFA855F7),
focusedIndicatorColor = Color(0xFFA855F7),
unfocusedIndicatorColor = Color(0xFF64748B)
),
shape = RoundedCornerShape(12.dp)
)
Spacer(modifier = Modifier.height(20.dp))
Text(
"Pilih Gradient:",
style = MaterialTheme.typography.bodyMedium,
color = Color.White,
fontWeight = FontWeight.SemiBold
)
Spacer(modifier = Modifier.height(12.dp))
gradients.chunked(4).forEach { row ->
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
row.forEachIndexed { _, gradient ->
val globalIndex = gradients.indexOf(gradient)
Box(
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
.clip(RoundedCornerShape(12.dp))
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(gradient.first),
Color(gradient.second)
)
)
)
.clickable { selectedGradient = globalIndex },
contentAlignment = Alignment.Center
) {
if (selectedGradient == globalIndex) {
Icon(
Icons.Default.Check,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(24.dp)
)
}
}
}
}
Spacer(modifier = Modifier.height(8.dp))
}
}
},
confirmButton = {
Button(
onClick = {
if (name.isNotBlank()) {
val gradient = gradients[selectedGradient]
onSave(name, gradient.first, gradient.second)
}
},
enabled = name.isNotBlank(),
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent
),
modifier = Modifier.background(
brush = Brush.linearGradient(
colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
),
shape = RoundedCornerShape(8.dp)
)
) {
Text("Simpan", color = Color.White, fontWeight = FontWeight.Bold)
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Batal", color = Color(0xFF94A3B8))
}
}
)
}

View File

@ -16,7 +16,6 @@ import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.outlined.StarBorder
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Divider
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@ -30,8 +29,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.notesai.Note
import com.example.notesai.util.Constants.AppColors.Divider
import com.example.notesai.data.model.Note
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@ -112,7 +110,7 @@ fun NoteCard(
Spacer(modifier = Modifier.height(12.dp))
Divider(
HorizontalDivider(
color = Color(0xFF334155),
thickness = 1.dp
)

View File

@ -39,8 +39,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.notesai.Note
import com.example.notesai.util.Constants.AppColors.Divider
import com.example.notesai.data.model.Note
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale

View File

@ -1,4 +1,4 @@
package com.example.notesai.presentation.screens.starred.components
package com.example.notesai.presentation.screens.starred
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
@ -9,10 +9,10 @@ import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import com.example.notesai.Category
import com.example.notesai.Note
import com.example.notesai.presentation.components.EmptyState
import com.example.notesai.presentation.screens.starred.StarredNoteCard
import com.example.notesai.presentation.screens.starred.components.StarredNoteCard
import com.example.notesai.data.model.Note
import com.example.notesai.data.model.Category
@OptIn(ExperimentalMaterial3Api::class)
@Composable

View File

@ -1,4 +1,4 @@
package com.example.notesai.presentation.screens.starred
package com.example.notesai.presentation.screens.starred.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@ -28,7 +28,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.example.notesai.Note
import com.example.notesai.data.model.Note
@Composable
fun StarredNoteCard(

View File

@ -1,4 +1,4 @@
package com.example.notesai.presentation.screens.trash.components
package com.example.notesai.presentation.screens.trash
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
@ -8,9 +8,10 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import com.example.notesai.Category
import com.example.notesai.Note
import com.example.notesai.presentation.components.EmptyState
import com.example.notesai.presentation.screens.trash.components.TrashNoteCard
import com.example.notesai.data.model.Note
import com.example.notesai.data.model.Category
@Composable
fun TrashScreen(

View File

@ -24,7 +24,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.example.notesai.Note
import com.example.notesai.data.model.Note
@Composable
fun TrashNoteCard(

View File

@ -25,3 +25,7 @@ fun <T> List<T>.replaceWhere(predicate: (T) -> Boolean, transform: (T) -> T): Li
fun <T> List<T>.removeWhere(predicate: (T) -> Boolean): List<T> {
return this.filter { !predicate(it) }
}
fun <T> List<T>.updateWhere(predicate: (T) -> Boolean, transform: (T) -> T): List<T> {
return this.map { if (predicate(it)) transform(it) else it }
}