Merge remote-tracking branch 'origin/1.1.0' into 1.1.0

This commit is contained in:
202310715082 FAZRI ABDURRAHMAN 2025-12-24 12:26:56 +07:00
commit 9e7c273ec7
13 changed files with 247 additions and 310 deletions

View File

@ -301,6 +301,4 @@
## **Features for Sprint 5 v1.1.0** ## **Features for Sprint 5 v1.1.0**
* Fungsi AI (Upload File) (ok) * Fungsi AI (Upload File) (ok)
* Fitur Sematkan Category, otomatis paling atas (ok) * Fitur Sematkan Category, otomatis paling atas (ok)
## **Unit Testing is Coming**

View File

@ -101,9 +101,6 @@ dependencies {
// PDF Parser // PDF Parser
implementation("com.tom-roush:pdfbox-android:2.0.27.0") implementation("com.tom-roush:pdfbox-android:2.0.27.0")
// FIREBASE SUDAH DIHAPUS - baris ini yang menyebabkan error
// implementation(libs.firebase.firestore.ktx)
// Testing // Testing
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.ext:junit:1.1.5")

View File

@ -40,52 +40,95 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContent { setContent {
MaterialTheme( NotesAppTheme()
colorScheme = darkColorScheme( }
primary = AppColors.Primary, }
onPrimary = Color.White, }
primaryContainer = AppColors.Primary.copy(alpha = 0.3f),
onPrimaryContainer = Color.White, @Composable
secondary = AppColors.Secondary, fun NotesAppTheme(content: @Composable () -> Unit = { NotesApp() }) {
onSecondary = Color.White, val context = LocalContext.current
secondaryContainer = AppColors.Secondary.copy(alpha = 0.3f), val dataStoreManager = remember { DataStoreManager(context) }
onSecondaryContainer = Color.White, var isDarkTheme by remember { mutableStateOf(true) }
background = AppColors.Background,
onBackground = AppColors.OnBackground, // Load theme preference
surface = AppColors.Surface, LaunchedEffect(Unit) {
onSurface = AppColors.OnSurface, dataStoreManager.themeFlow.collect { theme ->
surfaceVariant = AppColors.SurfaceVariant, isDarkTheme = theme == "dark"
onSurfaceVariant = AppColors.OnSurfaceVariant, AppColors.setTheme(isDarkTheme)
error = AppColors.Error, }
onError = Color.White, }
outline = AppColors.Border,
outlineVariant = AppColors.Divider // Create dynamic color scheme based on theme
), val colorScheme = if (isDarkTheme) {
typography = Typography( darkColorScheme(
displayLarge = MaterialTheme.typography.displayLarge.copy( primary = AppColors.Primary,
fontWeight = FontWeight.Bold onPrimary = Color.White,
), primaryContainer = AppColors.Primary.copy(alpha = 0.3f),
headlineLarge = MaterialTheme.typography.headlineLarge.copy( onPrimaryContainer = Color.White,
fontWeight = FontWeight.Bold secondary = AppColors.Secondary,
), onSecondary = Color.White,
titleLarge = MaterialTheme.typography.titleLarge.copy( secondaryContainer = AppColors.Secondary.copy(alpha = 0.3f),
fontWeight = FontWeight.SemiBold onSecondaryContainer = Color.White,
), background = AppColors.Background,
bodyLarge = MaterialTheme.typography.bodyLarge.copy( onBackground = AppColors.OnBackground,
lineHeight = 24.sp surface = AppColors.Surface,
), onSurface = AppColors.OnSurface,
bodyMedium = MaterialTheme.typography.bodyMedium.copy( surfaceVariant = AppColors.SurfaceVariant,
lineHeight = 20.sp onSurfaceVariant = AppColors.OnSurfaceVariant,
) error = AppColors.Error,
) onError = Color.White,
) { outline = AppColors.Border,
Surface( outlineVariant = AppColors.Divider
modifier = Modifier.fillMaxSize(), )
color = MaterialTheme.colorScheme.background } else {
) { lightColorScheme(
NotesApp() primary = AppColors.Primary,
} onPrimary = Color.White,
} primaryContainer = AppColors.Primary.copy(alpha = 0.1f),
onPrimaryContainer = AppColors.Primary,
secondary = AppColors.Secondary,
onSecondary = Color.White,
secondaryContainer = AppColors.Secondary.copy(alpha = 0.1f),
onSecondaryContainer = AppColors.Secondary,
background = AppColors.Background,
onBackground = AppColors.OnBackground,
surface = AppColors.Surface,
onSurface = AppColors.OnSurface,
surfaceVariant = AppColors.SurfaceVariant,
onSurfaceVariant = AppColors.OnSurfaceVariant,
error = AppColors.Error,
onError = Color.White,
outline = AppColors.Border,
outlineVariant = AppColors.Divider
)
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography(
displayLarge = MaterialTheme.typography.displayLarge.copy(
fontWeight = FontWeight.Bold
),
headlineLarge = MaterialTheme.typography.headlineLarge.copy(
fontWeight = FontWeight.Bold
),
titleLarge = MaterialTheme.typography.titleLarge.copy(
fontWeight = FontWeight.SemiBold
),
bodyLarge = MaterialTheme.typography.bodyLarge.copy(
lineHeight = 24.sp
),
bodyMedium = MaterialTheme.typography.bodyMedium.copy(
lineHeight = 20.sp
)
)
) {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
content()
} }
} }
} }
@ -529,6 +572,4 @@ fun NotesApp() {
) )
} }
} }
} }
private fun AppColors.setTheme(darkTheme: Boolean) {}

