2514 lines
94 KiB
Kotlin

package com.example.notesai
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
import androidx.compose.foundation.lazy.staggeredgrid.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
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.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.UUID
import androidx.compose.material.icons.outlined.Star
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.HorizontalDivider as Divider
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.filled.Send
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import com.google.ai.client.generativeai.GenerativeModel
import com.google.ai.client.generativeai.type.generationConfig
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material.icons.outlined.StarBorder
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.zIndex
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)
setContent {
MaterialTheme(
colorScheme = darkColorScheme(
primary = Color(0xFF6366F1),
secondary = Color(0xFFA855F7),
background = Color(0xFF0F172A),
surface = Color(0xFF1E293B),
onPrimary = Color.White,
onSecondary = Color.White,
onBackground = Color(0xFFE2E8F0),
onSurface = Color(0xFFE2E8F0)
)
) {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
NotesApp()
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NotesApp() {
val context = LocalContext.current
val dataStoreManager = remember { DataStoreManager(context) }
val scope = rememberCoroutineScope()
var categories by remember { mutableStateOf(listOf<Category>()) }
var notes by remember { mutableStateOf(listOf<Note>()) }
var selectedCategory by remember { mutableStateOf<Category?>(null) }
var currentScreen by remember { mutableStateOf("main") }
var drawerState by remember { mutableStateOf(false) }
var showCategoryDialog by remember { mutableStateOf(false) }
var showNoteDialog by remember { mutableStateOf(false) }
var editingNote by remember { mutableStateOf<Note?>(null) }
var searchQuery by remember { mutableStateOf("") }
var showSearch by remember { mutableStateOf(false) }
var showFullScreenNote by remember { mutableStateOf(false) }
var fullScreenNote by remember { mutableStateOf<Note?>(null) }
// Load data dari DataStore
LaunchedEffect(Unit) {
try {
dataStoreManager.categoriesFlow.collect { loadedCategories ->
categories = loadedCategories
}
} catch (e: Exception) {
e.printStackTrace()
}
}
LaunchedEffect(Unit) {
try {
dataStoreManager.notesFlow.collect { loadedNotes ->
notes = loadedNotes
}
} catch (e: Exception) {
e.printStackTrace()
}
}
// Simpan categories dengan debounce
LaunchedEffect(categories.size) {
if (categories.isNotEmpty()) {
delay(500)
try {
dataStoreManager.saveCategories(categories)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
// Simpan notes dengan debounce
LaunchedEffect(notes.size) {
if (notes.isNotEmpty()) {
delay(500)
try {
dataStoreManager.saveNotes(notes)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
Box(modifier = Modifier.fillMaxSize()) {
Scaffold(
topBar = {
if (!showFullScreenNote) {
ModernTopBar(
title = when(currentScreen) {
"main" -> if (selectedCategory != null) selectedCategory!!.name else "AI Notes"
"ai" -> "AI Helper"
"starred" -> "Berbintang"
"archive" -> "Arsip"
"trash" -> "Sampah"
else -> "AI Notes"
},
showBackButton = (selectedCategory != null && currentScreen == "main") || currentScreen == "ai" || currentScreen == "starred",
onBackClick = {
if (currentScreen == "ai" || currentScreen == "starred") {
currentScreen = "main"
} else {
selectedCategory = null
}
},
onMenuClick = { drawerState = !drawerState },
onSearchClick = { showSearch = !showSearch },
searchQuery = searchQuery,
onSearchQueryChange = { searchQuery = it },
showSearch = showSearch && currentScreen == "main"
)
}
},
floatingActionButton = {
AnimatedVisibility(
visible = currentScreen == "main" && !showFullScreenNote,
enter = scaleIn() + fadeIn(),
exit = scaleOut() + fadeOut()
) {
FloatingActionButton(
onClick = {
if (selectedCategory != null) {
editingNote = null
showNoteDialog = true
} else {
showCategoryDialog = true
}
},
containerColor = Color.Transparent,
modifier = Modifier
.shadow(8.dp, CircleShape)
.background(
brush = Brush.linearGradient(
colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
),
shape = CircleShape
)
) {
Icon(
Icons.Default.Add,
contentDescription = if (selectedCategory != null) "Tambah Catatan" else "Tambah Kategori",
tint = Color.White
)
}
}
},
bottomBar = {
if (!showFullScreenNote) {
ModernBottomBar(
currentScreen = currentScreen,
onHomeClick = {
currentScreen = "main"
selectedCategory = null
},
onAIClick = { currentScreen = "ai" }
)
}
}
) { padding ->
Box(modifier = Modifier.fillMaxSize()) {
if (showFullScreenNote && fullScreenNote != null) {
EditableFullScreenNoteView(
note = fullScreenNote!!,
onBack = {
showFullScreenNote = false
fullScreenNote = null
},
onSave = { title, content ->
notes = notes.map {
if (it.id == fullScreenNote!!.id) it.copy(
title = title,
content = content,
timestamp = System.currentTimeMillis()
)
else it
}
fullScreenNote = fullScreenNote!!.copy(title = title, content = content)
},
onArchive = {
notes = notes.map {
if (it.id == fullScreenNote!!.id) it.copy(isArchived = true)
else it
}
showFullScreenNote = false
fullScreenNote = null
},
onDelete = {
notes = notes.map {
if (it.id == fullScreenNote!!.id) it.copy(isDeleted = true)
else it
}
showFullScreenNote = false
fullScreenNote = null
},
onPinToggle = {
notes = notes.map {
if (it.id == fullScreenNote!!.id) it.copy(isPinned = !it.isPinned)
else it
}
fullScreenNote = fullScreenNote!!.copy(isPinned = !fullScreenNote!!.isPinned)
}
)
} else {
Column(
modifier = Modifier
.padding(padding)
.fillMaxSize()
) {
when (currentScreen) {
"main" -> MainScreen(
categories = categories,
notes = notes,
selectedCategory = selectedCategory,
searchQuery = searchQuery,
onCategoryClick = { selectedCategory = it },
onNoteClick = { note ->
fullScreenNote = note
showFullScreenNote = true
},
onPinToggle = { note ->
notes = notes.map {
if (it.id == note.id) it.copy(isPinned = !it.isPinned)
else it
}
},
onCategoryDelete = { category ->
// Delete kategori dan semua catatan di dalamnya
categories = categories.filter { it.id != category.id }
notes = notes.filter { it.categoryId != category.id }
selectedCategory = null
}
)
"starred" -> StarredNotesScreen(
notes = notes,
categories = categories,
onNoteClick = { note ->
fullScreenNote = note
showFullScreenNote = true
},
onMenuClick = { drawerState = true },
onBack = { currentScreen = "main" },
onUnpin = { note ->
notes = notes.map {
if (it.id == note.id) it.copy(isPinned = false)
else it
}
}
)
"ai" -> AIHelperScreen(
categories = categories,
notes = notes.filter { !it.isDeleted }
)
"archive" -> ArchiveScreen(
notes = notes.filter { it.isArchived && !it.isDeleted },
categories = categories,
onRestore = { note ->
notes = notes.map {
if (it.id == note.id) it.copy(isArchived = false)
else it
}
},
onDelete = { note ->
notes = notes.map {
if (it.id == note.id) it.copy(isDeleted = true)
else it
}
}
)
"trash" -> TrashScreen(
notes = notes.filter { it.isDeleted },
categories = categories,
onRestore = { note ->
notes = notes.map {
if (it.id == note.id) it.copy(isDeleted = false, isArchived = false)
else it
}
},
onDeletePermanent = { note ->
notes = notes.filter { it.id != note.id }
}
)
}
}
}
// Dialogs
if (showCategoryDialog) {
CategoryDialog(
onDismiss = { showCategoryDialog = false },
onSave = { name, gradientStart, gradientEnd ->
categories = categories + Category(
name = name,
gradientStart = gradientStart,
gradientEnd = gradientEnd
)
showCategoryDialog = false
}
)
}
if (showNoteDialog && selectedCategory != null) {
NoteDialog(
note = editingNote,
onDismiss = {
showNoteDialog = false
editingNote = null
},
onSave = { title, content ->
if (editingNote != null) {
notes = notes.map {
if (it.id == editingNote!!.id)
it.copy(
title = title,
content = content,
timestamp = System.currentTimeMillis()
)
else it
}
} else {
notes = notes + Note(
categoryId = selectedCategory!!.id,
title = title,
content = content
)
}
showNoteDialog = false
editingNote = null
},
onDelete = if (editingNote != null) {
{
notes = notes.map {
if (it.id == editingNote!!.id) it.copy(isDeleted = true)
else it
}
showNoteDialog = false
editingNote = null
}
} else null
)
}
}
}
// Drawer with Animation - DI LUAR SCAFFOLD agar di atas semua
AnimatedVisibility(
visible = drawerState,
enter = fadeIn() + slideInHorizontally(
initialOffsetX = { -it }
),
exit = fadeOut() + slideOutHorizontally(
targetOffsetX = { -it }
),
modifier = Modifier.zIndex(100f) // Z-index tinggi
) {
DrawerMenu(
currentScreen = currentScreen,
onDismiss = { drawerState = false },
onItemClick = { screen ->
currentScreen = screen
selectedCategory = null
drawerState = false
showSearch = false
searchQuery = ""
}
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EditableFullScreenNoteView(
note: Note,
onBack: () -> Unit,
onSave: (String, String) -> Unit,
onArchive: () -> Unit,
onDelete: () -> Unit,
onPinToggle: () -> Unit
) {
var title by remember { mutableStateOf(note.title) }
var content by remember { mutableStateOf(note.content) }
var showArchiveDialog by remember { mutableStateOf(false) }
var showDeleteDialog by remember { mutableStateOf(false) }
val dateFormat = remember { SimpleDateFormat("dd MMMM yyyy, HH:mm", Locale("id", "ID")) }
// Dialog Konfirmasi Arsip
if (showArchiveDialog) {
AlertDialog(
onDismissRequest = { showArchiveDialog = false },
title = {
Text(
text = "Arsipkan Catatan?",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
},
text = {
Text(
text = "Catatan ini akan dipindahkan ke arsip. Anda masih bisa mengaksesnya nanti.",
style = MaterialTheme.typography.bodyMedium
)
},
confirmButton = {
TextButton(
onClick = {
if (title.isNotBlank()) {
onSave(title, content)
}
onArchive()
showArchiveDialog = false
}
) {
Text("Arsipkan", color = MaterialTheme.colorScheme.primary)
}
},
dismissButton = {
TextButton(onClick = { showArchiveDialog = false }) {
Text("Batal", color = MaterialTheme.colorScheme.onSurface)
}
}
)
}
// Dialog Konfirmasi Hapus
if (showDeleteDialog) {
AlertDialog(
onDismissRequest = { showDeleteDialog = false },
title = {
Text(
text = "Hapus Catatan?",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
},
text = {
Text(
text = "Catatan ini akan dihapus permanen dan tidak bisa dikembalikan.",
style = MaterialTheme.typography.bodyMedium
)
},
confirmButton = {
TextButton(
onClick = {
onDelete()
showDeleteDialog = false
}
) {
Text("Hapus", color = Color(0xFFEF4444))
}
},
dismissButton = {
TextButton(onClick = { showDeleteDialog = false }) {
Text("Batal", color = MaterialTheme.colorScheme.onSurface)
}
}
)
}
Scaffold(
containerColor = MaterialTheme.colorScheme.background,
topBar = {
TopAppBar(
title = { },
navigationIcon = {
IconButton(onClick = {
if (title.isNotBlank()) {
onSave(title, content)
}
onBack()
}) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Kembali", tint = MaterialTheme.colorScheme.onBackground)
}
},
actions = {
IconButton(onClick = {
if (title.isNotBlank()) {
onSave(title, content)
}
onPinToggle()
}) {
Icon(
if (note.isPinned) Icons.Filled.Star else Icons.Outlined.StarBorder,
contentDescription = "Pin Catatan",
tint = if (note.isPinned) Color(0xFFFBBF24) else MaterialTheme.colorScheme.onSurface
)
}
IconButton(onClick = { showArchiveDialog = true }) {
Icon(Icons.Default.Archive, contentDescription = "Arsipkan", tint = MaterialTheme.colorScheme.onSurface)
}
IconButton(onClick = { showDeleteDialog = true }) {
Icon(Icons.Default.Delete, contentDescription = "Hapus", tint = MaterialTheme.colorScheme.onSurface)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent
)
)
}
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.verticalScroll(rememberScrollState())
.padding(horizontal = 20.dp)
) {
TextField(
value = title,
onValueChange = { title = it },
textStyle = MaterialTheme.typography.headlineLarge.copy(
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onBackground
),
placeholder = {
Text(
"Judul",
style = MaterialTheme.typography.headlineLarge,
color = Color(0xFF64748B)
)
},
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
cursorColor = Color(0xFFA855F7)
),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(12.dp))
Text(
text = "Terakhir diubah: ${dateFormat.format(Date(note.timestamp))}",
style = MaterialTheme.typography.bodySmall,
color = Color(0xFF64748B)
)
Divider(
modifier = Modifier.padding(vertical = 20.dp),
color = MaterialTheme.colorScheme.surface
)
TextField(
value = content,
onValueChange = { content = it },
textStyle = MaterialTheme.typography.bodyLarge.copy(
color = MaterialTheme.colorScheme.onSurface,
lineHeight = 28.sp
),
placeholder = {
Text(
"Mulai menulis...",
style = MaterialTheme.typography.bodyLarge,
color = Color(0xFF64748B)
)
},
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
cursorColor = Color(0xFFA855F7)
),
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 400.dp)
)
Spacer(modifier = Modifier.height(100.dp))
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ModernTopBar(
title: String,
showBackButton: Boolean,
onBackClick: () -> Unit,
onMenuClick: () -> Unit,
onSearchClick: () -> Unit,
searchQuery: String,
onSearchQueryChange: (String) -> Unit,
showSearch: Boolean
) {
TopAppBar(
title = {
if (showSearch) {
TextField(
value = searchQuery,
onValueChange = onSearchQueryChange,
placeholder = { Text("Cari catatan...", color = Color.White.copy(0.6f)) },
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
focusedTextColor = Color.White,
unfocusedTextColor = Color.White,
cursorColor = Color.White,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent
),
modifier = Modifier.fillMaxWidth()
)
} else {
Text(
title,
fontWeight = FontWeight.Bold,
fontSize = 22.sp
)
}
},
navigationIcon = {
IconButton(onClick = if (showBackButton) onBackClick else onMenuClick) {
Icon(
if (showBackButton) Icons.AutoMirrored.Filled.ArrowBack else Icons.Default.Menu,
contentDescription = null,
tint = Color.White
)
}
},
actions = {
IconButton(onClick = onSearchClick) {
Icon(
if (showSearch) Icons.Default.Close else Icons.Default.Search,
contentDescription = "Search",
tint = Color.White
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent
),
modifier = Modifier
.background(
brush = Brush.horizontalGradient(
colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
)
)
)
}
@Composable
fun ModernBottomBar(
currentScreen: String,
onHomeClick: () -> Unit,
onAIClick: () -> Unit
) {
BottomAppBar(
containerColor = Color.Transparent,
modifier = Modifier
.shadow(8.dp, RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp))
.background(
brush = Brush.verticalGradient(
colors = listOf(
Color(0xFF1E293B).copy(0.95f),
Color(0xFF334155).copy(0.95f)
)
),
shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp)
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.weight(1f)
.clickable(onClick = onHomeClick)
.padding(vertical = 8.dp)
) {
Icon(
Icons.Default.Home,
contentDescription = "Home",
tint = if (currentScreen == "main") Color(0xFF6366F1) else Color(0xFF94A3B8),
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.height(4.dp))
Text(
"Home",
color = if (currentScreen == "main") Color(0xFF6366F1) else Color(0xFF94A3B8),
style = MaterialTheme.typography.bodySmall,
fontWeight = if (currentScreen == "main") FontWeight.Bold else FontWeight.Normal
)
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.weight(1f)
.clickable(onClick = onAIClick)
.padding(vertical = 8.dp)
) {
Icon(
Icons.Default.Star,
contentDescription = "AI Helper",
tint = if (currentScreen == "ai") Color(0xFFFBBF24) else Color(0xFF94A3B8),
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.height(4.dp))
Text(
"AI Helper",
color = if (currentScreen == "ai") Color(0xFFFBBF24) else Color(0xFF94A3B8),
style = MaterialTheme.typography.bodySmall,
fontWeight = if (currentScreen == "ai") FontWeight.Bold else FontWeight.Normal
)
}
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun MainScreen(
categories: List<Category>,
notes: List<Note>,
selectedCategory: Category?,
searchQuery: String,
onCategoryClick: (Category) -> Unit,
onNoteClick: (Note) -> Unit,
onPinToggle: (Note) -> Unit,
onCategoryDelete: (Category) -> Unit
) {
Column(modifier = Modifier.fillMaxSize()) {
if (selectedCategory == null) {
// Beranda: Tampilkan kategori dengan search filtering
if (categories.isEmpty()) {
EmptyState(
icon = Icons.Default.Create,
message = "Buat kategori pertama Anda",
subtitle = "Tekan tombol + untuk memulai"
)
} else {
// Filter kategori berdasarkan searchQuery
val filteredCategories = if (searchQuery.isEmpty()) {
categories
} else {
categories.filter {
it.name.contains(searchQuery, ignoreCase = true)
}
}
if (filteredCategories.isEmpty()) {
EmptyState(
icon = Icons.Default.Search,
message = "Kategori tidak ditemukan",
subtitle = "Coba kata kunci lain"
)
} else {
LazyVerticalStaggeredGrid(
columns = StaggeredGridCells.Fixed(2),
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalItemSpacing = 12.dp,
modifier = Modifier.fillMaxSize()
) {
items(filteredCategories) { category ->
CategoryCard(
category = category,
noteCount = notes.count { it.categoryId == category.id && !it.isDeleted && !it.isArchived },
onClick = { onCategoryClick(category) },
onDelete = {
onCategoryDelete(category)
}
)
}
}
}
}
} else {
val categoryNotes = notes
.filter {
it.categoryId == selectedCategory.id &&
!it.isDeleted &&
!it.isArchived &&
(searchQuery.isEmpty() ||
it.title.contains(searchQuery, ignoreCase = true) ||
it.content.contains(searchQuery, ignoreCase = true))
}
.sortedWith(compareByDescending<Note> { it.isPinned }.thenByDescending { it.timestamp })
if (categoryNotes.isEmpty()) {
EmptyState(
icon = Icons.Default.Add,
message = if (searchQuery.isEmpty()) "Belum ada catatan" else "Tidak ada hasil",
subtitle = if (searchQuery.isEmpty()) "Tekan tombol + untuk membuat catatan" else "Coba kata kunci lain"
)
} else {
LazyVerticalStaggeredGrid(
columns = StaggeredGridCells.Fixed(2),
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalItemSpacing = 12.dp,
modifier = Modifier.fillMaxSize()
) {
items(categoryNotes) { note ->
NoteCard(
note = note,
onClick = { onNoteClick(note) },
onPinClick = { onPinToggle(note) },
)
}
}
}
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun CategoryCard(
category: Category,
noteCount: Int,
onClick: () -> Unit,
onDelete: () -> Unit = {}
) {
var showDeleteConfirm by remember { mutableStateOf(false) }
// Delete confirmation dialog
if (showDeleteConfirm) {
AlertDialog(
onDismissRequest = { showDeleteConfirm = false },
title = { Text("Hapus Kategori?", color = Color.White) },
text = {
Text("Kategori '${category.name}' dan semua catatan di dalamnya akan dihapus. Tindakan ini tidak dapat dibatalkan.", color = Color.White)
},
confirmButton = {
Button(
onClick = {
onDelete()
showDeleteConfirm = false
},
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFEF4444)
)
) {
Text("Hapus", color = Color.White)
}
},
dismissButton = {
Button(
onClick = { showDeleteConfirm = false },
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF64748B)
)
) {
Text("Batal", color = Color.White)
}
},
containerColor = Color(0xFF1E293B)
)
}
Card(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = Color.Transparent),
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(category.gradientStart),
Color(category.gradientEnd)
)
)
)
.padding(20.dp)
) {
Column {
Icon(
Icons.Default.Folder,
contentDescription = null,
tint = Color.White.copy(0.9f),
modifier = Modifier.size(32.dp)
)
Spacer(modifier = Modifier.height(12.dp))
Text(
category.name,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = Color.White
)
Spacer(modifier = Modifier.height(4.dp))
Text(
"$noteCount catatan",
style = MaterialTheme.typography.bodySmall,
color = Color.White.copy(0.8f)
)
}
// Delete button di top-right corner
IconButton(
onClick = {
showDeleteConfirm = true
},
modifier = Modifier
.align(Alignment.TopEnd)
.size(40.dp)
) {
Icon(
Icons.Default.Close,
contentDescription = "Hapus kategori",
tint = Color.White.copy(0.7f),
modifier = Modifier.size(20.dp)
)
}
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun NoteCard(
note: Note,
onClick: () -> Unit,
onPinClick: () -> Unit
) {
val dateFormat = SimpleDateFormat("dd MMM, HH:mm", Locale("id", "ID"))
Card(
modifier = Modifier
.fillMaxWidth()
.combinedClickable(
onClick = onClick,
),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF1E293B)
),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Top
) {
// Judul
Text(
note.title,
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = Color.White,
modifier = Modifier.weight(1f),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
IconButton(
onClick = onPinClick,
modifier = Modifier.size(24.dp)
) {
Icon(
if (note.isPinned) Icons.Filled.Star else Icons.Outlined.StarBorder,
contentDescription = "Pin",
tint = if (note.isPinned) Color(0xFFFBBF24) else Color.Gray,
modifier = Modifier.size(18.dp)
)
}
}
// Deskripsi
if (note.content.isNotEmpty()) {
Spacer(modifier = Modifier.height(12.dp))
Text(
text = "Deskripsi",
style = MaterialTheme.typography.labelSmall,
color = Color(0xFF94A3B8),
fontWeight = FontWeight.SemiBold
)
Spacer(modifier = Modifier.height(4.dp))
Text(
note.content,
style = MaterialTheme.typography.bodyMedium,
maxLines = 4,
overflow = TextOverflow.Ellipsis,
color = Color(0xFFCBD5E1),
lineHeight = 20.sp
)
}
Spacer(modifier = Modifier.height(12.dp))
Divider(
color = Color(0xFF334155),
thickness = 1.dp
)
Spacer(modifier = Modifier.height(8.dp))
// Timestamp
Text(
dateFormat.format(Date(note.timestamp)),
style = MaterialTheme.typography.bodySmall,
color = Color(0xFF64748B)
)
}
}
}
@Composable
fun DrawerMenu(
currentScreen: String,
onDismiss: () -> Unit,
onItemClick: (String) -> Unit
) {
Box(
modifier = Modifier
.fillMaxSize()
.statusBarsPadding() // Padding untuk status bar
.background(Color.Black.copy(alpha = 0.5f))
.clickable(
onClick = onDismiss,
indication = null,
interactionSource = remember { MutableInteractionSource() }
)
) {
Card(
modifier = Modifier
.fillMaxHeight()
.width(250.dp)
.align(Alignment.CenterStart)
.clickable(
onClick = {},
indication = null,
interactionSource = remember { MutableInteractionSource() }
),
shape = RoundedCornerShape(topEnd = 0.dp, bottomEnd = 0.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF1E293B)
),
elevation = CardDefaults.cardElevation(defaultElevation = 16.dp)
) {
Column(modifier = Modifier.fillMaxSize()) {
// Header Drawer dengan tombol close
Box(
modifier = Modifier
.fillMaxWidth()
.background(
brush = Brush.verticalGradient(
colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
)
)
.padding(24.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column {
Icon(
Icons.Default.Create,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(36.dp)
)
Spacer(modifier = Modifier.height(12.dp))
Text(
"AI Notes",
style = MaterialTheme.typography.headlineMedium,
color = Color.White,
fontWeight = FontWeight.Bold
)
Text(
"Smart & Modern",
style = MaterialTheme.typography.bodyMedium,
color = Color.White.copy(0.8f)
)
}
// // Tombol Close
// IconButton(
// onClick = onDismiss,
// modifier = Modifier
// .size(40.dp)
// .background(
// Color.White.copy(alpha = 0.2f),
// shape = CircleShape
// )
// ) {
// Icon(
// Icons.Default.Close,
// contentDescription = "Tutup Menu",
// tint = Color.White,
// modifier = Modifier.size(24.dp)
// )
// }
}
}
Spacer(modifier = Modifier.height(16.dp))
// Menu Items
MenuItem(
icon = Icons.Default.Home,
text = "Beranda",
isSelected = currentScreen == "main"
) { onItemClick("main") }
MenuItem(
icon = Icons.Default.Star,
text = "Berbintang",
isSelected = currentScreen == "starred"
) { onItemClick("starred") }
MenuItem(
icon = Icons.Default.Archive,
text = "Arsip",
isSelected = currentScreen == "archive"
) { onItemClick("archive") }
MenuItem(
icon = Icons.Default.Delete,
text = "Sampah",
isSelected = currentScreen == "trash"
) { onItemClick("trash") }
Spacer(modifier = Modifier.weight(1f))
// Footer
Divider(
color = Color.White.copy(alpha = 0.1f),
modifier = Modifier.padding(horizontal = 16.dp)
)
Text(
text = "Version 1.0.0",
style = MaterialTheme.typography.bodySmall,
color = Color.White.copy(alpha = 0.5f),
modifier = Modifier.padding(16.dp)
)
}
}
}
}
@Composable
fun MenuItem(
icon: ImageVector,
text: String,
isSelected: Boolean,
onClick: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.background(
if (isSelected) Color(0xFF334155) else Color.Transparent
)
.padding(horizontal = 20.dp, vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
icon,
contentDescription = null,
tint = if (isSelected) Color(0xFFA855F7) else Color(0xFF94A3B8)
)
Spacer(modifier = Modifier.width(16.dp))
Text(
text,
style = MaterialTheme.typography.bodyLarge,
color = if (isSelected) Color.White else Color(0xFF94A3B8),
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal
)
}
}
@Composable
fun CategoryDialog(
onDismiss: () -> Unit,
onSave: (String, Long, Long) -> Unit
) {
var name by remember { mutableStateOf("") }
var selectedGradient by remember { mutableStateOf(0) }
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)
)
AlertDialog(
onDismissRequest = onDismiss,
containerColor = Color(0xFF1E293B),
title = {
Text(
"Buat Kategori Baru",
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 { index, 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))
}
}
)
}
@Composable
fun NoteDialog(
note: Note?,
onDismiss: () -> Unit,
onSave: (String, String) -> Unit,
onDelete: (() -> Unit)?
) {
var title by remember { mutableStateOf(note?.title ?: "") }
var content by remember { mutableStateOf(note?.content ?: "") }
AlertDialog(
onDismissRequest = onDismiss,
containerColor = Color(0xFF1E293B),
title = {
Text(
if (note == null) "Catatan Baru" else "Edit Catatan",
color = Color.White,
fontWeight = FontWeight.Bold
)
},
text = {
Column {
OutlinedTextField(
value = title,
onValueChange = { title = it },
label = { Text("Judul", 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(12.dp))
OutlinedTextField(
value = content,
onValueChange = { content = it },
label = { Text("Isi Catatan", color = Color(0xFF94A3B8)) },
modifier = Modifier
.fillMaxWidth()
.height(200.dp),
maxLines = 10,
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)
)
}
},
confirmButton = {
Row {
if (onDelete != null) {
TextButton(onClick = onDelete) {
Text("Hapus", color = Color(0xFFEF4444), fontWeight = FontWeight.Bold)
}
Spacer(modifier = Modifier.width(8.dp))
}
Button(
onClick = { if (title.isNotBlank()) onSave(title, content) },
enabled = title.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))
}
}
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AIHelperScreen(
categories: List<Category>,
notes: List<Note>
) {
var prompt by remember { mutableStateOf("") }
var isLoading by remember { mutableStateOf(false) }
var selectedCategory by remember { mutableStateOf<Category?>(null) }
var showCategoryDropdown by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf("") }
var chatMessages by remember { mutableStateOf(listOf<ChatMessage>()) }
var showCopiedMessage by remember { mutableStateOf(false) }
var copiedMessageId by remember { mutableStateOf("") }
val scope = rememberCoroutineScope()
val clipboardManager = LocalClipboardManager.current
val scrollState = rememberScrollState()
// Inisialisasi Gemini Model
val generativeModel = remember {
GenerativeModel(
modelName = "gemini-2.5-flash",
apiKey = APIKey.GEMINI_API_KEY,
generationConfig = generationConfig {
temperature = 0.8f
topK = 40
topP = 0.95f
maxOutputTokens = 4096
candidateCount = 1
}
)
}
// Auto scroll ke bawah saat ada pesan baru
LaunchedEffect(chatMessages.size) {
if (chatMessages.isNotEmpty()) {
delay(100)
scrollState.animateScrollTo(scrollState.maxValue)
}
}
Column(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
) {
// Header
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = Color.Transparent),
shape = RoundedCornerShape(0.dp)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(
brush = Brush.linearGradient(
colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
)
)
.padding(20.dp)
) {
Column {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
Icons.Default.Star,
contentDescription = null,
tint = Color(0xFFFBBF24),
modifier = Modifier.size(28.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Column {
Text(
"AI Helper",
style = MaterialTheme.typography.titleLarge,
color = Color.White,
fontWeight = FontWeight.Bold
)
Text(
"Powered by Gemini AI",
style = MaterialTheme.typography.bodySmall,
color = Color.White.copy(0.8f)
)
}
}
}
}
}
// Category Selector & Stats - Compact Version
Column(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.background)
.padding(16.dp)
) {
// Category Selector
Box {
Card(
modifier = Modifier
.fillMaxWidth()
.clickable { showCategoryDropdown = !showCategoryDropdown },
colors = CardDefaults.cardColors(containerColor = Color(0xFF1E293B)),
shape = RoundedCornerShape(12.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
Icons.Default.Folder,
contentDescription = null,
tint = Color(0xFF6366F1),
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
selectedCategory?.name ?: "Semua Kategori",
color = Color.White,
style = MaterialTheme.typography.bodyMedium
)
}
Icon(
Icons.Default.ArrowDropDown,
contentDescription = null,
tint = Color(0xFF94A3B8)
)
}
}
DropdownMenu(
expanded = showCategoryDropdown,
onDismissRequest = { showCategoryDropdown = false },
modifier = Modifier
.fillMaxWidth()
.background(Color(0xFF1E293B))
) {
DropdownMenuItem(
text = { Text("Semua Kategori", color = Color.White) },
onClick = {
selectedCategory = null
showCategoryDropdown = false
}
)
categories.forEach { category ->
DropdownMenuItem(
text = { Text(category.name, color = Color.White) },
onClick = {
selectedCategory = category
showCategoryDropdown = false
}
)
}
}
}
// Stats - Compact
Spacer(modifier = Modifier.height(12.dp))
val filteredNotes = if (selectedCategory != null) {
notes.filter { it.categoryId == selectedCategory!!.id && !it.isArchived }
} else {
notes.filter { !it.isArchived }
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
CompactStatItem(
label = "Total",
value = filteredNotes.size.toString(),
color = Color(0xFF6366F1)
)
CompactStatItem(
label = "Dipasang",
value = filteredNotes.count { it.isPinned }.toString(),
color = Color(0xFFFBBF24)
)
CompactStatItem(
label = "Kategori",
value = categories.size.toString(),
color = Color(0xFFA855F7)
)
}
}
Divider(color = Color(0xFF334155), thickness = 1.dp)
// Chat Area
Column(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
) {
if (chatMessages.isEmpty()) {
// Welcome State
Column(
modifier = Modifier
.fillMaxSize()
.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
Icons.Default.Star,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = Color(0xFF6366F1).copy(0.5f)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
"Mulai Percakapan",
style = MaterialTheme.typography.titleLarge,
color = Color.White,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Text(
"Tanyakan apa saja tentang catatan Anda",
style = MaterialTheme.typography.bodyMedium,
color = Color(0xFF94A3B8),
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(24.dp))
// Suggestion Chips
Column(
horizontalAlignment = Alignment.Start,
modifier = Modifier.fillMaxWidth(0.8f)
) {
Text(
"Contoh pertanyaan:",
style = MaterialTheme.typography.bodySmall,
color = Color(0xFF64748B),
modifier = Modifier.padding(bottom = 8.dp)
)
SuggestionChip("Analisis catatan saya", onSelect = { prompt = it })
SuggestionChip("Buat ringkasan", onSelect = { prompt = it })
SuggestionChip("Berikan saran organisasi", onSelect = { prompt = it })
}
}
} else {
// Chat Messages
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.padding(16.dp)
) {
chatMessages.forEach { message ->
ChatBubble(
message = message,
onCopy = {
clipboardManager.setText(AnnotatedString(message.message))
copiedMessageId = message.id
showCopiedMessage = true
scope.launch {
delay(2000)
showCopiedMessage = false
}
},
showCopied = showCopiedMessage && copiedMessageId == message.id
)
Spacer(modifier = Modifier.height(12.dp))
}
// Loading Indicator
if (isLoading) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
horizontalArrangement = Arrangement.Start
) {
Card(
colors = CardDefaults.cardColors(
containerColor = Color(0xFF1E293B)
),
shape = RoundedCornerShape(16.dp)
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
color = Color(0xFF6366F1),
strokeWidth = 2.dp
)
Spacer(modifier = Modifier.width(12.dp))
Text(
"AI sedang berpikir...",
color = Color(0xFF94A3B8),
style = MaterialTheme.typography.bodyMedium
)
}
}
}
}
// Error Message
if (errorMessage.isNotEmpty()) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFEF4444).copy(0.2f)
),
shape = RoundedCornerShape(12.dp)
) {
Row(
modifier = Modifier.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
Icons.Default.Warning,
contentDescription = null,
tint = Color(0xFFEF4444),
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
errorMessage,
color = Color(0xFFEF4444),
style = MaterialTheme.typography.bodySmall
)
}
}
}
Spacer(modifier = Modifier.height(80.dp))
}
}
}
// Input Area
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF1E293B)
),
shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.Bottom
) {
OutlinedTextField(
value = prompt,
onValueChange = { prompt = it },
placeholder = {
Text(
"Ketik pesan...",
color = Color(0xFF64748B)
)
},
modifier = Modifier
.weight(1f)
.heightIn(min = 48.dp, max = 120.dp),
colors = TextFieldDefaults.colors(
focusedTextColor = Color.White,
unfocusedTextColor = Color.White,
focusedContainerColor = Color(0xFF334155),
unfocusedContainerColor = Color(0xFF334155),
cursorColor = Color(0xFFA855F7),
focusedIndicatorColor = Color(0xFF6366F1),
unfocusedIndicatorColor = Color(0xFF475569)
),
shape = RoundedCornerShape(24.dp),
maxLines = 4
)
Spacer(modifier = Modifier.width(12.dp))
// Send Button
FloatingActionButton(
onClick = {
if (prompt.isNotBlank() && !isLoading) {
scope.launch {
// Add user message
chatMessages = chatMessages + ChatMessage(
message = prompt,
isUser = true
)
val userPrompt = prompt
prompt = ""
isLoading = true
errorMessage = ""
try {
val filteredNotes = if (selectedCategory != null) {
notes.filter { it.categoryId == selectedCategory!!.id && !it.isArchived }
} else {
notes.filter { !it.isArchived }
}
val notesContext = buildString {
appendLine("Data catatan pengguna:")
appendLine("Total catatan: ${filteredNotes.size}")
appendLine("Kategori: ${selectedCategory?.name ?: "Semua"}")
appendLine()
appendLine("Daftar catatan:")
filteredNotes.take(10).forEach { note ->
appendLine("- Judul: ${note.title}")
appendLine(" Isi: ${note.content.take(100)}")
appendLine()
}
}
val fullPrompt = "$notesContext\n\nPertanyaan: $userPrompt\n\nBerikan jawaban dalam bahasa Indonesia yang natural dan membantu."
val result = generativeModel.generateContent(fullPrompt)
val response = result.text ?: "Tidak ada respons dari AI"
// Add AI response
chatMessages = chatMessages + ChatMessage(
message = response,
isUser = false
)
} catch (e: Exception) {
errorMessage = "Error: ${e.message}"
} finally {
isLoading = false
}
}
}
},
containerColor = Color.Transparent,
modifier = Modifier
.size(48.dp)
.background(
brush = Brush.linearGradient(
colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
),
shape = CircleShape
)
) {
Icon(
Icons.Default.Send,
contentDescription = "Send",
tint = Color.White,
modifier = Modifier.size(24.dp)
)
}
}
}
}
}
@Composable
fun ChatBubble(
message: ChatMessage,
onCopy: () -> Unit,
showCopied: Boolean
) {
val dateFormat = remember { SimpleDateFormat("HH:mm", Locale("id", "ID")) }
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = if (message.isUser) Arrangement.End else Arrangement.Start
) {
if (!message.isUser) {
// Ganti ikon bintang dengan ikon robot/sparkles
Icon(
Icons.Default.AutoAwesome, // Atau bisa diganti dengan ikon lain seperti AutoAwesome
contentDescription = null,
tint = Color(0xFF6366F1), // Warna ungu/biru untuk AI
modifier = Modifier
.size(32.dp)
.padding(end = 8.dp)
)
}
Column(
modifier = Modifier.fillMaxWidth(0.85f),
horizontalAlignment = if (message.isUser) Alignment.End else Alignment.Start
) {
Card(
colors = CardDefaults.cardColors(
containerColor = if (message.isUser)
Color(0xFF6366F1)
else
Color(0xFF1E293B)
),
shape = RoundedCornerShape(
topStart = 16.dp,
topEnd = 16.dp,
bottomStart = if (message.isUser) 16.dp else 4.dp,
bottomEnd = if (message.isUser) 4.dp else 16.dp
)
) {
Column(modifier = Modifier.padding(12.dp)) {
Text(
message.message,
color = Color.White,
style = MaterialTheme.typography.bodyMedium,
lineHeight = 20.sp
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
dateFormat.format(Date(message.timestamp)),
color = Color.White.copy(0.6f),
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(top = 4.dp)
)
if (!message.isUser) {
IconButton(
onClick = onCopy,
modifier = Modifier.size(32.dp)
) {
Icon(
Icons.Default.ContentCopy,
contentDescription = "Copy",
tint = Color.White.copy(0.7f),
modifier = Modifier.size(16.dp)
)
}
}
}
}
}
if (showCopied && !message.isUser) {
Text(
"✓ Disalin",
color = Color(0xFF10B981),
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(top = 4.dp, start = 8.dp)
)
}
}
}
}
@Composable
fun CompactStatItem(label: String, value: String, color: Color) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.background(
color = Color(0xFF1E293B),
shape = RoundedCornerShape(8.dp)
)
.padding(horizontal = 12.dp, vertical = 8.dp)
) {
Text(
value,
style = MaterialTheme.typography.titleMedium,
color = color,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.width(4.dp))
Text(
label,
style = MaterialTheme.typography.bodySmall,
color = Color(0xFF94A3B8)
)
}
}
@Composable
fun SuggestionChip(text: String, onSelect: (String) -> Unit) {
Card(
modifier = Modifier
.padding(vertical = 4.dp)
.clickable { onSelect(text) },
colors = CardDefaults.cardColors(
containerColor = Color(0xFF1E293B)
),
shape = RoundedCornerShape(12.dp)
) {
Row(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
Icons.Default.Star,
contentDescription = null,
tint = Color(0xFF6366F1),
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text,
color = Color.White,
style = MaterialTheme.typography.bodyMedium
)
}
}
}
@Composable
fun StatItem(label: String, value: String, color: Color) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(8.dp)
) {
Text(
value,
style = MaterialTheme.typography.headlineMedium,
color = color,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(4.dp))
Text(
label,
style = MaterialTheme.typography.bodySmall,
color = Color(0xFF94A3B8)
)
}
}
@Composable
fun ArchiveScreen(
notes: List<Note>,
categories: List<Category>,
onRestore: (Note) -> Unit,
onDelete: (Note) -> Unit
) {
if (notes.isEmpty()) {
EmptyState(
icon = Icons.Default.Archive,
message = "Arsip kosong",
subtitle = "Catatan yang diarsipkan akan muncul di sini"
)
} else {
LazyColumn(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(notes) { note ->
val category = categories.find { it.id == note.categoryId }
ArchiveNoteCard(
note = note,
categoryName = category?.name ?: "Unknown",
onRestore = { onRestore(note) },
onDelete = { onDelete(note) }
)
}
}
}
}
@Composable
fun ArchiveNoteCard(
note: Note,
categoryName: String,
onRestore: () -> Unit,
onDelete: () -> Unit
) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF1E293B)
),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(modifier = Modifier.weight(1f)) {
Text(
note.title,
fontWeight = FontWeight.Bold,
color = Color.White,
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(4.dp))
Text(
categoryName,
color = Color(0xFF64748B),
style = MaterialTheme.typography.bodySmall
)
}
}
if (note.content.isNotEmpty()) {
Spacer(modifier = Modifier.height(8.dp))
Text(
note.content,
maxLines = 2,
color = Color(0xFF94A3B8),
style = MaterialTheme.typography.bodyMedium
)
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 12.dp),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = onRestore) {
Icon(
Icons.Default.AccountBox,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = Color(0xFF10B981)
)
Spacer(modifier = Modifier.width(4.dp))
Text("Pulihkan", color = Color(0xFF10B981), fontWeight = FontWeight.Bold)
}
Spacer(modifier = Modifier.width(8.dp))
TextButton(onClick = onDelete) {
Icon(
Icons.Default.Delete,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = Color(0xFFEF4444)
)
Spacer(modifier = Modifier.width(4.dp))
Text("Hapus", color = Color(0xFFEF4444), fontWeight = FontWeight.Bold)
}
}
}
}
}
@Composable
fun TrashScreen(
notes: List<Note>,
categories: List<Category>,
onRestore: (Note) -> Unit,
onDeletePermanent: (Note) -> Unit
) {
if (notes.isEmpty()) {
EmptyState(
icon = Icons.Default.Delete,
message = "Sampah kosong",
subtitle = "Catatan yang dihapus akan muncul di sini"
)
} else {
LazyColumn(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(notes) { note ->
val category = categories.find { it.id == note.categoryId }
TrashNoteCard(
note = note,
categoryName = category?.name ?: "Unknown",
onRestore = { onRestore(note) },
onDeletePermanent = { onDeletePermanent(note) }
)
}
}
}
}
@Composable
fun TrashNoteCard(
note: Note,
categoryName: String,
onRestore: () -> Unit,
onDeletePermanent: () -> Unit
) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF7F1D1D).copy(0.2f)
),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(modifier = Modifier.weight(1f)) {
Text(
note.title,
fontWeight = FontWeight.Bold,
color = Color.White,
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(4.dp))
Text(
categoryName,
color = Color(0xFF64748B),
style = MaterialTheme.typography.bodySmall
)
}
}
if (note.content.isNotEmpty()) {
Spacer(modifier = Modifier.height(8.dp))
Text(
note.content,
maxLines = 2,
color = Color(0xFF94A3B8),
style = MaterialTheme.typography.bodyMedium
)
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 12.dp),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = onRestore) {
Icon(
Icons.Default.AccountBox,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = Color(0xFF10B981)
)
Spacer(modifier = Modifier.width(4.dp))
Text("Pulihkan", color = Color(0xFF10B981), fontWeight = FontWeight.Bold)
}
Spacer(modifier = Modifier.width(8.dp))
TextButton(onClick = onDeletePermanent) {
Icon(
Icons.Default.Delete,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = Color(0xFFEF4444)
)
Spacer(modifier = Modifier.width(4.dp))
Text("Hapus Permanen", color = Color(0xFFEF4444), fontWeight = FontWeight.Bold)
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun StarredNotesScreen(
notes: List<Note>,
categories: List<Category>,
onNoteClick: (Note) -> Unit,
onMenuClick: () -> Unit,
onBack: () -> Unit,
onUnpin: (Note) -> Unit
) {
val starredNotes = notes.filter { it.isPinned && !it.isArchived && !it.isDeleted }
if (starredNotes.isEmpty()) {
EmptyState(
icon = Icons.Default.Star,
message = "Belum ada catatan berbintang",
subtitle = "Catatan yang ditandai berbintang akan muncul di sini"
)
} else {
LazyColumn(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(starredNotes) { note ->
val category = categories.find { it.id == note.categoryId }
StarredNoteCard(
note = note,
categoryName = category?.name ?: "Unknown",
onClick = { onNoteClick(note) },
onUnpin = { onUnpin(note) }
)
}
}
}
}
@Composable
fun StarredNoteCard(
note: Note,
categoryName: String,
onClick: () -> Unit,
onUnpin: () -> Unit
) {
Card(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF1E293B)
),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Top
) {
Column(modifier = Modifier.weight(1f)) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
Icons.Default.Star,
contentDescription = null,
tint = Color(0xFFFBBF24),
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
note.title,
fontWeight = FontWeight.Bold,
color = Color.White,
style = MaterialTheme.typography.titleMedium
)
}
Spacer(modifier = Modifier.height(4.dp))
Text(
categoryName,
color = Color(0xFF64748B),
style = MaterialTheme.typography.bodySmall
)
}
}
if (note.content.isNotEmpty()) {
Spacer(modifier = Modifier.height(8.dp))
Text(
note.content,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
color = Color(0xFF94A3B8),
style = MaterialTheme.typography.bodyMedium
)
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 12.dp),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = onClick) {
Icon(
Icons.Default.Info,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = Color(0xFF6366F1)
)
Spacer(modifier = Modifier.width(4.dp))
Text("Lihat Detail", color = Color(0xFF6366F1), fontWeight = FontWeight.Bold)
}
Spacer(modifier = Modifier.width(8.dp))
TextButton(onClick = onUnpin) {
Icon(
Icons.Outlined.StarBorder,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = Color(0xFFFBBF24)
)
Spacer(modifier = Modifier.width(4.dp))
Text("Hapus Bintang", color = Color(0xFFFBBF24), fontWeight = FontWeight.Bold)
}
}
}
}
}
@Composable
fun EmptyState(
icon: ImageVector,
message: String,
subtitle: String
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(32.dp)
) {
Icon(
icon,
contentDescription = null,
modifier = Modifier.size(80.dp),
tint = Color(0xFF475569)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
message,
style = MaterialTheme.typography.titleLarge,
color = Color(0xFF94A3B8),
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Text(
subtitle,
style = MaterialTheme.typography.bodyMedium,
color = Color(0xFF64748B)
)
}
}
}