Mengubah Warna dan menyesuaikan UI/UX Halaman AI Helper

This commit is contained in:
202310715297 RAIHAN ARIQ MUZAKKI 2025-12-13 23:15:24 +07:00
parent 0f0ac6b8f3
commit 80774b58ea
7 changed files with 563 additions and 503 deletions

View File

@ -7,7 +7,7 @@ plugins {
android { android {
namespace = "com.example.notesai" namespace = "com.example.notesai"
compileSdk = 34 compileSdk = 35
defaultConfig { defaultConfig {
applicationId = "com.example.notesai" applicationId = "com.example.notesai"
@ -64,6 +64,9 @@ dependencies {
implementation("androidx.datastore:datastore-preferences:1.0.0") implementation("androidx.datastore:datastore-preferences:1.0.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
implementation("com.google.ai.client.generativeai:generativeai:0.9.0") implementation("com.google.ai.client.generativeai:generativeai:0.9.0")
implementation(libs.androidx.ui.text)
implementation(libs.androidx.material3)
implementation(libs.androidx.animation.core)
// Untuk integrasi Gemini AI (optional - uncomment jika sudah ada API key) // Untuk integrasi Gemini AI (optional - uncomment jika sudah ada API key)
// implementation("com.google.ai.client.generativeai:generativeai:0.1.2") // implementation("com.google.ai.client.generativeai:generativeai:0.1.2")

View File

@ -1,71 +1,41 @@
package com.example.notesai.presentation.screens.ai package com.example.notesai.presentation.screens.ai
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.*
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.rememberScrollState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.filled.Folder import androidx.compose.material3.*
import androidx.compose.material.icons.filled.Send import androidx.compose.runtime.*
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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.example.notesai.data.model.Note import androidx.compose.ui.unit.sp
import com.example.notesai.data.model.ChatMessage
import com.example.notesai.data.model.Category import com.example.notesai.data.model.Category
import com.example.notesai.config.APIKey import com.example.notesai.data.model.ChatMessage
import com.example.notesai.presentation.screens.ai.components.ChatBubble import com.example.notesai.data.model.Note
import com.example.notesai.presentation.screens.ai.components.CompactStatItem import com.example.notesai.util.Constants
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.GenerativeModel
import com.google.ai.client.generativeai.type.generationConfig import com.google.ai.client.generativeai.type.generationConfig
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.collections.plus import java.text.SimpleDateFormat
import java.util.*
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.presentation.screens.ai.components.StatItem
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -86,11 +56,11 @@ fun AIHelperScreen(
val clipboardManager = LocalClipboardManager.current val clipboardManager = LocalClipboardManager.current
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
// Inisialisasi Gemini Model // Inisialisasi Gemini Model
val generativeModel = remember { val generativeModel = remember {
GenerativeModel( GenerativeModel(
modelName = "gemini-2.5-flash", modelName = "gemini-2.0-flash-exp",
apiKey = APIKey.GEMINI_API_KEY, apiKey = com.example.notesai.config.APIKey.GEMINI_API_KEY,
generationConfig = generationConfig { generationConfig = generationConfig {
temperature = 0.8f temperature = 0.8f
topK = 40 topK = 40
@ -110,217 +80,197 @@ fun AIHelperScreen(
} }
Column( Column(
modifier = Modifier.Companion modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(MaterialTheme.colorScheme.background) .background(Constants.AppColors.Background)
) { ) {
// Header // Category Selector & Stats - Compact
Card( Surface(
modifier = Modifier.Companion.fillMaxWidth(), color = Constants.AppColors.Surface,
colors = CardDefaults.cardColors(containerColor = Color.Companion.Transparent), shadowElevation = 2.dp
shape = RoundedCornerShape(0.dp)
) { ) {
Box( Column(
modifier = Modifier.Companion modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.background( .padding(16.dp)
brush = Brush.Companion.linearGradient(
colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
)
)
.padding(20.dp)
) { ) {
Column { // Category Selector
Row(verticalAlignment = Alignment.Companion.CenterVertically) { Box {
Icon( Card(
Icons.Default.Star, onClick = { showCategoryDropdown = !showCategoryDropdown },
contentDescription = null, modifier = Modifier.fillMaxWidth(),
tint = Color(0xFFFBBF24), colors = CardDefaults.cardColors(
modifier = Modifier.Companion.size(28.dp) containerColor = Constants.AppColors.SurfaceVariant
) ),
Spacer(modifier = Modifier.Companion.width(12.dp)) shape = RoundedCornerShape(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) { Row(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(
Icons.Default.Folder,
contentDescription = null,
tint = Constants.AppColors.Primary,
modifier = Modifier.size(20.dp)
)
Text(
selectedCategory?.name ?: "Semua Kategori",
color = Constants.AppColors.OnSurface,
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium
)
}
Icon( Icon(
Icons.Default.Folder, Icons.Default.ArrowDropDown,
contentDescription = null, contentDescription = null,
tint = Color(0xFF6366F1), tint = Constants.AppColors.OnSurfaceVariant
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( DropdownMenu(
expanded = showCategoryDropdown, expanded = showCategoryDropdown,
onDismissRequest = { showCategoryDropdown = false }, onDismissRequest = { showCategoryDropdown = false },
modifier = Modifier.Companion modifier = Modifier
.fillMaxWidth() .fillMaxWidth(0.9f)
.background(Color(0xFF1E293B)) .background(Constants.AppColors.SurfaceElevated)
) { ) {
DropdownMenuItem(
text = { Text("Semua Kategori", color = Color.Companion.White) },
onClick = {
selectedCategory = null
showCategoryDropdown = false
}
)
categories.forEach { category ->
DropdownMenuItem( DropdownMenuItem(
text = { Text(category.name, color = Color.Companion.White) }, text = { Text("Semua Kategori", color = Constants.AppColors.OnSurface) },
onClick = { onClick = {
selectedCategory = category selectedCategory = null
showCategoryDropdown = false showCategoryDropdown = false
} }
) )
categories.forEach { category ->
DropdownMenuItem(
text = { Text(category.name, color = Constants.AppColors.OnSurface) },
onClick = {
selectedCategory = category
showCategoryDropdown = false
}
)
}
} }
} }
}
// Stats - Compact Spacer(modifier = Modifier.height(12.dp))
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( // Stats - Compact
modifier = Modifier.Companion.fillMaxWidth(), val filteredNotes = if (selectedCategory != null) {
horizontalArrangement = Arrangement.SpaceEvenly notes.filter { it.categoryId == selectedCategory!!.id && !it.isArchived }
) { } else {
CompactStatItem( notes.filter { !it.isArchived }
label = "Total", }
value = filteredNotes.size.toString(),
color = Color(0xFF6366F1) Row(
) modifier = Modifier.fillMaxWidth(),
CompactStatItem( horizontalArrangement = Arrangement.SpaceEvenly
label = "Dipasang", ) {
value = filteredNotes.count { it.isPinned }.toString(), CompactStatItem(
color = Color(0xFFFBBF24) icon = Icons.Default.Description,
) value = filteredNotes.size.toString(),
CompactStatItem( label = "Catatan"
label = "Kategori", )
value = categories.size.toString(), CompactStatItem(
color = Color(0xFFA855F7) icon = Icons.Default.Star,
) value = filteredNotes.count { it.isPinned }.toString(),
label = "Dipasang"
)
CompactStatItem(
icon = Icons.Default.Folder,
value = categories.size.toString(),
label = "Kategori"
)
}
} }
} }
Divider(color = Color(0xFF334155), thickness = 1.dp) HorizontalDivider(color = Constants.AppColors.Divider)
// Chat Area // Chat Area
Column( Box(
modifier = Modifier.Companion modifier = Modifier
.weight(1f) .weight(1f)
.fillMaxWidth() .fillMaxWidth()
) { ) {
if (chatMessages.isEmpty()) { if (chatMessages.isEmpty()) {
// Welcome State // Welcome State
Column( Column(
modifier = Modifier.Companion modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(32.dp), .padding(32.dp),
horizontalAlignment = Alignment.Companion.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
Icon( Box(
Icons.Default.Star, modifier = Modifier
contentDescription = null, .size(80.dp)
modifier = Modifier.Companion.size(64.dp), .background(
tint = Color(0xFF6366F1).copy(0.5f) color = Constants.AppColors.Primary.copy(alpha = 0.1f),
) shape = CircleShape
Spacer(modifier = Modifier.Companion.height(16.dp)) ),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Default.AutoAwesome,
contentDescription = null,
modifier = Modifier.size(40.dp),
tint = Constants.AppColors.Primary
)
}
Spacer(modifier = Modifier.height(24.dp))
Text( Text(
"Mulai Percakapan", "AI Assistant",
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.headlineMedium,
color = Color.Companion.White, color = Constants.AppColors.OnBackground,
fontWeight = FontWeight.Companion.Bold fontWeight = FontWeight.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)) Spacer(modifier = Modifier.height(8.dp))
Text(
"Tanyakan apa saja tentang catatan Anda",
style = MaterialTheme.typography.bodyLarge,
color = Constants.AppColors.OnSurfaceVariant,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(32.dp))
// Suggestion Chips // Suggestion Chips
Column( Column(
horizontalAlignment = Alignment.Companion.Start, horizontalAlignment = Alignment.Start,
modifier = Modifier.Companion.fillMaxWidth(0.8f) verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth(0.85f)
) { ) {
Text( Text(
"Contoh pertanyaan:", "Contoh pertanyaan:",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.labelMedium,
color = Color(0xFF64748B), color = Constants.AppColors.OnSurfaceTertiary
modifier = Modifier.Companion.padding(bottom = 8.dp)
) )
SuggestionChip("Analisis catatan saya", onSelect = { prompt = it }) SuggestionChip("Analisis catatan saya") { prompt = it }
SuggestionChip("Buat ringkasan", onSelect = { prompt = it }) SuggestionChip("Buat ringkasan") { prompt = it }
SuggestionChip("Berikan saran organisasi", onSelect = { prompt = it }) SuggestionChip("Berikan saran organisasi") { prompt = it }
} }
} }
} else { } else {
// Chat Messages // Chat Messages
Column( Column(
modifier = Modifier.Companion modifier = Modifier
.fillMaxSize() .fillMaxSize()
.verticalScroll(scrollState) .verticalScroll(scrollState)
.padding(16.dp) .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 100.dp)
) { ) {
chatMessages.forEach { message -> chatMessages.forEach { message ->
ChatBubble( ChatBubble(
@ -336,36 +286,34 @@ fun AIHelperScreen(
}, },
showCopied = showCopiedMessage && copiedMessageId == message.id showCopied = showCopiedMessage && copiedMessageId == message.id
) )
Spacer(modifier = Modifier.Companion.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
} }
// Loading Indicator // Loading Indicator
if (isLoading) { if (isLoading) {
Row( Row(
modifier = Modifier.Companion modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 8.dp), .padding(vertical = 8.dp),
horizontalArrangement = Arrangement.Start horizontalArrangement = Arrangement.Start
) { ) {
Card( Surface(
colors = CardDefaults.cardColors( color = Constants.AppColors.SurfaceVariant,
containerColor = Color(0xFF1E293B) shape = RoundedCornerShape(16.dp)
),
shape = androidx.compose.foundation.shape.RoundedCornerShape(16.dp)
) { ) {
Row( Row(
modifier = Modifier.Companion.padding(16.dp), modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.Companion.CenterVertically verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
CircularProgressIndicator( CircularProgressIndicator(
modifier = Modifier.Companion.size(20.dp), modifier = Modifier.size(20.dp),
color = Color(0xFF6366F1), color = Constants.AppColors.Primary,
strokeWidth = 2.dp strokeWidth = 2.dp
) )
Spacer(modifier = Modifier.Companion.width(12.dp))
Text( Text(
"AI sedang berpikir...", "AI sedang berpikir...",
color = Color(0xFF94A3B8), color = Constants.AppColors.OnSurfaceVariant,
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium
) )
} }
@ -375,52 +323,46 @@ fun AIHelperScreen(
// Error Message // Error Message
if (errorMessage.isNotEmpty()) { if (errorMessage.isNotEmpty()) {
Card( Surface(
modifier = Modifier.Companion.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors( color = Constants.AppColors.Error.copy(alpha = 0.1f),
containerColor = Color(0xFFEF4444).copy(0.2f) shape = RoundedCornerShape(12.dp)
),
shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp)
) { ) {
Row( Row(
modifier = Modifier.Companion.padding(12.dp), modifier = Modifier.padding(12.dp),
verticalAlignment = Alignment.Companion.CenterVertically verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
Icon( Icon(
Icons.Default.Warning, Icons.Default.Warning,
contentDescription = null, contentDescription = null,
tint = Color(0xFFEF4444), tint = Constants.AppColors.Error,
modifier = Modifier.Companion.size(20.dp) modifier = Modifier.size(20.dp)
) )
Spacer(modifier = Modifier.Companion.width(8.dp))
Text( Text(
errorMessage, errorMessage,
color = Color(0xFFEF4444), color = Constants.AppColors.Error,
style = MaterialTheme.typography.bodySmall style = MaterialTheme.typography.bodySmall
) )
} }
} }
} }
Spacer(modifier = Modifier.Companion.height(80.dp))
} }
} }
} }
// Input Area // Input Area - Minimalist
Card( Surface(
modifier = Modifier.Companion.fillMaxWidth(), color = Constants.AppColors.Surface,
colors = CardDefaults.cardColors( shadowElevation = 8.dp,
containerColor = Color(0xFF1E293B) shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp)
),
shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) { ) {
Row( Row(
modifier = Modifier.Companion modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp), .padding(16.dp),
verticalAlignment = Alignment.Companion.Bottom verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
OutlinedTextField( OutlinedTextField(
value = prompt, value = prompt,
@ -428,33 +370,30 @@ fun AIHelperScreen(
placeholder = { placeholder = {
Text( Text(
"Ketik pesan...", "Ketik pesan...",
color = Color(0xFF64748B) color = Constants.AppColors.OnSurfaceTertiary
) )
}, },
modifier = Modifier.Companion modifier = Modifier
.weight(1f) .weight(1f)
.heightIn(min = 48.dp, max = 120.dp), .heightIn(min = 48.dp, max = 120.dp),
colors = TextFieldDefaults.colors( colors = OutlinedTextFieldDefaults.colors(
focusedTextColor = Color.Companion.White, focusedTextColor = Constants.AppColors.OnBackground,
unfocusedTextColor = Color.Companion.White, unfocusedTextColor = Constants.AppColors.OnSurface,
focusedContainerColor = Color(0xFF334155), focusedContainerColor = Constants.AppColors.SurfaceVariant,
unfocusedContainerColor = Color(0xFF334155), unfocusedContainerColor = Constants.AppColors.SurfaceVariant,
cursorColor = Color(0xFFA855F7), cursorColor = Constants.AppColors.Primary,
focusedIndicatorColor = Color(0xFF6366F1), focusedBorderColor = Constants.AppColors.Primary,
unfocusedIndicatorColor = Color(0xFF475569) unfocusedBorderColor = Color.Transparent
), ),
shape = androidx.compose.foundation.shape.RoundedCornerShape(24.dp), shape = RoundedCornerShape(24.dp),
maxLines = 4 maxLines = 4
) )
Spacer(modifier = Modifier.Companion.width(12.dp))
// Send Button // Send Button
FloatingActionButton( FloatingActionButton(
onClick = { onClick = {
if (prompt.isNotBlank() && !isLoading) { if (prompt.isNotBlank() && !isLoading) {
scope.launch { scope.launch {
// Add user message
chatMessages = chatMessages + ChatMessage( chatMessages = chatMessages + ChatMessage(
message = prompt, message = prompt,
isUser = true isUser = true
@ -485,18 +424,14 @@ fun AIHelperScreen(
} }
} }
val fullPrompt = val fullPrompt = "$notesContext\n\nPertanyaan: $userPrompt\n\nBerikan jawaban dalam bahasa Indonesia yang natural dan membantu."
"$notesContext\n\nPertanyaan: $userPrompt\n\nBerikan jawaban dalam bahasa Indonesia yang natural dan membantu."
val result = generativeModel.generateContent(fullPrompt) val result = generativeModel.generateContent(fullPrompt)
val response = result.text ?: "Tidak ada respons dari AI" val response = result.text ?: "Tidak ada respons dari AI"
// Add AI response
chatMessages = chatMessages + ChatMessage( chatMessages = chatMessages + ChatMessage(
message = response, message = response,
isUser = false isUser = false
) )
} catch (e: Exception) { } catch (e: Exception) {
errorMessage = "Error: ${e.message}" errorMessage = "Error: ${e.message}"
} finally { } finally {
@ -505,21 +440,14 @@ fun AIHelperScreen(
} }
} }
}, },
containerColor = Color.Companion.Transparent, containerColor = Constants.AppColors.Primary,
modifier = Modifier.Companion modifier = Modifier.size(48.dp)
.size(48.dp)
.background(
brush = Brush.Companion.linearGradient(
colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
),
shape = CircleShape
)
) { ) {
Icon( Icon(
Icons.Default.Send, Icons.Default.Send,
contentDescription = "Send", contentDescription = "Send",
tint = Color.Companion.White, tint = Color.White,
modifier = Modifier.Companion.size(24.dp) modifier = Modifier.size(20.dp)
) )
} }
} }

View File

@ -1,11 +1,16 @@
package com.example.notesai.presentation.screens.ai.components package com.example.notesai.presentation.screens.ai.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AutoAwesome import androidx.compose.material.icons.filled.AutoAwesome
@ -15,6 +20,7 @@ import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -24,6 +30,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.example.notesai.data.model.ChatMessage import com.example.notesai.data.model.ChatMessage
import com.example.notesai.util.Constants
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
@ -41,28 +48,34 @@ fun ChatBubble(
horizontalArrangement = if (message.isUser) Arrangement.End else Arrangement.Start horizontalArrangement = if (message.isUser) Arrangement.End else Arrangement.Start
) { ) {
if (!message.isUser) { if (!message.isUser) {
// Ganti ikon bintang dengan ikon robot/sparkles Box(
Icon(
Icons.Default.AutoAwesome, // Atau bisa diganti dengan ikon lain seperti AutoAwesome
contentDescription = null,
tint = Color(0xFF6366F1), // Warna ungu/biru untuk AI
modifier = Modifier modifier = Modifier
.size(32.dp) .size(32.dp)
.padding(end = 8.dp) .background(
) color = Constants.AppColors.Primary.copy(alpha = 0.1f),
shape = CircleShape
),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Default.AutoAwesome,
contentDescription = null,
tint = Constants.AppColors.Primary,
modifier = Modifier.size(16.dp)
)
}
Spacer(modifier = Modifier.width(8.dp))
} }
Column( Column(
modifier = Modifier.fillMaxWidth(0.85f), modifier = Modifier.fillMaxWidth(0.85f),
horizontalAlignment = if (message.isUser) Alignment.End else Alignment.Start horizontalAlignment = if (message.isUser) Alignment.End else Alignment.Start
) { ) {
Card( Surface(
colors = CardDefaults.cardColors( color = if (message.isUser)
containerColor = if (message.isUser) Constants.AppColors.Primary
Color(0xFF6366F1) else
else Constants.AppColors.SurfaceVariant,
Color(0xFF1E293B)
),
shape = RoundedCornerShape( shape = RoundedCornerShape(
topStart = 16.dp, topStart = 16.dp,
topEnd = 16.dp, topEnd = 16.dp,
@ -73,7 +86,7 @@ fun ChatBubble(
Column(modifier = Modifier.padding(12.dp)) { Column(modifier = Modifier.padding(12.dp)) {
Text( Text(
message.message, message.message,
color = Color.White, color = if (message.isUser) Color.White else Constants.AppColors.OnSurface,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
lineHeight = 20.sp lineHeight = 20.sp
) )
@ -85,21 +98,25 @@ fun ChatBubble(
) { ) {
Text( Text(
dateFormat.format(Date(message.timestamp)), dateFormat.format(Date(message.timestamp)),
color = Color.White.copy(0.6f), color = if (message.isUser)
Color.White.copy(0.7f)
else
Constants.AppColors.OnSurfaceTertiary,
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
fontSize = 11.sp,
modifier = Modifier.padding(top = 4.dp) modifier = Modifier.padding(top = 4.dp)
) )
if (!message.isUser) { if (!message.isUser) {
IconButton( IconButton(
onClick = onCopy, onClick = onCopy,
modifier = Modifier.size(32.dp) modifier = Modifier.size(28.dp)
) { ) {
Icon( Icon(
Icons.Default.ContentCopy, Icons.Default.ContentCopy,
contentDescription = "Copy", contentDescription = "Copy",
tint = Color.White.copy(0.7f), tint = Constants.AppColors.OnSurfaceVariant,
modifier = Modifier.size(16.dp) modifier = Modifier.size(14.dp)
) )
} }
} }
@ -110,8 +127,9 @@ fun ChatBubble(
if (showCopied && !message.isUser) { if (showCopied && !message.isUser) {
Text( Text(
"✓ Disalin", "✓ Disalin",
color = Color(0xFF10B981), color = Constants.AppColors.Success,
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
fontSize = 11.sp,
modifier = Modifier.padding(top = 4.dp, start = 8.dp) modifier = Modifier.padding(top = 4.dp, start = 8.dp)
) )
} }

View File

@ -1,11 +1,15 @@
package com.example.notesai.presentation.screens.ai.components package com.example.notesai.presentation.screens.ai.components
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -14,29 +18,45 @@ import androidx.compose.ui.Modifier
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 androidx.compose.ui.unit.sp
import com.example.notesai.util.Constants
@Composable @Composable
fun CompactStatItem(label: String, value: String, color: Color) { fun CompactStatItem(
icon: androidx.compose.ui.graphics.vector.ImageVector,
value: String,
label: String
) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp),
modifier = Modifier modifier = Modifier
.background( .background(
color = Color(0xFF1E293B), color = Constants.AppColors.SurfaceVariant,
shape = RoundedCornerShape(8.dp) shape = RoundedCornerShape(8.dp)
) )
.padding(horizontal = 12.dp, vertical = 8.dp) .padding(horizontal = 12.dp, vertical = 8.dp)
) { ) {
Text( Icon(
value, icon,
style = MaterialTheme.typography.titleMedium, contentDescription = null,
color = color, tint = Constants.AppColors.Primary,
fontWeight = FontWeight.Bold modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.width(4.dp))
Text(
label,
style = MaterialTheme.typography.bodySmall,
color = Color(0xFF94A3B8)
) )
Column {
Text(
value,
style = MaterialTheme.typography.titleMedium,
color = Constants.AppColors.OnBackground,
fontWeight = FontWeight.Bold,
fontSize = 16.sp
)
Text(
label,
style = MaterialTheme.typography.bodySmall,
color = Constants.AppColors.OnSurfaceTertiary,
fontSize = 11.sp
)
}
} }
} }

View File

@ -1,6 +1,7 @@
package com.example.notesai.presentation.screens.ai.components package com.example.notesai.presentation.screens.ai.components
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -8,44 +9,48 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Lightbulb
import androidx.compose.material.icons.filled.Star import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.notesai.util.Constants
@Composable @Composable
fun SuggestionChip(text: String, onSelect: (String) -> Unit) { fun SuggestionChip(
Card( text: String,
modifier = Modifier onSelect: (String) -> Unit
.padding(vertical = 4.dp) ) {
.clickable { onSelect(text) }, Surface(
colors = CardDefaults.cardColors( onClick = { onSelect(text) },
containerColor = Color(0xFF1E293B) color = Constants.AppColors.SurfaceVariant,
),
shape = RoundedCornerShape(12.dp) shape = RoundedCornerShape(12.dp)
) { ) {
Row( Row(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
Icon( Icon(
Icons.Default.Star, Icons.Default.Lightbulb,
contentDescription = null, contentDescription = null,
tint = Color(0xFF6366F1), tint = Constants.AppColors.Primary,
modifier = Modifier.size(16.dp) modifier = Modifier.size(16.dp)
) )
Spacer(modifier = Modifier.width(8.dp))
Text( Text(
text, text,
color = Color.White, color = Constants.AppColors.OnSurface,
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium,
fontSize = 14.sp
) )
} }
} }

View File

@ -1,21 +1,28 @@
// File: presentation/screens/main/components/CategoryCard.kt
package com.example.notesai.presentation.screens.main.components package com.example.notesai.presentation.screens.main.components
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.FolderOpen
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Brush 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 androidx.compose.ui.unit.sp
import com.example.notesai.data.model.Category import com.example.notesai.data.model.Category
import com.example.notesai.util.Constants
@Composable @Composable
fun CategoryCard( fun CategoryCard(
@ -28,16 +35,33 @@ fun CategoryCard(
var showDeleteConfirm by remember { mutableStateOf(false) } var showDeleteConfirm by remember { mutableStateOf(false) }
var showEditDialog by remember { mutableStateOf(false) } var showEditDialog by remember { mutableStateOf(false) }
var showMenu by remember { mutableStateOf(false) } var showMenu by remember { mutableStateOf(false) }
var isPressed by remember { mutableStateOf(false) }
// Smooth scale animation
val scale by animateFloatAsState(
targetValue = if (isPressed) 0.95f else 1f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
),
label = "scale"
)
// Delete Confirmation Dialog // Delete Confirmation Dialog
if (showDeleteConfirm) { if (showDeleteConfirm) {
AlertDialog( AlertDialog(
onDismissRequest = { showDeleteConfirm = false }, onDismissRequest = { showDeleteConfirm = false },
title = { Text("Pindahkan ke Sampah?", color = Color.White) }, title = {
Text(
"Pindahkan ke Sampah?",
color = Constants.AppColors.OnBackground,
fontWeight = FontWeight.Bold
)
},
text = { text = {
Text( Text(
"Kategori '${category.name}' dan semua catatan di dalamnya akan dipindahkan ke sampah.", "Kategori '${category.name}' dan $noteCount catatan di dalamnya akan dipindahkan ke sampah.",
color = Color.White color = Constants.AppColors.OnSurfaceVariant
) )
}, },
confirmButton = { confirmButton = {
@ -47,23 +71,19 @@ fun CategoryCard(
showDeleteConfirm = false showDeleteConfirm = false
}, },
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFEF4444) containerColor = Constants.AppColors.Error
) )
) { ) {
Text("Hapus", color = Color.White) Text("Hapus", color = Color.White)
} }
}, },
dismissButton = { dismissButton = {
Button( TextButton(onClick = { showDeleteConfirm = false }) {
onClick = { showDeleteConfirm = false }, Text("Batal", color = Constants.AppColors.OnSurfaceVariant)
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF64748B)
)
) {
Text("Batal", color = Color.White)
} }
}, },
containerColor = Color(0xFF1E293B) containerColor = Constants.AppColors.Surface,
shape = RoundedCornerShape(Constants.Radius.Large.dp)
) )
} }
@ -79,13 +99,19 @@ fun CategoryCard(
) )
} }
// Main Card - Minimalist Design
Card( Card(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.scale(scale)
.clickable(onClick = onClick), .clickable(onClick = onClick),
shape = RoundedCornerShape(20.dp), shape = RoundedCornerShape(Constants.Radius.Large.dp),
colors = CardDefaults.cardColors(containerColor = Color.Transparent), colors = CardDefaults.cardColors(
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) containerColor = Constants.AppColors.Surface
),
elevation = CardDefaults.cardElevation(
defaultElevation = Constants.Elevation.Small.dp
)
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
@ -93,94 +119,146 @@ fun CategoryCard(
.background( .background(
brush = Brush.linearGradient( brush = Brush.linearGradient(
colors = listOf( colors = listOf(
Color(category.gradientStart), Color(category.gradientStart).copy(alpha = 0.1f),
Color(category.gradientEnd) Color(category.gradientEnd).copy(alpha = 0.05f)
) )
) )
) )
.padding(20.dp)
) { ) {
Column { Column(
Icon( modifier = Modifier
Icons.Default.Folder, .fillMaxWidth()
contentDescription = null, .padding(Constants.Spacing.Large.dp)
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)
)
}
// Menu Button (Titik Tiga)
Box(
modifier = Modifier.align(Alignment.TopEnd)
) { ) {
IconButton( Row(
onClick = { showMenu = true } modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Top
) { ) {
Icon( // Icon dengan gradient accent
Icons.Default.MoreVert, Box(
contentDescription = "Menu", modifier = Modifier
tint = Color.White.copy(0.9f) .size(48.dp)
) .clip(RoundedCornerShape(Constants.Radius.Medium.dp))
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(category.gradientStart).copy(alpha = 0.2f),
Color(category.gradientEnd).copy(alpha = 0.1f)
)
)
),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Outlined.FolderOpen,
contentDescription = null,
tint = Color(category.gradientStart),
modifier = Modifier.size(24.dp)
)
}
// Menu Button
Box {
IconButton(
onClick = { showMenu = true },
modifier = Modifier.size(32.dp)
) {
Icon(
Icons.Default.MoreVert,
contentDescription = "Menu",
tint = Constants.AppColors.OnSurfaceVariant,
modifier = Modifier.size(20.dp)
)
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false },
modifier = Modifier.background(Constants.AppColors.SurfaceElevated)
) {
DropdownMenuItem(
text = {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(Constants.Spacing.Small.dp)
) {
Icon(
Icons.Default.Edit,
contentDescription = null,
tint = Constants.AppColors.Primary,
modifier = Modifier.size(18.dp)
)
Text(
"Edit Kategori",
color = Constants.AppColors.OnSurface,
fontSize = 14.sp
)
}
},
onClick = {
showMenu = false
showEditDialog = true
}
)
DropdownMenuItem(
text = {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(Constants.Spacing.Small.dp)
) {
Icon(
Icons.Default.Delete,
contentDescription = null,
tint = Constants.AppColors.Error,
modifier = Modifier.size(18.dp)
)
Text(
"Pindah ke Sampah",
color = Constants.AppColors.OnSurface,
fontSize = 14.sp
)
}
},
onClick = {
showMenu = false
showDeleteConfirm = true
}
)
}
}
} }
DropdownMenu( Spacer(modifier = Modifier.height(Constants.Spacing.Medium.dp))
expanded = showMenu,
onDismissRequest = { showMenu = false },
modifier = Modifier.background(Color(0xFF1E293B))
) {
DropdownMenuItem(
text = {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(
Icons.Default.Edit,
contentDescription = null,
tint = Color(0xFF6366F1),
modifier = Modifier.size(20.dp)
)
Text("Edit Kategori", color = Color.White)
}
},
onClick = {
showMenu = false
showEditDialog = true
}
)
DropdownMenuItem( // Category Name
text = { Text(
Row( category.name,
verticalAlignment = Alignment.CenterVertically, style = MaterialTheme.typography.titleLarge,
horizontalArrangement = Arrangement.spacedBy(8.dp) fontWeight = FontWeight.Bold,
) { color = Constants.AppColors.OnBackground,
Icon( fontSize = 18.sp
Icons.Default.Delete, )
contentDescription = null,
tint = Color(0xFFEF4444), Spacer(modifier = Modifier.height(Constants.Spacing.ExtraSmall.dp))
modifier = Modifier.size(20.dp)
) // Note Count dengan subtle styling
Text("Pindah ke Sampah", color = Color.White) Row(
} verticalAlignment = Alignment.CenterVertically,
}, horizontalArrangement = Arrangement.spacedBy(4.dp)
onClick = { ) {
showMenu = false Icon(
showDeleteConfirm = true Icons.Default.Description,
} contentDescription = null,
tint = Constants.AppColors.OnSurfaceTertiary,
modifier = Modifier.size(14.dp)
)
Text(
"$noteCount catatan",
style = MaterialTheme.typography.bodySmall,
color = Constants.AppColors.OnSurfaceTertiary,
fontSize = 13.sp
) )
} }
} }
@ -194,31 +272,23 @@ fun EditCategoryDialog(
onDismiss: () -> Unit, onDismiss: () -> Unit,
onSave: (String, Long, Long) -> Unit onSave: (String, Long, Long) -> Unit
) { ) {
val gradients = listOf(
Pair(0xFF6366F1L, 0xFFA855F7L),
Pair(0xFFEC4899L, 0xFFF59E0BL),
Pair(0xFF8B5CF6L, 0xFFEC4899L),
Pair(0xFF06B6D4L, 0xFF3B82F6L),
Pair(0xFF10B981L, 0xFF059669L),
Pair(0xFFF59E0BL, 0xFFEF4444L),
Pair(0xFF6366F1L, 0xFF8B5CF6L),
Pair(0xFFEF4444L, 0xFFDC2626L)
)
var name by remember { mutableStateOf(category.name) } var name by remember { mutableStateOf(category.name) }
var selectedGradient by remember { var selectedGradient by remember {
mutableStateOf( mutableStateOf(
gradients.indexOfFirst { Constants.AppColors.CategoryColors.indexOfFirst {
it.first == category.gradientStart && it.second == category.gradientEnd it.first == category.gradientStart && it.second == category.gradientEnd
}.takeIf { it >= 0 } ?: 0 }.takeIf { it >= 0 } ?: 0
) )
} }
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
containerColor = Color(0xFF1E293B), containerColor = Constants.AppColors.Surface,
shape = RoundedCornerShape(Constants.Radius.Large.dp),
title = { title = {
Text( Text(
"Edit Kategori", "Edit Kategori",
color = Color.White, color = Constants.AppColors.OnBackground,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
}, },
@ -227,42 +297,51 @@ fun EditCategoryDialog(
OutlinedTextField( OutlinedTextField(
value = name, value = name,
onValueChange = { name = it }, onValueChange = { name = it },
label = { Text("Nama Kategori", color = Color(0xFF94A3B8)) }, label = {
Text(
"Nama Kategori",
color = Constants.AppColors.OnSurfaceVariant
)
},
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = TextFieldDefaults.colors( colors = OutlinedTextFieldDefaults.colors(
focusedTextColor = Color.White, focusedTextColor = Constants.AppColors.OnBackground,
unfocusedTextColor = Color.White, unfocusedTextColor = Constants.AppColors.OnSurface,
focusedContainerColor = Color(0xFF334155), focusedContainerColor = Constants.AppColors.SurfaceVariant,
unfocusedContainerColor = Color(0xFF334155), unfocusedContainerColor = Constants.AppColors.SurfaceVariant,
cursorColor = Color(0xFFA855F7), cursorColor = Constants.AppColors.Primary,
focusedIndicatorColor = Color(0xFFA855F7), focusedBorderColor = Constants.AppColors.Primary,
unfocusedIndicatorColor = Color(0xFF64748B) unfocusedBorderColor = Constants.AppColors.Border
), ),
shape = RoundedCornerShape(12.dp) shape = RoundedCornerShape(Constants.Radius.Medium.dp)
) )
Spacer(modifier = Modifier.height(20.dp)) Spacer(modifier = Modifier.height(20.dp))
Text( Text(
"Pilih Gradient:", "Pilih Warna:",
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = Color.White, color = Constants.AppColors.OnSurface,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold,
fontSize = 14.sp
) )
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
gradients.chunked(4).forEach { row -> Constants.AppColors.CategoryColors.chunked(4).forEach { row ->
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
row.forEachIndexed { _, gradient -> row.forEachIndexed { _, gradient ->
val globalIndex = gradients.indexOf(gradient) val globalIndex = Constants.AppColors.CategoryColors.indexOf(gradient)
val isSelected = selectedGradient == globalIndex
Box( Box(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.aspectRatio(1f) .aspectRatio(1f)
.clip(RoundedCornerShape(12.dp)) .clip(RoundedCornerShape(Constants.Radius.Medium.dp))
.background( .background(
brush = Brush.linearGradient( brush = Brush.linearGradient(
colors = listOf( colors = listOf(
@ -274,7 +353,11 @@ fun EditCategoryDialog(
.clickable { selectedGradient = globalIndex }, .clickable { selectedGradient = globalIndex },
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
if (selectedGradient == globalIndex) { this@Row.AnimatedVisibility(
visible = isSelected,
enter = scaleIn() + fadeIn(),
exit = scaleOut() + fadeOut()
) {
Icon( Icon(
Icons.Default.Check, Icons.Default.Check,
contentDescription = null, contentDescription = null,
@ -293,19 +376,13 @@ fun EditCategoryDialog(
Button( Button(
onClick = { onClick = {
if (name.isNotBlank()) { if (name.isNotBlank()) {
val gradient = gradients[selectedGradient] val gradient = Constants.AppColors.CategoryColors[selectedGradient]
onSave(name, gradient.first, gradient.second) onSave(name, gradient.first, gradient.second)
} }
}, },
enabled = name.isNotBlank(), enabled = name.isNotBlank(),
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent containerColor = Constants.AppColors.Primary
),
modifier = Modifier.background(
brush = Brush.linearGradient(
colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
),
shape = RoundedCornerShape(8.dp)
) )
) { ) {
Text("Simpan", color = Color.White, fontWeight = FontWeight.Bold) Text("Simpan", color = Color.White, fontWeight = FontWeight.Bold)
@ -313,7 +390,7 @@ fun EditCategoryDialog(
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismiss) { TextButton(onClick = onDismiss) {
Text("Batal", color = Color(0xFF94A3B8)) Text("Batal", color = Constants.AppColors.OnSurfaceVariant)
} }
} }
) )

View File

@ -1,38 +1,28 @@
// File: presentation/screens/main/components/NoteCard.kt
package com.example.notesai.presentation.screens.main.components package com.example.notesai.presentation.screens.main.components
import androidx.compose.animation.core.*
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.*
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.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Star import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.outlined.StarBorder import androidx.compose.material.icons.outlined.StarBorder
import androidx.compose.material3.Card import androidx.compose.material3.*
import androidx.compose.material3.CardDefaults import androidx.compose.runtime.*
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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
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.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.example.notesai.data.model.Note import com.example.notesai.data.model.Note
import com.example.notesai.util.Constants
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.*
import java.util.Locale
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
@ -43,86 +33,105 @@ fun NoteCard(
) { ) {
val dateFormat = SimpleDateFormat("dd MMM, HH:mm", Locale("id", "ID")) val dateFormat = SimpleDateFormat("dd MMM, HH:mm", Locale("id", "ID"))
// Scale animation on press
var isPressed by remember { mutableStateOf(false) }
val scale by animateFloatAsState(
targetValue = if (isPressed) 0.95f else 1f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
),
label = "scale"
)
Card( Card(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.combinedClickable( .scale(scale)
onClick = onClick, .combinedClickable(onClick = onClick),
),
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
containerColor = Color(0xFF1E293B) containerColor = Constants.AppColors.SurfaceVariant // Lebih terang dari Surface
), ),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) elevation = CardDefaults.cardElevation(
defaultElevation = 2.dp
)
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp) .padding(16.dp)
) { ) {
// Header: Title + Pin
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Top verticalAlignment = Alignment.Top
) { ) {
// Judul // Title
Text( Text(
note.title, note.title,
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = Color.White, color = Constants.AppColors.OnBackground,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
maxLines = 2, maxLines = 2,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis,
fontSize = 18.sp
) )
// Pin Button
IconButton( IconButton(
onClick = onPinClick, onClick = onPinClick,
modifier = Modifier.size(24.dp) modifier = Modifier.size(32.dp)
) { ) {
Icon( Icon(
if (note.isPinned) Icons.Filled.Star else Icons.Outlined.StarBorder, if (note.isPinned) Icons.Filled.Star else Icons.Outlined.StarBorder,
contentDescription = "Pin", contentDescription = "Pin",
tint = if (note.isPinned) Color(0xFFFBBF24) else Color.Gray, tint = if (note.isPinned) Constants.AppColors.Warning else Constants.AppColors.OnSurfaceVariant,
modifier = Modifier.size(18.dp) modifier = Modifier.size(18.dp)
) )
} }
} }
// Deskripsi // Content Preview
if (note.content.isNotEmpty()) { if (note.content.isNotEmpty()) {
Spacer(modifier = Modifier.height(12.dp)) 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( Text(
note.content, note.content,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
maxLines = 4, maxLines = 4,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
color = Color(0xFFCBD5E1), color = Constants.AppColors.OnSurfaceVariant,
lineHeight = 20.sp lineHeight = 20.sp,
fontSize = 14.sp
) )
} }
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(16.dp))
// Divider
HorizontalDivider( HorizontalDivider(
color = Color(0xFF334155), color = Constants.AppColors.Divider,
thickness = 1.dp thickness = 1.dp
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(12.dp))
// Timestamp // Footer: Timestamp
Text( Row(
dateFormat.format(Date(note.timestamp)), modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.bodySmall, horizontalArrangement = Arrangement.SpaceBetween,
color = Color(0xFF64748B) verticalAlignment = Alignment.CenterVertically
) ) {
Text(
dateFormat.format(Date(note.timestamp)),
style = MaterialTheme.typography.bodySmall,
color = Constants.AppColors.OnSurfaceTertiary,
fontSize = 12.sp
)
}
} }
} }
} }