View File

@ -1,8 +1,9 @@
// File: data/model/ChatMessage.kt
package com.example.notesai.data.model package com.example.notesai.data.model
import kotlinx.serialization.Serializable
import java.util.UUID import java.util.UUID
@Serializable
data class ChatMessage( data class ChatMessage(
val id: String = UUID.randomUUID().toString(), val id: String = UUID.randomUUID().toString(),
val message: String, val message: String,

View File

@ -11,7 +11,7 @@ data class SerializableCategory(
val gradientEnd: Long, val gradientEnd: Long,
val timestamp: Long, val timestamp: Long,
val isDeleted: Boolean = false, val isDeleted: Boolean = false,
val isPinned: Boolean = false // NEW: Tambahkan ini val isPinned: Boolean = false
) )
@SuppressLint("UnsafeOptInUsageError") @SuppressLint("UnsafeOptInUsageError")

View File

@ -89,11 +89,11 @@ fun CategoryDialog(
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
// Color Grid // Color Grid - 2 rows of 4 colors
Constants.CategoryColors.chunked(4).forEach { row -> Constants.CategoryColors.chunked(4).forEach { row ->
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
row.forEachIndexed { _, gradient -> row.forEachIndexed { _, gradient ->
val globalIndex = Constants.CategoryColors.indexOf(gradient) val globalIndex = Constants.CategoryColors.indexOf(gradient)
@ -135,13 +135,13 @@ fun CategoryDialog(
Icons.Default.Check, Icons.Default.Check,
contentDescription = "Selected", contentDescription = "Selected",
tint = Color.White, tint = Color.White,
modifier = Modifier.size(24.dp) modifier = Modifier.size(28.dp)
) )
} }
} }
} }
} }
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(12.dp))
} }
} }
}, },

View File

