Migrasi Components & Screen

This commit is contained in:
202310715297 RAIHAN ARIQ MUZAKKI 2025-12-13 14:40:11 +07:00
parent 0d22d94905
commit 63b10a3e1c
17 changed files with 1919 additions and 1516 deletions

File diff suppressed because it is too large Load Diff

View File

@ -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(

View File

@ -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<Category>,
notes: List<Note>
) {
var prompt by remember { mutableStateOf("") }
var isLoading by remember { mutableStateOf(false) }
var selectedCategory by remember { mutableStateOf<Category?>(null) }
var showCategoryDropdown by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf("") }
var chatMessages by remember { mutableStateOf(listOf<ChatMessage>()) }
var showCopiedMessage by remember { mutableStateOf(false) }
var copiedMessageId by remember { mutableStateOf("") }
val scope = rememberCoroutineScope()
val clipboardManager = LocalClipboardManager.current
val scrollState = rememberScrollState()
// Inisialisasi Gemini Model
val generativeModel = remember {
GenerativeModel(
modelName = "gemini-2.5-flash",
apiKey = APIKey.GEMINI_API_KEY,
generationConfig = generationConfig {
temperature = 0.8f
topK = 40
topP = 0.95f
maxOutputTokens = 4096
candidateCount = 1
}
)
}
// Auto scroll ke bawah saat ada pesan baru
LaunchedEffect(chatMessages.size) {
if (chatMessages.isNotEmpty()) {
delay(100)
scrollState.animateScrollTo(scrollState.maxValue)
}
}
Column(
modifier = Modifier.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)
)
}
}
}
}
}

View File

@ -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)
)
}
}
}
}

View File

@ -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)
)
}
}

View File

@ -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)
)
}
}

View File

@ -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
)
}
}
}

View File

@ -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<Note>,
categories: List<Category>,
onRestore: (Note) -> Unit,
onDelete: (Note) -> Unit
) {
if (notes.isEmpty()) {
EmptyState(
icon = Icons.Default.Archive,
message = "Arsip kosong",
subtitle = "Catatan yang diarsipkan akan muncul di sini"
)
} else {
LazyColumn(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(notes) { note ->
val category = categories.find { it.id == note.categoryId }
ArchiveNoteCard(
note = note,
categoryName = category?.name ?: "Unknown",
onRestore = { onRestore(note) },
onDelete = { onDelete(note) }
)
}
}
}
}

View File

@ -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)
}
}
}
}
}

View File

@ -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<Category>,
notes: List<Note>,
selectedCategory: Category?,
searchQuery: String,
onCategoryClick: (Category) -> Unit,
onNoteClick: (Note) -> Unit,
onPinToggle: (Note) -> Unit,
onCategoryDelete: (Category) -> Unit
) {
Column(modifier = Modifier.fillMaxSize()) {
if (selectedCategory == null) {
// Beranda: Tampilkan kategori dengan search filtering
if (categories.isEmpty()) {
EmptyState(
icon = Icons.Default.Create,
message = "Buat kategori pertama Anda",
subtitle = "Tekan tombol + untuk memulai"
)
} else {
// Filter kategori berdasarkan searchQuery
val filteredCategories = if (searchQuery.isEmpty()) {
categories
} else {
categories.filter {
it.name.contains(searchQuery, ignoreCase = true)
}
}
if (filteredCategories.isEmpty()) {
EmptyState(
icon = Icons.Default.Search,
message = "Kategori tidak ditemukan",
subtitle = "Coba kata kunci lain"
)
} else {
LazyVerticalStaggeredGrid(
columns = StaggeredGridCells.Fixed(2),
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalItemSpacing = 12.dp,
modifier = Modifier.fillMaxSize()
) {
items(filteredCategories) { category ->
CategoryCard(
category = category,
noteCount = notes.count { it.categoryId == category.id && !it.isDeleted && !it.isArchived },
onClick = { onCategoryClick(category) },
onDelete = {
onCategoryDelete(category)
}
)
}
}
}
}
} else {
val categoryNotes = notes
.filter {
it.categoryId == selectedCategory.id &&
!it.isDeleted &&
!it.isArchived &&
(searchQuery.isEmpty() ||
it.title.contains(searchQuery, ignoreCase = true) ||
it.content.contains(searchQuery, ignoreCase = true))
}
.sortedWith(compareByDescending<Note> { it.isPinned }.thenByDescending { it.timestamp })
if (categoryNotes.isEmpty()) {
EmptyState(
icon = Icons.Default.Add,
message = if (searchQuery.isEmpty()) "Belum ada catatan" else "Tidak ada hasil",
subtitle = if (searchQuery.isEmpty()) "Tekan tombol + untuk membuat catatan" else "Coba kata kunci lain"
)
} else {
LazyVerticalStaggeredGrid(
columns = StaggeredGridCells.Fixed(2),
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalItemSpacing = 12.dp,
modifier = Modifier.fillMaxSize()
) {
items(categoryNotes) { note ->
NoteCard(
note = note,
onClick = { onNoteClick(note) },
onPinClick = { onPinToggle(note) },
)
}
}
}
}
}
}
@Composable
fun NoteCard(note: Note, onClick: () -> Unit, onPinClick: () -> Unit) {
TODO("Not yet implemented")
}

View File

@ -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)
)
}
}
}
}

View File

@ -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)
)
}
}
}

View File

@ -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))
}
}
}

View File

@ -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
)
}
}
}
}
}

View File

@ -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<Note>,
categories: List<Category>,
onNoteClick: (Note) -> Unit,
onMenuClick: () -> Unit,
onBack: () -> Unit,
onUnpin: (Note) -> Unit
) {
val starredNotes = notes.filter { it.isPinned && !it.isArchived && !it.isDeleted }
if (starredNotes.isEmpty()) {
EmptyState(
icon = Icons.Default.Star,
message = "Belum ada catatan berbintang",
subtitle = "Catatan yang ditandai berbintang akan muncul di sini"
)
} else {
LazyColumn(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(starredNotes) { note ->
val category = categories.find { it.id == note.categoryId }
StarredNoteCard(
note = note,
categoryName = category?.name ?: "Unknown",
onClick = { onNoteClick(note) },
onUnpin = { onUnpin(note) }
)
}
}
}
}

View File

@ -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)
}
}
}
}
}

View File

@ -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<Note>,
categories: List<Category>,
onRestore: (Note) -> Unit,
onDeletePermanent: (Note) -> Unit
) {
if (notes.isEmpty()) {
EmptyState(
icon = Icons.Default.Delete,
message = "Sampah kosong",
subtitle = "Catatan yang dihapus akan muncul di sini"
)
} else {
LazyColumn(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(notes) { note ->
val category = categories.find { it.id == note.categoryId }
TrashNoteCard(
note = note,
categoryName = category?.name ?: "Unknown",
onRestore = { onRestore(note) },
onDeletePermanent = { onDeletePermanent(note) }
)
}
}
}
}