Migrasi Components & Screen
This commit is contained in:
parent
0d22d94905
commit
63b10a3e1c
File diff suppressed because it is too large
Load Diff
@ -38,8 +38,6 @@ import androidx.compose.ui.graphics.Brush
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.example.notesai.MenuItem
|
|
||||||
import com.example.notesai.util.Constants.AppColors.Divider
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DrawerMenu(
|
fun DrawerMenu(
|
||||||
|
|||||||
@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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")
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user