@ -64,7 +64,7 @@ fun AIHelperScreen(
var showHistoryDrawer by remember { mutableStateOf(false) } var showHistoryDrawer by remember { mutableStateOf(false) }
var currentChatId by remember { mutableStateOf<String?>(null) } var currentChatId by remember { mutableStateOf<String?>(null) }
// NEW: File Upload States // File Upload States
var uploadedFile by remember { mutableStateOf<FileParseResult.Success?>(null) } var uploadedFile by remember { mutableStateOf<FileParseResult.Success?>(null) }
var isGeneratingSummary by remember { mutableStateOf(false) } var isGeneratingSummary by remember { mutableStateOf(false) }
@ -145,19 +145,21 @@ fun AIHelperScreen(
.fillMaxSize() .fillMaxSize()
.background(AppColors.Background) .background(AppColors.Background)
) { ) {
// Top Bar with History Button & Stats // UPDATED: Floating Top Bar
Surface( Box(
color = AppColors.Surface, modifier = Modifier
shadowElevation = 2.dp .fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp)
) { ) {
Column( Surface(
modifier = Modifier color = AppColors.SurfaceElevated,
.fillMaxWidth() shape = RoundedCornerShape(Constants.Radius.ExtraLarge.dp),
.padding(16.dp) shadowElevation = Constants.Elevation.Large.dp
) { ) {
// Top Row - Menu & New Chat
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@ -234,41 +236,9 @@ fun AIHelperScreen(
} }
} }
} }
Spacer(modifier = Modifier.height(12.dp))
// Stats - Compact
val filteredNotes = if (selectedCategory != null) {
notes.filter { it.categoryId == selectedCategory!!.id && !it.isArchived }
} else {
notes.filter { !it.isArchived }
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
CompactStatItem(
icon = Icons.Default.Description,
value = filteredNotes.size.toString(),
label = "Catatan"
)
CompactStatItem(
icon = Icons.Default.Star,
value = filteredNotes.count { it.isPinned }.toString(),
label = "Dipasang"
)
CompactStatItem(
icon = Icons.Default.Folder,
value = categories.size.toString(),
label = "Kategori"
)
}
} }
} }
HorizontalDivider(color = AppColors.Divider)
// Chat Area // Chat Area
Box( Box(
modifier = Modifier modifier = Modifier
@ -276,18 +246,20 @@ fun AIHelperScreen(
.fillMaxWidth() .fillMaxWidth()
) { ) {
if (chatMessages.isEmpty()) { if (chatMessages.isEmpty()) {
// Welcome State // Welcome State - Optimized Layout
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(32.dp) .padding(horizontal = 24.dp, vertical = 16.dp)
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
Spacer(modifier = Modifier.weight(0.5f))
Box( Box(
modifier = Modifier modifier = Modifier
.size(80.dp) .size(64.dp)
.background( .background(
color = AppColors.Primary.copy(alpha = 0.1f), color = AppColors.Primary.copy(alpha = 0.1f),
shape = CircleShape shape = CircleShape
@ -297,50 +269,49 @@ fun AIHelperScreen(
Icon( Icon(
Icons.Default.AutoAwesome, Icons.Default.AutoAwesome,
contentDescription = null, contentDescription = null,
modifier = Modifier.size(40.dp), modifier = Modifier.size(32.dp),
tint = AppColors.Primary tint = AppColors.Primary
) )
} }
Spacer(modifier = Modifier.height(24.dp))
Text( Text(
"AI Assistant", "AI Assistant",
style = MaterialTheme.typography.headlineMedium, style = MaterialTheme.typography.headlineMedium,
color = AppColors.OnBackground, color = AppColors.OnBackground,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold,
fontSize = 24.sp
)
Text(
"Tanyakan apa saja tentang catatan Anda",
style = MaterialTheme.typography.bodyMedium,
color = AppColors.OnSurfaceVariant,
textAlign = TextAlign.Center,
fontSize = 14.sp
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text( // Suggestion Chips - Lebih besar
"Tanyakan apa saja tentang catatan Anda",
style = MaterialTheme.typography.bodyLarge,
color = AppColors.OnSurfaceVariant,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(32.dp))
// Suggestion Chips
Column( Column(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier.fillMaxWidth(0.85f) modifier = Modifier.fillMaxWidth()
) { ) {
Text( Text(
"Contoh pertanyaan:", "Contoh pertanyaan:",
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
color = AppColors.OnSurfaceTertiary color = AppColors.OnSurfaceTertiary,
fontSize = 13.sp
) )
SuggestionChip("Analisis catatan saya") { prompt = it } SuggestionChip("Analisis catatan saya") { prompt = it }
SuggestionChip("Buat ringkasan") { prompt = it } SuggestionChip("Buat ringkasan") { prompt = it }
SuggestionChip("Berikan saran organisasi") { prompt = it } SuggestionChip("Berikan saran organisasi") { prompt = it }
} }
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(8.dp))
// NEW: File Upload Button // File Upload Button - PENTING: Jangan dihapus!
FileUploadButton( FileUploadButton(
onFileSelected = { fileResult -> onFileSelected = { fileResult ->
uploadedFile = fileResult uploadedFile = fileResult
@ -401,14 +372,16 @@ fun AIHelperScreen(
}, },
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Spacer(modifier = Modifier.weight(0.5f))
} }
} else { } else {
// Chat Messages // Chat Messages - UPDATED: Bottom padding dari 100dp ke 120dp
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.verticalScroll(scrollState) .verticalScroll(scrollState)
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 100.dp) .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 120.dp)
) { ) {
chatMessages.forEach { message -> chatMessages.forEach { message ->
ChatBubble( ChatBubble(
@ -427,7 +400,7 @@ fun AIHelperScreen(
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
} }
// Loading Indicator (untuk chat biasa DAN file summary) // Loading Indicator
if (isLoading || isGeneratingSummary) { if (isLoading || isGeneratingSummary) {
Row( Row(
modifier = Modifier modifier = Modifier
@ -489,7 +462,7 @@ fun AIHelperScreen(
} }
} }
// Input Area - Minimalist // Input Area
Surface( Surface(
color = AppColors.Surface, color = AppColors.Surface,
shadowElevation = 8.dp, shadowElevation = 8.dp,
@ -501,7 +474,7 @@ fun AIHelperScreen(
.padding(16.dp), .padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
// NEW: Upload File Button (di atas input text) // Upload File Button (di atas input text)
if (chatMessages.isNotEmpty() && !isGeneratingSummary && !isLoading) { if (chatMessages.isNotEmpty() && !isGeneratingSummary && !isLoading) {
FileUploadButton( FileUploadButton(
onFileSelected = { fileResult -> onFileSelected = { fileResult ->
@ -533,7 +506,6 @@ fun AIHelperScreen(
isUser = true isUser = true
) )
// Scroll to show loading
delay(100) delay(100)
scrollState.animateScrollTo(scrollState.maxValue) scrollState.animateScrollTo(scrollState.maxValue)

View File

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

View File

@ -1,35 +0,0 @@
package com.example.notesai.presentation.screens.ai.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@Composable
fun StatItem(label: String, value: String, color: Color) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(8.dp)
) {
Text(
value,
style = MaterialTheme.typography.headlineMedium,
color = color,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(4.dp))
Text(
label,
style = MaterialTheme.typography.bodySmall,
color = Color(0xFF94A3B8)
)
}
}

View File

@ -32,7 +32,7 @@ fun CategoryCard(
onClick: () -> Unit, onClick: () -> Unit,
onDelete: () -> Unit = {}, onDelete: () -> Unit = {},
onEdit: (String, Long, Long) -> Unit = { _, _, _ -> }, onEdit: (String, Long, Long) -> Unit = { _, _, _ -> },
onPin: () -> Unit = {} // NEW: Pin callback onPin: () -> Unit = {}
) { ) {
var showDeleteConfirm by remember { mutableStateOf(false) } var showDeleteConfirm by remember { mutableStateOf(false) }
var showEditDialog by remember { mutableStateOf(false) } var showEditDialog by remember { mutableStateOf(false) }
@ -160,7 +160,7 @@ fun CategoryCard(
) )
} }
// NEW: Pin Indicator & Menu Button // Pin Indicator & Menu Button
Row( Row(
horizontalArrangement = Arrangement.spacedBy(4.dp), horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
@ -198,7 +198,7 @@ fun CategoryCard(
onDismissRequest = { showMenu = false }, onDismissRequest = { showMenu = false },
modifier = Modifier.background(AppColors.SurfaceElevated) modifier = Modifier.background(AppColors.SurfaceElevated)
) { ) {
// NEW: Pin/Unpin Menu Item // Pin/Unpin Menu Item
DropdownMenuItem( DropdownMenuItem(
text = { text = {
Row( Row(
@ -382,10 +382,11 @@ fun EditCategoryDialog(
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
// 8 colors in 2 rows
Constants.CategoryColors.chunked(4).forEach { row -> Constants.CategoryColors.chunked(4).forEach { row ->
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
row.forEachIndexed { _, gradient -> row.forEachIndexed { _, gradient ->
val globalIndex = Constants.CategoryColors.indexOf(gradient) val globalIndex = Constants.CategoryColors.indexOf(gradient)
@ -416,13 +417,13 @@ fun EditCategoryDialog(
Icons.Default.Check, Icons.Default.Check,
contentDescription = null, contentDescription = null,
tint = Color.White, tint = Color.White,
modifier = Modifier.size(24.dp) modifier = Modifier.size(28.dp)
) )
} }
} }
} }
} }
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(12.dp))
} }
} }
}, },

View File

@ -20,7 +20,7 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor // ✅ ADD import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.* import androidx.compose.ui.platform.*
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
@ -57,7 +57,6 @@ fun EditableFullScreenNoteView(
) )
} }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val bringIntoViewRequester = remember { BringIntoViewRequester() } val bringIntoViewRequester = remember { BringIntoViewRequester() }
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
@ -78,7 +77,7 @@ fun EditableFullScreenNoteView(
} }
} }
// 🔥 AUTO SAVE SAAT APP BACKGROUND / KELUAR // AUTO SAVE SAAT APP BACKGROUND / KELUAR
val lifecycleOwner = LocalLifecycleOwner.current val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) { DisposableEffect(lifecycleOwner) {
@ -95,7 +94,6 @@ fun EditableFullScreenNoteView(
} }
} }
val dateFormat = remember { val dateFormat = remember {
SimpleDateFormat("dd MMMM yyyy, HH:mm", Locale("id", "ID")) SimpleDateFormat("dd MMMM yyyy, HH:mm", Locale("id", "ID"))
} }
@ -177,21 +175,32 @@ fun EditableFullScreenNoteView(
) )
) { ) {
TextField( // FIXED: BasicTextField untuk judul agar sejajar dengan konten
BasicTextField(
value = title, value = title,
onValueChange = { title = it }, onValueChange = { title = it },
textStyle = MaterialTheme.typography.headlineLarge.copy( textStyle = MaterialTheme.typography.headlineLarge.copy(
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onBackground color = MaterialTheme.colorScheme.onBackground
), ),
placeholder = { Text("Judul") }, cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
modifier = Modifier.fillMaxWidth(), modifier = Modifier
colors = TextFieldDefaults.colors( .fillMaxWidth()
focusedContainerColor = Color.Transparent, .padding(vertical = 8.dp),
unfocusedContainerColor = Color.Transparent, decorationBox = { innerTextField ->
focusedIndicatorColor = Color.Transparent, Box {
unfocusedIndicatorColor = Color.Transparent if (title.isEmpty()) {
) Text(
"Judul",
style = MaterialTheme.typography.headlineLarge.copy(
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.4f)
)
)
}
innerTextField()
}
}
) )
Spacer(Modifier.height(12.dp)) Spacer(Modifier.height(12.dp))
@ -204,7 +213,7 @@ fun EditableFullScreenNoteView(
HorizontalDivider(Modifier.padding(vertical = 20.dp)) HorizontalDivider(Modifier.padding(vertical = 20.dp))
// ✅ FIX UTAMA: set cursorBrush agar insertion cursor muncul // Konten editor
BasicTextField( BasicTextField(
value = editorState.value, value = editorState.value,
onValueChange = { onValueChange = {
@ -213,7 +222,7 @@ fun EditableFullScreenNoteView(
bringIntoViewRequester.bringIntoView() bringIntoViewRequester.bringIntoView()
} }
}, },
cursorBrush = SolidColor(Color(0xFFA855F7)), // ✅ ADD THIS cursorBrush = SolidColor(Color(0xFFA855F7)),
textStyle = MaterialTheme.typography.bodyLarge.copy( textStyle = MaterialTheme.typography.bodyLarge.copy(
color = MaterialTheme.colorScheme.onBackground, color = MaterialTheme.colorScheme.onBackground,
lineHeight = 28.sp lineHeight = 28.sp
@ -285,9 +294,9 @@ fun EditableFullScreenNoteView(
editorState.toggleItalic() editorState.toggleItalic()
}, },
onUnderline = { ensureFocus(); editorState.toggleUnderline() }, onUnderline = { ensureFocus(); editorState.toggleUnderline() },
onHeading = { ensureFocus(); editorState.setHeading(1) }, // sementara H1 onHeading = { ensureFocus(); editorState.setHeading(1) },
onBullet = { ensureFocus(); editorState.toggleBulletList() } onBullet = { ensureFocus(); editorState.toggleBulletList() }
) )
} }
} }
} }

