diff --git a/app/src/main/java/com/example/notesai/MainActivity.kt b/app/src/main/java/com/example/notesai/MainActivity.kt index e3c6f96..f8ee96d 100644 --- a/app/src/main/java/com/example/notesai/MainActivity.kt +++ b/app/src/main/java/com/example/notesai/MainActivity.kt @@ -4,58 +4,35 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.animation.* -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.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 com.example.notesai.config.APIKey import com.example.notesai.data.local.DataStoreManager +import com.example.notesai.presentation.components.DrawerMenu +import com.example.notesai.presentation.components.ModernBottomBar +import com.example.notesai.presentation.components.ModernTopBar +import com.example.notesai.presentation.dialogs.CategoryDialog +import com.example.notesai.presentation.dialogs.NoteDialog +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 kotlinx.coroutines.delay // Data Classes @@ -462,1483 +439,3 @@ fun NotesApp() { } } -@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(ExperimentalFoundationApi::class) -@Composable -fun MainScreen( - categories: List, - notes: List, - 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 { 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) - ) - } - } -} - - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun AIHelperScreen( - categories: List, - notes: List -) { - var prompt by remember { mutableStateOf("") } - var isLoading by remember { mutableStateOf(false) } - var selectedCategory by remember { mutableStateOf(null) } - var showCategoryDropdown by remember { mutableStateOf(false) } - var errorMessage by remember { mutableStateOf("") } - var chatMessages by remember { mutableStateOf(listOf()) } - 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, - categories: List, - 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, - categories: List, - 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, - categories: List, - 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) - } - } - } - } -} - diff --git a/app/src/main/java/com/example/notesai/presentation/components/DrawerMenu.kt b/app/src/main/java/com/example/notesai/presentation/components/DrawerMenu.kt index 7faf725..533d620 100644 --- a/app/src/main/java/com/example/notesai/presentation/components/DrawerMenu.kt +++ b/app/src/main/java/com/example/notesai/presentation/components/DrawerMenu.kt @@ -38,8 +38,6 @@ 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.MenuItem -import com.example.notesai.util.Constants.AppColors.Divider @Composable fun DrawerMenu( diff --git a/app/src/main/java/com/example/notesai/presentation/screens/ai/AIHelperScreen.kt b/app/src/main/java/com/example/notesai/presentation/screens/ai/AIHelperScreen.kt new file mode 100644 index 0000000..6e00ef6 --- /dev/null +++ b/app/src/main/java/com/example/notesai/presentation/screens/ai/AIHelperScreen.kt @@ -0,0 +1,528 @@ +package com.example.notesai.presentation.screens.ai + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.Folder +import androidx.compose.material.icons.filled.Send +import androidx.compose.material.icons.filled.Star +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Divider +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalClipboardManager +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.config.APIKey +import com.example.notesai.presentation.screens.ai.components.ChatBubble +import com.example.notesai.presentation.screens.ai.components.CompactStatItem +import com.example.notesai.presentation.screens.ai.components.SuggestionChip +import com.example.notesai.util.Constants.AppColors.Divider +import com.google.ai.client.generativeai.GenerativeModel +import com.google.ai.client.generativeai.type.generationConfig +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlin.collections.plus + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AIHelperScreen( + categories: List, + notes: List +) { + var prompt by remember { mutableStateOf("") } + var isLoading by remember { mutableStateOf(false) } + var selectedCategory by remember { mutableStateOf(null) } + var showCategoryDropdown by remember { mutableStateOf(false) } + var errorMessage by remember { mutableStateOf("") } + var chatMessages by remember { mutableStateOf(listOf()) } + 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.Companion + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + ) { + // Header + Card( + modifier = Modifier.Companion.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = Color.Companion.Transparent), + shape = RoundedCornerShape(0.dp) + ) { + Box( + modifier = Modifier.Companion + .fillMaxWidth() + .background( + brush = Brush.Companion.linearGradient( + colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7)) + ) + ) + .padding(20.dp) + ) { + Column { + Row(verticalAlignment = Alignment.Companion.CenterVertically) { + Icon( + Icons.Default.Star, + contentDescription = null, + tint = Color(0xFFFBBF24), + modifier = Modifier.Companion.size(28.dp) + ) + Spacer(modifier = Modifier.Companion.width(12.dp)) + Column { + Text( + "AI Helper", + style = MaterialTheme.typography.titleLarge, + color = Color.Companion.White, + fontWeight = FontWeight.Companion.Bold + ) + Text( + "Powered by Gemini AI", + style = MaterialTheme.typography.bodySmall, + color = Color.Companion.White.copy(0.8f) + ) + } + } + } + } + } + + // Category Selector & Stats - Compact Version + Column( + modifier = Modifier.Companion + .fillMaxWidth() + .background(MaterialTheme.colorScheme.background) + .padding(16.dp) + ) { + // Category Selector + Box { + Card( + modifier = Modifier.Companion + .fillMaxWidth() + .clickable { showCategoryDropdown = !showCategoryDropdown }, + colors = CardDefaults.cardColors(containerColor = Color(0xFF1E293B)), + shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp) + ) { + Row( + modifier = Modifier.Companion + .fillMaxWidth() + .padding(12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Companion.CenterVertically + ) { + Row(verticalAlignment = Alignment.Companion.CenterVertically) { + Icon( + Icons.Default.Folder, + contentDescription = null, + tint = Color(0xFF6366F1), + modifier = Modifier.Companion.size(20.dp) + ) + Spacer(modifier = Modifier.Companion.width(8.dp)) + Text( + selectedCategory?.name ?: "Semua Kategori", + color = Color.Companion.White, + style = MaterialTheme.typography.bodyMedium + ) + } + Icon( + Icons.Default.ArrowDropDown, + contentDescription = null, + tint = Color(0xFF94A3B8) + ) + } + } + + DropdownMenu( + expanded = showCategoryDropdown, + onDismissRequest = { showCategoryDropdown = false }, + modifier = Modifier.Companion + .fillMaxWidth() + .background(Color(0xFF1E293B)) + ) { + DropdownMenuItem( + text = { Text("Semua Kategori", color = Color.Companion.White) }, + onClick = { + selectedCategory = null + showCategoryDropdown = false + } + ) + categories.forEach { category -> + DropdownMenuItem( + text = { Text(category.name, color = Color.Companion.White) }, + onClick = { + selectedCategory = category + showCategoryDropdown = false + } + ) + } + } + } + + // Stats - Compact + Spacer(modifier = Modifier.Companion.height(12.dp)) + val filteredNotes = if (selectedCategory != null) { + notes.filter { it.categoryId == selectedCategory!!.id && !it.isArchived } + } else { + notes.filter { !it.isArchived } + } + + Row( + modifier = Modifier.Companion.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.Companion + .weight(1f) + .fillMaxWidth() + ) { + if (chatMessages.isEmpty()) { + // Welcome State + Column( + modifier = Modifier.Companion + .fillMaxSize() + .padding(32.dp), + horizontalAlignment = Alignment.Companion.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Icon( + Icons.Default.Star, + contentDescription = null, + modifier = Modifier.Companion.size(64.dp), + tint = Color(0xFF6366F1).copy(0.5f) + ) + Spacer(modifier = Modifier.Companion.height(16.dp)) + Text( + "Mulai Percakapan", + style = MaterialTheme.typography.titleLarge, + color = Color.Companion.White, + fontWeight = FontWeight.Companion.Bold + ) + Spacer(modifier = Modifier.Companion.height(8.dp)) + Text( + "Tanyakan apa saja tentang catatan Anda", + style = MaterialTheme.typography.bodyMedium, + color = Color(0xFF94A3B8), + textAlign = TextAlign.Companion.Center + ) + + Spacer(modifier = Modifier.Companion.height(24.dp)) + + // Suggestion Chips + Column( + horizontalAlignment = Alignment.Companion.Start, + modifier = Modifier.Companion.fillMaxWidth(0.8f) + ) { + Text( + "Contoh pertanyaan:", + style = MaterialTheme.typography.bodySmall, + color = Color(0xFF64748B), + modifier = Modifier.Companion.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.Companion + .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.Companion.height(12.dp)) + } + + // Loading Indicator + if (isLoading) { + Row( + modifier = Modifier.Companion + .fillMaxWidth() + .padding(vertical = 8.dp), + horizontalArrangement = Arrangement.Start + ) { + Card( + colors = CardDefaults.cardColors( + containerColor = Color(0xFF1E293B) + ), + shape = androidx.compose.foundation.shape.RoundedCornerShape(16.dp) + ) { + Row( + modifier = Modifier.Companion.padding(16.dp), + verticalAlignment = Alignment.Companion.CenterVertically + ) { + CircularProgressIndicator( + modifier = Modifier.Companion.size(20.dp), + color = Color(0xFF6366F1), + strokeWidth = 2.dp + ) + Spacer(modifier = Modifier.Companion.width(12.dp)) + Text( + "AI sedang berpikir...", + color = Color(0xFF94A3B8), + style = MaterialTheme.typography.bodyMedium + ) + } + } + } + } + + // Error Message + if (errorMessage.isNotEmpty()) { + Card( + modifier = Modifier.Companion.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFEF4444).copy(0.2f) + ), + shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp) + ) { + Row( + modifier = Modifier.Companion.padding(12.dp), + verticalAlignment = Alignment.Companion.CenterVertically + ) { + Icon( + Icons.Default.Warning, + contentDescription = null, + tint = Color(0xFFEF4444), + modifier = Modifier.Companion.size(20.dp) + ) + Spacer(modifier = Modifier.Companion.width(8.dp)) + Text( + errorMessage, + color = Color(0xFFEF4444), + style = MaterialTheme.typography.bodySmall + ) + } + } + } + + Spacer(modifier = Modifier.Companion.height(80.dp)) + } + } + } + + // Input Area + Card( + modifier = Modifier.Companion.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = Color(0xFF1E293B) + ), + shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 8.dp) + ) { + Row( + modifier = Modifier.Companion + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.Companion.Bottom + ) { + OutlinedTextField( + value = prompt, + onValueChange = { prompt = it }, + placeholder = { + Text( + "Ketik pesan...", + color = Color(0xFF64748B) + ) + }, + modifier = Modifier.Companion + .weight(1f) + .heightIn(min = 48.dp, max = 120.dp), + colors = TextFieldDefaults.colors( + focusedTextColor = Color.Companion.White, + unfocusedTextColor = Color.Companion.White, + focusedContainerColor = Color(0xFF334155), + unfocusedContainerColor = Color(0xFF334155), + cursorColor = Color(0xFFA855F7), + focusedIndicatorColor = Color(0xFF6366F1), + unfocusedIndicatorColor = Color(0xFF475569) + ), + shape = androidx.compose.foundation.shape.RoundedCornerShape(24.dp), + maxLines = 4 + ) + + Spacer(modifier = Modifier.Companion.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.Companion.Transparent, + modifier = Modifier.Companion + .size(48.dp) + .background( + brush = Brush.Companion.linearGradient( + colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7)) + ), + shape = CircleShape + ) + ) { + Icon( + Icons.Default.Send, + contentDescription = "Send", + tint = Color.Companion.White, + modifier = Modifier.Companion.size(24.dp) + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/notesai/presentation/screens/ai/components/ChatBubble.kt b/app/src/main/java/com/example/notesai/presentation/screens/ai/components/ChatBubble.kt new file mode 100644 index 0000000..52abc26 --- /dev/null +++ b/app/src/main/java/com/example/notesai/presentation/screens/ai/components/ChatBubble.kt @@ -0,0 +1,120 @@ +package com.example.notesai.presentation.screens.ai.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AutoAwesome +import androidx.compose.material.icons.filled.ContentCopy +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.remember +import androidx.compose.ui.Alignment +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 java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +@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) + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/notesai/presentation/screens/ai/components/CompactStatItem.kt b/app/src/main/java/com/example/notesai/presentation/screens/ai/components/CompactStatItem.kt new file mode 100644 index 0000000..9a89728 --- /dev/null +++ b/app/src/main/java/com/example/notesai/presentation/screens/ai/components/CompactStatItem.kt @@ -0,0 +1,42 @@ +package com.example.notesai.presentation.screens.ai.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.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) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/notesai/presentation/screens/ai/components/StatItem.kt b/app/src/main/java/com/example/notesai/presentation/screens/ai/components/StatItem.kt new file mode 100644 index 0000000..9966133 --- /dev/null +++ b/app/src/main/java/com/example/notesai/presentation/screens/ai/components/StatItem.kt @@ -0,0 +1,35 @@ +package com.example.notesai.presentation.screens.ai.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp + +@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) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/notesai/presentation/screens/ai/components/SuggestionChip.kt b/app/src/main/java/com/example/notesai/presentation/screens/ai/components/SuggestionChip.kt new file mode 100644 index 0000000..63dd02e --- /dev/null +++ b/app/src/main/java/com/example/notesai/presentation/screens/ai/components/SuggestionChip.kt @@ -0,0 +1,52 @@ +package com.example.notesai.presentation.screens.ai.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Star +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +@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 + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/notesai/presentation/screens/archive/ArchiveScreen.kt b/app/src/main/java/com/example/notesai/presentation/screens/archive/ArchiveScreen.kt new file mode 100644 index 0000000..45d0781 --- /dev/null +++ b/app/src/main/java/com/example/notesai/presentation/screens/archive/ArchiveScreen.kt @@ -0,0 +1,45 @@ +package com.example.notesai.presentation.screens.archive + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +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.presentation.components.EmptyState +import com.example.notesai.presentation.screens.archive.components.ArchiveNoteCard + +@Composable +fun ArchiveScreen( + notes: List, + categories: List, + 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) } + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/notesai/presentation/screens/archive/components/ArchiveNoteCard.kt b/app/src/main/java/com/example/notesai/presentation/screens/archive/components/ArchiveNoteCard.kt new file mode 100644 index 0000000..ba40f49 --- /dev/null +++ b/app/src/main/java/com/example/notesai/presentation/screens/archive/components/ArchiveNoteCard.kt @@ -0,0 +1,105 @@ +package com.example.notesai.presentation.screens.archive.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountBox +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +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 + +@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) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/notesai/presentation/screens/main/MainScreen.kt b/app/src/main/java/com/example/notesai/presentation/screens/main/MainScreen.kt new file mode 100644 index 0000000..898289d --- /dev/null +++ b/app/src/main/java/com/example/notesai/presentation/screens/main/MainScreen.kt @@ -0,0 +1,123 @@ +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.lazy.staggeredgrid.LazyVerticalStaggeredGrid +import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells +import androidx.compose.foundation.lazy.staggeredgrid.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Create +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.presentation.components.EmptyState +import com.example.notesai.presentation.screens.main.components.CategoryCard + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun MainScreen( + categories: List, + notes: List, + 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 { 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) }, + ) + } + } + } + } + } +} + +@Composable +fun NoteCard(note: Note, onClick: () -> Unit, onPinClick: () -> Unit) { + TODO("Not yet implemented") +} \ No newline at end of file diff --git a/app/src/main/java/com/example/notesai/presentation/screens/main/components/CategoryCard.kt b/app/src/main/java/com/example/notesai/presentation/screens/main/components/CategoryCard.kt new file mode 100644 index 0000000..96e6f9b --- /dev/null +++ b/app/src/main/java/com/example/notesai/presentation/screens/main/components/CategoryCard.kt @@ -0,0 +1,145 @@ +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.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.ui.Alignment +import androidx.compose.ui.Modifier +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 + +@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) + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/notesai/presentation/screens/main/components/NoteCard.kt b/app/src/main/java/com/example/notesai/presentation/screens/main/components/NoteCard.kt new file mode 100644 index 0000000..5cc1c82 --- /dev/null +++ b/app/src/main/java/com/example/notesai/presentation/screens/main/components/NoteCard.kt @@ -0,0 +1,130 @@ +package com.example.notesai.presentation.screens.main.components + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +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 +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +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 androidx.compose.ui.unit.sp +import com.example.notesai.Note +import com.example.notesai.util.Constants.AppColors.Divider +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +@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) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/notesai/presentation/screens/note/EditableFullScreenNoteView.kt b/app/src/main/java/com/example/notesai/presentation/screens/note/EditableFullScreenNoteView.kt new file mode 100644 index 0000000..05987a4 --- /dev/null +++ b/app/src/main/java/com/example/notesai/presentation/screens/note/EditableFullScreenNoteView.kt @@ -0,0 +1,251 @@ +package com.example.notesai.presentation.screens.note + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Archive +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Star +import androidx.compose.material.icons.outlined.StarBorder +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Divider +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +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.ui.Modifier +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 java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +@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)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/notesai/presentation/screens/starred/StarredNoteCard.kt b/app/src/main/java/com/example/notesai/presentation/screens/starred/StarredNoteCard.kt new file mode 100644 index 0000000..6b44854 --- /dev/null +++ b/app/src/main/java/com/example/notesai/presentation/screens/starred/StarredNoteCard.kt @@ -0,0 +1,132 @@ +package com.example.notesai.presentation.screens.starred + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Info +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.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +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 + +@Composable +fun StarredNoteCard( + note: Note, + categoryName: String, + onClick: () -> Unit, + onUnpin: () -> Unit +) { + Card( + modifier = Modifier.Companion + .fillMaxWidth() + .clickable(onClick = onClick), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = Color(0xFF1E293B) + ), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column(modifier = Modifier.Companion.padding(16.dp)) { + Row( + modifier = Modifier.Companion.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Companion.Top + ) { + Column(modifier = Modifier.Companion.weight(1f)) { + Row( + verticalAlignment = Alignment.Companion.CenterVertically + ) { + Icon( + Icons.Default.Star, + contentDescription = null, + tint = Color(0xFFFBBF24), + modifier = Modifier.Companion.size(16.dp) + ) + Spacer(modifier = Modifier.Companion.width(8.dp)) + Text( + note.title, + fontWeight = FontWeight.Companion.Bold, + color = Color.Companion.White, + style = MaterialTheme.typography.titleMedium + ) + } + Spacer(modifier = Modifier.Companion.height(4.dp)) + Text( + categoryName, + color = Color(0xFF64748B), + style = MaterialTheme.typography.bodySmall + ) + } + } + + if (note.content.isNotEmpty()) { + Spacer(modifier = Modifier.Companion.height(8.dp)) + Text( + note.content, + maxLines = 2, + overflow = TextOverflow.Companion.Ellipsis, + color = Color(0xFF94A3B8), + style = MaterialTheme.typography.bodyMedium + ) + } + + Row( + modifier = Modifier.Companion + .fillMaxWidth() + .padding(top = 12.dp), + horizontalArrangement = Arrangement.End + ) { + TextButton(onClick = onClick) { + Icon( + Icons.Default.Info, + contentDescription = null, + modifier = Modifier.Companion.size(18.dp), + tint = Color(0xFF6366F1) + ) + Spacer(modifier = Modifier.Companion.width(4.dp)) + Text( + "Lihat Detail", + color = Color(0xFF6366F1), + fontWeight = FontWeight.Companion.Bold + ) + } + Spacer(modifier = Modifier.Companion.width(8.dp)) + TextButton(onClick = onUnpin) { + Icon( + Icons.Outlined.StarBorder, + contentDescription = null, + modifier = Modifier.Companion.size(18.dp), + tint = Color(0xFFFBBF24) + ) + Spacer(modifier = Modifier.Companion.width(4.dp)) + Text( + "Hapus Bintang", + color = Color(0xFFFBBF24), + fontWeight = FontWeight.Companion.Bold + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/notesai/presentation/screens/starred/components/StarredNotesScreen.kt b/app/src/main/java/com/example/notesai/presentation/screens/starred/components/StarredNotesScreen.kt new file mode 100644 index 0000000..7075b59 --- /dev/null +++ b/app/src/main/java/com/example/notesai/presentation/screens/starred/components/StarredNotesScreen.kt @@ -0,0 +1,51 @@ +package com.example.notesai.presentation.screens.starred.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +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 + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun StarredNotesScreen( + notes: List, + categories: List, + 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) } + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/notesai/presentation/screens/trash/components/TrashNoteCard.kt b/app/src/main/java/com/example/notesai/presentation/screens/trash/components/TrashNoteCard.kt new file mode 100644 index 0000000..b578fc9 --- /dev/null +++ b/app/src/main/java/com/example/notesai/presentation/screens/trash/components/TrashNoteCard.kt @@ -0,0 +1,105 @@ +package com.example.notesai.presentation.screens.trash.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountBox +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +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 + +@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) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/notesai/presentation/screens/trash/components/TrashScreen.kt b/app/src/main/java/com/example/notesai/presentation/screens/trash/components/TrashScreen.kt new file mode 100644 index 0000000..d92bc52 --- /dev/null +++ b/app/src/main/java/com/example/notesai/presentation/screens/trash/components/TrashScreen.kt @@ -0,0 +1,44 @@ +package com.example.notesai.presentation.screens.trash.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +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 + +@Composable +fun TrashScreen( + notes: List, + categories: List, + 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) } + ) + } + } + } +} \ No newline at end of file