View File

@ -1,3 +0,0 @@
import com.example.notesai.util.AppColors
annotation class AppColors

View File

@ -1,34 +1,77 @@
package com.example.notesai.util package com.example.notesai.util
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
object AppColors { object AppColors {
// Theme state
private var isDarkTheme by mutableStateOf(true)
// Primary Colors // Primary Colors
val Primary = Color(0xFF6C63FF) val Primary: Color
val Secondary = Color(0xFF03DAC6) get() = if (isDarkTheme) Color(0xFF6C63FF) else Color(0xFF5A52D5)
val Accent = Color(0xFFFF6B9D)
val Secondary: Color
get() = if (isDarkTheme) Color(0xFF03DAC6) else Color(0xFF018786)
val Accent: Color
get() = if (isDarkTheme) Color(0xFFFF6B9D) else Color(0xFFE91E63)
// Background Colors // Background Colors
val Background = Color(0xFF121212) val Background: Color
val Surface = Color(0xFF1E1E1E) get() = if (isDarkTheme) Color(0xFF121212) else Color(0xFFFAFAFA)
val SurfaceVariant = Color(0xFF2A2A2A)
val SurfaceElevated = Color(0xFF252525) val Surface: Color
get() = if (isDarkTheme) Color(0xFF1E1E1E) else Color(0xFFFFFFFF)
val SurfaceVariant: Color
get() = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5)
val SurfaceElevated: Color
get() = if (isDarkTheme) Color(0xFF252525) else Color(0xFFFFFFFF)
// Text Colors // Text Colors
val OnBackground = Color(0xFFE1E1E1) val OnBackground: Color
val OnSurface = Color(0xFFCCCCCC) get() = if (isDarkTheme) Color(0xFFE1E1E1) else Color(0xFF1C1B1F)
val OnSurfaceVariant = Color(0xFF9E9E9E)
val OnSurfaceTertiary = Color(0xFF757575) val OnSurface: Color
get() = if (isDarkTheme) Color(0xFFCCCCCC) else Color(0xFF1C1B1F)
val OnSurfaceVariant: Color
get() = if (isDarkTheme) Color(0xFF9E9E9E) else Color(0xFF49454F)
val OnSurfaceTertiary: Color
get() = if (isDarkTheme) Color(0xFF757575) else Color(0xFF79747E)
// Utility Colors // Utility Colors
val Error = Color(0xFFCF6679) val Error: Color
val Warning = Color(0xFFFFB74D) get() = if (isDarkTheme) Color(0xFFCF6679) else Color(0xFFB3261E)
val Success = Color(0xFF81C784)
val Info = Color(0xFF64B5F6) val Warning: Color
get() = if (isDarkTheme) Color(0xFFFFB74D) else Color(0xFFF57C00)
val Success: Color
get() = if (isDarkTheme) Color(0xFF81C784) else Color(0xFF388E3C)
val Info: Color
get() = if (isDarkTheme) Color(0xFF64B5F6) else Color(0xFF1976D2)
// Border & Divider // Border & Divider
val Border = Color(0xFF3A3A3A) val Border: Color
val Divider = Color(0xFF2E2E2E) get() = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE0E0E0)
val Divider: Color
get() = if (isDarkTheme) Color(0xFF2E2E2E) else Color(0xFFE0E0E0)
// Function to update theme
fun setTheme(darkTheme: Boolean) {
isDarkTheme = darkTheme
}
// Get current theme state
fun isDark(): Boolean = isDarkTheme
} }
object Constants { object Constants {
@ -69,54 +112,30 @@ object Constants {
// Reference to AppColors for compatibility // Reference to AppColors for compatibility
val AppColors = com.example.notesai.util.AppColors val AppColors = com.example.notesai.util.AppColors
// Category gradient colors // Category gradient colors - 8 pilihan warna
val CategoryColors = listOf( val CategoryColors = listOf(
// Purple gradients // Purple gradient
0xFF6750A4L to 0xFF7E57C2L, 0xFF6750A4L to 0xFF7E57C2L,
// Pink gradient
0xFF9C27B0L to 0xFFE91E63L, 0xFF9C27B0L to 0xFFE91E63L,
// Blue gradients // Blue gradient
0xFF2196F3L to 0xFF03A9F4L, 0xFF2196F3L to 0xFF03A9F4L,
0xFF1976D2L to 0xFF4FC3F7L,
// Green gradients // Green gradient
0xFF4CAF50L to 0xFF8BC34AL, 0xFF4CAF50L to 0xFF8BC34AL,
0xFF009688L to 0xFF00BCD4L,
// Orange gradients // Orange gradient
0xFFFF9800L to 0xFFFFB74DL, 0xFFFF9800L to 0xFFFFB74DL,
0xFFFF5722L to 0xFFFF7043L,
// Red gradients // Red gradient
0xFFF44336L to 0xFFE91E63L, 0xFFF44336L to 0xFFE91E63L,
0xFFD32F2FL to 0xFFFF5252L,
// Teal gradients // Teal gradient
0xFF009688L to 0xFF26A69AL, 0xFF009688L to 0xFF26A69AL,
0xFF00897BL to 0xFF4DB6ACL,
// Indigo gradients // Indigo gradient
0xFF3F51B5L to 0xFF5C6BC0L, 0xFF3F51B5L to 0xFF5C6BC0L
0xFF303F9FL to 0xFF7986CBL,
// Amber gradients
0xFFFFC107L to 0xFFFFD54FL,
0xFFFFB300L to 0xFFFFCA28L,
// Pink gradients
0xFFE91E63L to 0xFFF06292L,
0xFFC2185BL to 0xFFEC407AL,
// Cyan gradients
0xFF00BCD4L to 0xFF26C6DAL,
0xFF0097A7L to 0xFF00ACC1L,
// Deep Purple gradients
0xFF673AB7L to 0xFF9575CDL,
0xFF512DA8L to 0xFF7E57C2L,
// Lime gradients
0xFFCDDC39L to 0xFFD4E157L,
0xFFAFB42BL to 0xFFC0CA33L
) )
} }