Menambahkan fitur pin dikategori dan catatan

This commit is contained in:
202310715082 FAZRI ABDURRAHMAN 2025-12-23 00:16:03 +07:00
parent 3692a291c7
commit 85a2c65017
12 changed files with 648 additions and 553 deletions

View File

@ -4,10 +4,10 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-12-18T02:27:11.898714800Z"> <DropdownSelection timestamp="2025-12-18T06:53:17.556062600Z">
<Target type="DEFAULT_BOOT"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\Fazri Abdurrahman\.android\avd\Medium_Tablet.avd" /> <DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\Fazri Abdurrahman\.android\avd\Medium_Phone.avd" />
</handle> </handle>
</Target> </Target>
</DropdownSelection> </DropdownSelection>

View File

@ -16,8 +16,6 @@ android {
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {
useSupportLibrary = true useSupportLibrary = true
@ -33,65 +31,20 @@ android {
) )
} }
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8
} }
kotlinOptions { kotlinOptions {
jvmTarget = "1.8" jvmTarget = "1.8"
} }
buildFeatures { buildFeatures {
compose = true compose = true
} }
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
}
dependencies {
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
implementation("androidx.activity:activity-compose:1.8.2")
implementation(platform("androidx.compose:compose-bom:2024.02.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.material:material-icons-extended-android:1.6.7")
implementation("com.google.android.material:material:1.9.0")
implementation("androidx.datastore:datastore-preferences:1.0.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.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)
implementation(libs.androidx.glance)
implementation(libs.androidx.animation)
implementation(libs.androidx.ui.graphics)
// Untuk integrasi Gemini AI (optional - uncomment jika sudah ada API key)
// implementation("com.google.ai.client.generativeai:generativeai:0.1.2")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation(platform("androidx.compose:compose-bom:2024.02.00"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
// File picker
implementation("androidx.activity:activity-compose:1.8.2")
// PDF Parser (ONLY THIS ONE!)
implementation("com.tom-roush:pdfbox-android:2.0.27.0")
// File operations
implementation("androidx.documentfile:documentfile:1.0.1")
}
android {
packaging { packaging {
resources { resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}" excludes += "/META-INF/{AL2.0,LGPL2.1}"
@ -107,3 +60,58 @@ android {
} }
} }
} }
dependencies {
// Core Android
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
implementation("androidx.activity:activity-compose:1.8.2")
// Compose BOM
implementation(platform("androidx.compose:compose-bom:2024.02.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.material:material-icons-extended-android:1.6.7")
// Material Design
implementation("com.google.android.material:material:1.9.0")
// DataStore
implementation("androidx.datastore:datastore-preferences:1.0.0")
// Serialization
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
// Gemini AI
implementation("com.google.ai.client.generativeai:generativeai:0.9.0")
// Version Catalog (libs)
implementation(libs.androidx.ui.text)
implementation(libs.androidx.material3)
implementation(libs.androidx.animation.core)
implementation(libs.androidx.glance)
implementation(libs.androidx.animation)
implementation(libs.androidx.ui.graphics)
// File operations
implementation("androidx.documentfile:documentfile:1.0.1")
// PDF Parser
implementation("com.tom-roush:pdfbox-android:2.0.27.0")
// FIREBASE SUDAH DIHAPUS - baris ini yang menyebabkan error
// implementation(libs.firebase.firestore.ktx)
// Testing
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation(platform("androidx.compose:compose-bom:2024.02.00"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
// Debug
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
}

View File

@ -44,11 +44,11 @@ class MainActivity : ComponentActivity() {
colorScheme = darkColorScheme( colorScheme = darkColorScheme(
primary = AppColors.Primary, primary = AppColors.Primary,
onPrimary = Color.White, onPrimary = Color.White,
primaryContainer = AppColors.PrimaryContainer, primaryContainer = AppColors.Primary.copy(alpha = 0.3f),
onPrimaryContainer = Color.White, onPrimaryContainer = Color.White,
secondary = AppColors.Secondary, secondary = AppColors.Secondary,
onSecondary = Color.White, onSecondary = Color.White,
secondaryContainer = AppColors.SecondaryVariant, secondaryContainer = AppColors.Secondary.copy(alpha = 0.3f),
onSecondaryContainer = Color.White, onSecondaryContainer = Color.White,
background = AppColors.Background, background = AppColors.Background,
onBackground = AppColors.OnBackground, onBackground = AppColors.OnBackground,
@ -90,15 +90,6 @@ class MainActivity : ComponentActivity() {
} }
} }
fun sortCategories(categories: List<Category>): List<Category> {
return categories
.filter { !it.isDeleted }
.sortedWith(
compareByDescending<Category> { it.isPinned } // Pinned dulu
.thenByDescending { it.timestamp } // Lalu timestamp
)
}
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun NotesApp() { fun NotesApp() {
@ -106,6 +97,7 @@ fun NotesApp() {
val dataStoreManager = remember { DataStoreManager(context) } val dataStoreManager = remember { DataStoreManager(context) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current
var categories by remember { mutableStateOf(listOf<Category>()) } var categories by remember { mutableStateOf(listOf<Category>()) }
var notes by remember { mutableStateOf(listOf<Note>()) } var notes by remember { mutableStateOf(listOf<Note>()) }
var selectedCategory by remember { mutableStateOf<Category?>(null) } var selectedCategory by remember { mutableStateOf<Category?>(null) }
@ -120,10 +112,17 @@ fun NotesApp() {
var fullScreenNote by remember { mutableStateOf<Note?>(null) } var fullScreenNote by remember { mutableStateOf<Note?>(null) }
var isDarkTheme by remember { mutableStateOf(true) } var isDarkTheme by remember { mutableStateOf(true) }
// Guard flags to prevent race conditions
var isDataLoaded by remember { mutableStateOf(false) } var isDataLoaded by remember { mutableStateOf(false) }
// Load theme preference fun sortCategories(categories: List<Category>): List<Category> {
return categories
.filter { !it.isDeleted }
.sortedWith(
compareByDescending<Category> { it.isPinned }
.thenByDescending { it.timestamp }
)
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
dataStoreManager.themeFlow.collect { theme -> dataStoreManager.themeFlow.collect { theme ->
isDarkTheme = theme == "dark" isDarkTheme = theme == "dark"
@ -131,7 +130,6 @@ fun NotesApp() {
} }
} }
// Load categories ONCE
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
dataStoreManager.categoriesFlow.collect { loadedCategories -> dataStoreManager.categoriesFlow.collect { loadedCategories ->
if (!isDataLoaded) { if (!isDataLoaded) {
@ -141,18 +139,16 @@ fun NotesApp() {
} }
} }
// Load notes ONCE
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
dataStoreManager.notesFlow.collect { loadedNotes -> dataStoreManager.notesFlow.collect { loadedNotes ->
if (!isDataLoaded) { if (!isDataLoaded) {
android.util.Log.d("NotesApp", "Loading ${loadedNotes.size} notes") android.util.Log.d("NotesApp", "Loading ${loadedNotes.size} notes")
notes = loadedNotes notes = loadedNotes
isDataLoaded = true // Mark as loaded isDataLoaded = true
} }
} }
} }
// Save categories when changed
LaunchedEffect(categories) { LaunchedEffect(categories) {
if (isDataLoaded && categories.isNotEmpty()) { if (isDataLoaded && categories.isNotEmpty()) {
android.util.Log.d("NotesApp", "Saving ${categories.size} categories") android.util.Log.d("NotesApp", "Saving ${categories.size} categories")
@ -162,7 +158,6 @@ fun NotesApp() {
} }
} }
// Save notes when changed
LaunchedEffect(notes) { LaunchedEffect(notes) {
if (isDataLoaded && notes.isNotEmpty()) { if (isDataLoaded && notes.isNotEmpty()) {
android.util.Log.d("NotesApp", "Saving ${notes.size} notes") android.util.Log.d("NotesApp", "Saving ${notes.size} notes")
@ -172,7 +167,6 @@ fun NotesApp() {
} }
} }
// Save on lifecycle events
DisposableEffect(lifecycleOwner) { DisposableEffect(lifecycleOwner) {
val observer = androidx.lifecycle.LifecycleEventObserver { _, event -> val observer = androidx.lifecycle.LifecycleEventObserver { _, event ->
if (event == androidx.lifecycle.Lifecycle.Event.ON_PAUSE || if (event == androidx.lifecycle.Lifecycle.Event.ON_PAUSE ||
@ -319,7 +313,7 @@ fun NotesApp() {
) { ) {
when (currentScreen) { when (currentScreen) {
"main" -> MainScreen( "main" -> MainScreen(
categories = categories.filter { !it.isDeleted }, categories = sortCategories(categories),
notes = notes, notes = notes,
selectedCategory = selectedCategory, selectedCategory = selectedCategory,
searchQuery = searchQuery, searchQuery = searchQuery,
@ -359,6 +353,12 @@ fun NotesApp() {
} }
} }
}, },
onCategoryPin = { category ->
categories = categories.map {
if (it.id == category.id) it.copy(isPinned = !it.isPinned)
else it
}
},
onNoteEdit = { note -> onNoteEdit = { note ->
editingNote = note editingNote = note
showNoteDialog = true showNoteDialog = true
@ -439,7 +439,6 @@ fun NotesApp() {
} }
} }
// Dialogs
if (showCategoryDialog) { if (showCategoryDialog) {
CategoryDialog( CategoryDialog(
onDismiss = { showCategoryDialog = false }, onDismiss = { showCategoryDialog = false },
@ -457,6 +456,7 @@ fun NotesApp() {
if (showNoteDialog && selectedCategory != null) { if (showNoteDialog && selectedCategory != null) {
NoteDialog( NoteDialog(
note = editingNote, note = editingNote,
categoryId = selectedCategory!!.id,
onDismiss = { onDismiss = {
showNoteDialog = false showNoteDialog = false
editingNote = null editingNote = null
@ -498,7 +498,6 @@ fun NotesApp() {
} }
} }
// Drawer with Animation
AnimatedVisibility( AnimatedVisibility(
visible = drawerState, visible = drawerState,
enter = fadeIn() + slideInHorizontally( enter = fadeIn() + slideInHorizontally(
@ -531,3 +530,5 @@ fun NotesApp() {
} }
} }
} }
private fun AppColors.setTheme(darkTheme: Boolean) {}

View File

@ -39,7 +39,7 @@ fun DrawerMenu(
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(AppColors.Overlay) .background(Color.Black.copy(alpha = 0.5f))
.clickable( .clickable(
onClick = onDismiss, onClick = onDismiss,
indication = null, indication = null,
@ -99,7 +99,7 @@ fun DrawerMenu(
Spacer(modifier = Modifier.height(Constants.Spacing.Medium.dp)) Spacer(modifier = Modifier.height(Constants.Spacing.Medium.dp))
Text( Text(
Constants.APP_NAME, "AI Notes",
style = MaterialTheme.typography.headlineMedium, style = MaterialTheme.typography.headlineMedium,
color = AppColors.OnBackground, color = AppColors.OnBackground,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
@ -179,7 +179,7 @@ fun DrawerMenu(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Text(
"Version ${Constants.APP_VERSION}", "Version 1.0.0",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = AppColors.OnSurfaceTertiary, color = AppColors.OnSurfaceTertiary,
fontSize = 12.sp fontSize = 12.sp

View File

@ -1,42 +1,41 @@
// File: presentation/dialogs/CategoryDialog.kt package com.example.notesai.presentation.dialogs
package com.example.notesai.presentation.dialogs
import androidx.compose.animation.* import androidx.compose.animation.*
import androidx.compose.animation.core.* 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.Check import androidx.compose.material.icons.filled.Check
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.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 androidx.compose.ui.unit.sp
import com.example.notesai.util.AppColors import com.example.notesai.util.AppColors
import com.example.notesai.util.Constants import com.example.notesai.util.Constants
@Composable @Composable
fun CategoryDialog( fun CategoryDialog(
onDismiss: () -> Unit, onDismiss: () -> Unit,
onSave: (String, Long, Long) -> Unit onSave: (String, Long, Long) -> Unit
) { ) {
var name by remember { mutableStateOf("") } var name by remember { mutableStateOf("") }
var selectedGradient by remember { mutableStateOf(0) } var selectedGradient by remember { mutableStateOf(0) }
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
containerColor = AppColors.Surface, containerColor = AppColors.Surface,
shape = RoundedCornerShape(20.dp), shape = RoundedCornerShape(Constants.Radius.Large.dp),
title = { title = {
Text( Text(
"Buat Kategori Baru", "Kategori Baru",
color = AppColors.OnBackground, color = AppColors.OnBackground,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontSize = 20.sp fontSize = 20.sp
@ -44,9 +43,9 @@
}, },
text = { text = {
Column( Column(
verticalArrangement = Arrangement.spacedBy(20.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
// Input Nama // Name Input
OutlinedTextField( OutlinedTextField(
value = name, value = name,
onValueChange = { name = it }, onValueChange = { name = it },
@ -58,7 +57,7 @@
}, },
placeholder = { placeholder = {
Text( Text(
"Contoh: Pekerjaan, Personal", "Contoh: Pekerjaan, Pribadi...",
color = AppColors.OnSurfaceTertiary, color = AppColors.OnSurfaceTertiary,
fontSize = 14.sp fontSize = 14.sp
) )
@ -73,14 +72,13 @@
focusedBorderColor = AppColors.Primary, focusedBorderColor = AppColors.Primary,
unfocusedBorderColor = Color.Transparent unfocusedBorderColor = Color.Transparent
), ),
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(Constants.Radius.Medium.dp),
singleLine = true singleLine = true
) )
// Gradient Selector Spacer(modifier = Modifier.height(8.dp))
Column(
verticalArrangement = Arrangement.spacedBy(12.dp) // Color picker title
) {
Text( Text(
"Pilih Warna:", "Pilih Warna:",
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
@ -89,30 +87,23 @@
fontSize = 14.sp fontSize = 14.sp
) )
Spacer(modifier = Modifier.height(8.dp))
// Color Grid
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(8.dp)
) { ) {
row.forEach { gradient -> row.forEachIndexed { _, gradient ->
val globalIndex = Constants.CategoryColors.indexOf(gradient) val globalIndex = Constants.CategoryColors.indexOf(gradient)
val isSelected = selectedGradient == globalIndex val isSelected = selectedGradient == globalIndex
// Scale animation
val scale by animateFloatAsState(
targetValue = if (isSelected) 1.1f else 1f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessMedium
),
label = "scale"
)
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(
@ -124,34 +115,54 @@
.clickable { selectedGradient = globalIndex }, .clickable { selectedGradient = globalIndex },
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
// Check icon dengan animation // Checkmark with animation
this@Row.AnimatedVisibility( androidx.compose.animation.AnimatedVisibility(
visible = isSelected, visible = isSelected,
enter = scaleIn() + fadeIn(), enter = scaleIn(
exit = scaleOut() + fadeOut() animationSpec = spring(
) { dampingRatio = Spring.DampingRatioMediumBouncy,
Surface( stiffness = Spring.StiffnessMedium
color = Color.White.copy(alpha = 0.9f), )
shape = RoundedCornerShape(8.dp) ) + fadeIn(),
exit = scaleOut(
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessMedium
)
) + fadeOut()
) { ) {
Icon( Icon(
Icons.Default.Check, Icons.Default.Check,
contentDescription = null, contentDescription = "Selected",
tint = Color(gradient.first), tint = Color.White,
modifier = Modifier modifier = Modifier.size(24.dp)
.padding(6.dp)
.size(20.dp)
) )
} }
} }
} }
} }
} Spacer(modifier = Modifier.height(8.dp))
}
} }
} }
}, },
confirmButton = { confirmButton = {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
// Cancel button
TextButton(
onClick = onDismiss,
shape = RoundedCornerShape(Constants.Radius.Medium.dp),
modifier = Modifier.height(48.dp)
) {
Text(
"Batal",
color = AppColors.OnSurfaceVariant,
fontSize = 15.sp
)
}
// Save button
Button( Button(
onClick = { onClick = {
if (name.isNotBlank()) { if (name.isNotBlank()) {
@ -164,29 +175,17 @@
containerColor = AppColors.Primary, containerColor = AppColors.Primary,
disabledContainerColor = AppColors.Primary.copy(alpha = 0.5f) disabledContainerColor = AppColors.Primary.copy(alpha = 0.5f)
), ),
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(Constants.Radius.Medium.dp),
modifier = Modifier.height(48.dp) modifier = Modifier.height(48.dp)
) { ) {
Text( Text(
"Simpan", "Buat",
color = Color.White, color = Color.White,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontSize = 15.sp fontSize = 15.sp
) )
} }
},
dismissButton = {
TextButton(
onClick = onDismiss,
shape = RoundedCornerShape(12.dp),
modifier = Modifier.height(48.dp)
) {
Text(
"Batal",
color = AppColors.OnSurfaceVariant,
fontSize = 15.sp
)
} }
} }
) )
} }

View File

@ -17,10 +17,11 @@ import com.example.notesai.util.Constants
@Composable @Composable
fun NoteDialog( fun NoteDialog(
note: Note?, categoryId: String, // Parameter untuk kategori ID
note: Note? = null, // Null jika buat baru, isi jika edit
onDismiss: () -> Unit, onDismiss: () -> Unit,
onSave: (String, String) -> Unit, onSave: (String, String) -> Unit,
onDelete: (() -> Unit)? onDelete: (() -> Unit)? = null
) { ) {
var title by remember { mutableStateOf(note?.title ?: "") } var title by remember { mutableStateOf(note?.title ?: "") }
var description by remember { mutableStateOf(note?.description ?: "") } var description by remember { mutableStateOf(note?.description ?: "") }
@ -31,7 +32,7 @@ fun NoteDialog(
AlertDialog( AlertDialog(
onDismissRequest = { showDeleteConfirm = false }, onDismissRequest = { showDeleteConfirm = false },
containerColor = AppColors.Surface, containerColor = AppColors.Surface,
shape = RoundedCornerShape(20.dp), shape = RoundedCornerShape(Constants.Radius.Large.dp),
title = { title = {
Text( Text(
"Hapus Catatan?", "Hapus Catatan?",
@ -54,7 +55,7 @@ fun NoteDialog(
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = AppColors.Error containerColor = AppColors.Error
), ),
shape = RoundedCornerShape(12.dp) shape = RoundedCornerShape(Constants.Radius.Medium.dp)
) { ) {
Text("Hapus", color = Color.White, fontWeight = FontWeight.Bold) Text("Hapus", color = Color.White, fontWeight = FontWeight.Bold)
} }
@ -62,7 +63,7 @@ fun NoteDialog(
dismissButton = { dismissButton = {
TextButton( TextButton(
onClick = { showDeleteConfirm = false }, onClick = { showDeleteConfirm = false },
shape = RoundedCornerShape(12.dp) shape = RoundedCornerShape(Constants.Radius.Medium.dp)
) { ) {
Text("Batal", color = AppColors.OnSurfaceVariant) Text("Batal", color = AppColors.OnSurfaceVariant)
} }
@ -73,7 +74,7 @@ fun NoteDialog(
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
containerColor = AppColors.Surface, containerColor = AppColors.Surface,
shape = RoundedCornerShape(20.dp), shape = RoundedCornerShape(Constants.Radius.Large.dp),
title = { title = {
Text( Text(
if (note == null) "Catatan Baru" else "Edit Catatan", if (note == null) "Catatan Baru" else "Edit Catatan",
@ -113,7 +114,7 @@ fun NoteDialog(
focusedBorderColor = AppColors.Primary, focusedBorderColor = AppColors.Primary,
unfocusedBorderColor = Color.Transparent unfocusedBorderColor = Color.Transparent
), ),
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(Constants.Radius.Medium.dp),
singleLine = true singleLine = true
) )
@ -146,7 +147,7 @@ fun NoteDialog(
focusedBorderColor = AppColors.Primary, focusedBorderColor = AppColors.Primary,
unfocusedBorderColor = Color.Transparent unfocusedBorderColor = Color.Transparent
), ),
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(Constants.Radius.Medium.dp),
maxLines = 8 maxLines = 8
) )
} }
@ -174,7 +175,7 @@ fun NoteDialog(
// Cancel button // Cancel button
TextButton( TextButton(
onClick = onDismiss, onClick = onDismiss,
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(Constants.Radius.Medium.dp),
modifier = Modifier.height(48.dp) modifier = Modifier.height(48.dp)
) { ) {
Text( Text(
@ -196,7 +197,7 @@ fun NoteDialog(
containerColor = AppColors.Primary, containerColor = AppColors.Primary,
disabledContainerColor = AppColors.Primary.copy(alpha = 0.5f) disabledContainerColor = AppColors.Primary.copy(alpha = 0.5f)
), ),
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(Constants.Radius.Medium.dp),
modifier = Modifier.height(48.dp) modifier = Modifier.height(48.dp)
) { ) {
Text( Text(

View File

@ -72,7 +72,7 @@ fun ChatHistoryDrawer(
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(AppColors.Overlay) .background(Color.Black.copy(alpha = 0.5f))
.clickable( .clickable(
onClick = onDismiss, onClick = onDismiss,
indication = null, indication = null,
@ -883,6 +883,18 @@ private fun EmptyHistoryState(
Spacer(modifier = Modifier.height(6.dp)) Spacer(modifier = Modifier.height(6.dp))
Text(
if (searchQuery.isNotEmpty())
"Coba kata kunci lain"
else
"Mulai chat dengan AI",
style = MaterialTheme.typography.bodySmall,
color = AppColors.OnSurfaceTertiary,
fontSize = 16.sp
)
Spacer(modifier = Modifier.height(6.dp))
Text( Text(
if (searchQuery.isNotEmpty()) if (searchQuery.isNotEmpty())
"Coba kata kunci lain" "Coba kata kunci lain"

View File

@ -28,6 +28,7 @@ fun MainScreen(
onPinToggle: (Note) -> Unit, onPinToggle: (Note) -> Unit,
onCategoryDelete: (Category) -> Unit, onCategoryDelete: (Category) -> Unit,
onCategoryEdit: (Category, String, Long, Long) -> Unit, onCategoryEdit: (Category, String, Long, Long) -> Unit,
onCategoryPin: (Category) -> Unit, // NEW: Pin category callback
onNoteEdit: (Note) -> Unit = {}, onNoteEdit: (Note) -> Unit = {},
onNoteDelete: (Note) -> Unit = {} onNoteDelete: (Note) -> Unit = {}
) { ) {
@ -63,7 +64,7 @@ fun MainScreen(
start = 16.dp, start = 16.dp,
end = 16.dp, end = 16.dp,
top = 16.dp, top = 16.dp,
bottom = 100.dp // Extra space untuk floating bottom bar bottom = 100.dp
), ),
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalItemSpacing = 12.dp, verticalItemSpacing = 12.dp,
@ -81,7 +82,8 @@ fun MainScreen(
onDelete = { onCategoryDelete(category) }, onDelete = { onCategoryDelete(category) },
onEdit = { name, gradientStart, gradientEnd -> onEdit = { name, gradientStart, gradientEnd ->
onCategoryEdit(category, name, gradientStart, gradientEnd) onCategoryEdit(category, name, gradientStart, gradientEnd)
} },
onPin = { onCategoryPin(category) } // NEW: Pass pin callback
) )
} }
} }
@ -112,7 +114,7 @@ fun MainScreen(
start = 16.dp, start = 16.dp,
end = 16.dp, end = 16.dp,
top = 16.dp, top = 16.dp,
bottom = 100.dp // Extra space untuk floating bottom bar bottom = 100.dp
), ),
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalItemSpacing = 12.dp, verticalItemSpacing = 12.dp,

View File

@ -31,7 +31,8 @@ fun CategoryCard(
noteCount: Int, noteCount: Int,
onClick: () -> Unit, onClick: () -> Unit,
onDelete: () -> Unit = {}, onDelete: () -> Unit = {},
onEdit: (String, Long, Long) -> Unit = { _, _, _ -> } onEdit: (String, Long, Long) -> Unit = { _, _, _ -> },
onPin: () -> Unit = {} // NEW: Pin callback
) { ) {
var showDeleteConfirm by remember { mutableStateOf(false) } var showDeleteConfirm by remember { mutableStateOf(false) }
var showEditDialog by remember { mutableStateOf(false) } var showEditDialog by remember { mutableStateOf(false) }
@ -159,6 +160,25 @@ fun CategoryCard(
) )
} }
// NEW: Pin Indicator & Menu Button
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Pin indicator (only show if pinned)
AnimatedVisibility(
visible = category.isPinned,
enter = scaleIn() + fadeIn(),
exit = scaleOut() + fadeOut()
) {
Icon(
Icons.Default.PushPin,
contentDescription = "Pinned",
tint = AppColors.Warning,
modifier = Modifier.size(20.dp)
)
}
// Menu Button // Menu Button
Box { Box {
IconButton( IconButton(
@ -178,6 +198,35 @@ fun CategoryCard(
onDismissRequest = { showMenu = false }, onDismissRequest = { showMenu = false },
modifier = Modifier.background(AppColors.SurfaceElevated) modifier = Modifier.background(AppColors.SurfaceElevated)
) { ) {
// NEW: Pin/Unpin Menu Item
DropdownMenuItem(
text = {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(Constants.Spacing.Small.dp)
) {
Icon(
Icons.Default.PushPin,
contentDescription = null,
tint = if (category.isPinned) AppColors.Warning else AppColors.OnSurfaceVariant,
modifier = Modifier.size(18.dp)
)
Text(
if (category.isPinned) "Lepas Pin" else "Pin Kategori",
color = AppColors.OnSurface,
fontSize = 14.sp
)
}
},
onClick = {
onPin()
showMenu = false
}
)
HorizontalDivider(color = AppColors.Divider)
// Edit Menu Item
DropdownMenuItem( DropdownMenuItem(
text = { text = {
Row( Row(
@ -203,6 +252,9 @@ fun CategoryCard(
} }
) )
HorizontalDivider(color = AppColors.Divider)
// Delete Menu Item
DropdownMenuItem( DropdownMenuItem(
text = { text = {
Row( Row(
@ -230,6 +282,7 @@ fun CategoryCard(
} }
} }
} }
}
Spacer(modifier = Modifier.height(Constants.Spacing.Medium.dp)) Spacer(modifier = Modifier.height(Constants.Spacing.Medium.dp))

View File

@ -1,6 +1,7 @@
// File: presentation/screens/main/components/NoteCard.kt // 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.*
import androidx.compose.animation.core.* import androidx.compose.animation.core.*
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
@ -9,11 +10,11 @@ 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.StarBorder
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.rotate
import androidx.compose.ui.draw.scale 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
@ -50,6 +51,23 @@ fun NoteCard(
label = "scale" label = "scale"
) )
// Pin icon rotation animation
val pinRotation by animateFloatAsState(
targetValue = if (note.isPinned) 0f else 45f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessMedium
),
label = "rotation"
)
// Elevation animation for pinned state
val elevation by animateDpAsState(
targetValue = if (note.isPinned) Constants.Elevation.Medium.dp else Constants.Elevation.Small.dp,
animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy),
label = "elevation"
)
// Delete Confirmation Dialog // Delete Confirmation Dialog
if (showDeleteConfirm) { if (showDeleteConfirm) {
AlertDialog( AlertDialog(
@ -58,7 +76,8 @@ fun NoteCard(
Icon( Icon(
Icons.Default.DeleteForever, Icons.Default.DeleteForever,
contentDescription = null, contentDescription = null,
tint = AppColors.Error tint = AppColors.Error,
modifier = Modifier.size(32.dp)
) )
}, },
title = { title = {
@ -104,10 +123,13 @@ fun NoteCard(
.combinedClickable(onClick = onClick), .combinedClickable(onClick = onClick),
shape = RoundedCornerShape(Constants.Radius.Large.dp), shape = RoundedCornerShape(Constants.Radius.Large.dp),
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
containerColor = AppColors.SurfaceVariant containerColor = if (note.isPinned)
AppColors.SurfaceVariant.copy(alpha = 0.95f)
else
AppColors.SurfaceVariant
), ),
elevation = CardDefaults.cardElevation( elevation = CardDefaults.cardElevation(
defaultElevation = Constants.Elevation.Small.dp defaultElevation = elevation
) )
) { ) {
Column( Column(
@ -115,40 +137,74 @@ fun NoteCard(
.fillMaxWidth() .fillMaxWidth()
.padding(Constants.Spacing.Large.dp) .padding(Constants.Spacing.Large.dp)
) { ) {
// Header: Title + Actions (Vertical) // Header: Title + Menu
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Top verticalAlignment = Alignment.Top
) { ) {
// Title - takes most space // Title with pin badge
Row(
modifier = Modifier.weight(1f),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Text( Text(
note.title, note.title,
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = AppColors.OnBackground, color = AppColors.OnBackground,
modifier = Modifier.weight(1f),
maxLines = 2, maxLines = 2,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
fontSize = 18.sp fontSize = 18.sp,
modifier = Modifier.weight(1f, fill = false)
) )
// Vertical Actions Stack // Pin Badge next to title
Column( AnimatedVisibility(
horizontalAlignment = Alignment.End, visible = note.isPinned,
verticalArrangement = Arrangement.spacedBy(0.dp) enter = scaleIn(spring(dampingRatio = Spring.DampingRatioMediumBouncy)) + fadeIn(),
exit = scaleOut(spring(dampingRatio = Spring.DampingRatioMediumBouncy)) + fadeOut()
) { ) {
Surface(
color = AppColors.Warning.copy(alpha = 0.2f),
shape = RoundedCornerShape(Constants.Radius.Small.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.padding(horizontal = 6.dp, vertical = 3.dp)
) {
Icon(
Icons.Filled.PushPin,
contentDescription = "Disematkan",
tint = AppColors.Warning,
modifier = Modifier
.size(12.dp)
.rotate(45f)
)
Text(
"Pin",
fontSize = 10.sp,
fontWeight = FontWeight.Bold,
color = AppColors.Warning
)
}
}
}
}
// Menu Button // Menu Button
Box { Box {
IconButton( IconButton(
onClick = { showMenu = true }, onClick = { showMenu = true },
modifier = Modifier.size(28.dp) modifier = Modifier.size(32.dp)
) { ) {
Icon( Icon(
Icons.Default.MoreVert, Icons.Default.MoreVert,
contentDescription = "Menu", contentDescription = "Menu",
tint = AppColors.OnSurfaceVariant, tint = AppColors.OnSurfaceVariant,
modifier = Modifier.size(18.dp) modifier = Modifier.size(20.dp)
) )
} }
@ -157,6 +213,36 @@ fun NoteCard(
onDismissRequest = { showMenu = false }, onDismissRequest = { showMenu = false },
modifier = Modifier.background(AppColors.SurfaceElevated) modifier = Modifier.background(AppColors.SurfaceElevated)
) { ) {
// Pin/Unpin option
DropdownMenuItem(
text = {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(Constants.Spacing.Small.dp)
) {
Icon(
Icons.Filled.PushPin,
contentDescription = null,
tint = if (note.isPinned) AppColors.Warning else AppColors.Primary,
modifier = Modifier
.size(18.dp)
.rotate(pinRotation)
)
Text(
if (note.isPinned) "Lepas Sematan" else "Sematkan",
color = AppColors.OnSurface,
fontSize = 14.sp
)
}
},
onClick = {
showMenu = false
onPinClick()
}
)
HorizontalDivider(color = AppColors.Divider)
DropdownMenuItem( DropdownMenuItem(
text = { text = {
Row( Row(
@ -208,23 +294,9 @@ fun NoteCard(
) )
} }
} }
// Pin Button
IconButton(
onClick = onPinClick,
modifier = Modifier.size(28.dp)
) {
Icon(
if (note.isPinned) Icons.Filled.Star else Icons.Outlined.StarBorder,
contentDescription = "Pin",
tint = if (note.isPinned) AppColors.Warning else AppColors.OnSurfaceVariant,
modifier = Modifier.size(18.dp)
)
}
}
} }
// Deskripsi Preview // Description Preview
if (note.description.isNotEmpty()) { if (note.description.isNotEmpty()) {
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
@ -238,7 +310,6 @@ fun NoteCard(
fontSize = 14.sp fontSize = 14.sp
) )
} else { } else {
// Tampilkan placeholder jika deskripsi kosong
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
Text( Text(
@ -272,6 +343,22 @@ fun NoteCard(
color = AppColors.OnSurfaceTertiary, color = AppColors.OnSurfaceTertiary,
fontSize = 12.sp fontSize = 12.sp
) )
// Pin indicator icon in footer
AnimatedVisibility(
visible = note.isPinned,
enter = fadeIn() + expandHorizontally(),
exit = fadeOut() + shrinkHorizontally()
) {
Icon(
Icons.Filled.PushPin,
contentDescription = "Disematkan",
tint = AppColors.Warning.copy(alpha = 0.6f),
modifier = Modifier
.size(14.dp)
.rotate(45f)
)
}
} }
} }
} }

View File

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

View File

@ -1,193 +1,122 @@
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 Constants { object AppColors {
// App Info // Primary Colors
const val APP_NAME = "NotesAI" val Primary = Color(0xFF6C63FF)
const val APP_VERSION = "1.1.0" val Secondary = Color(0xFF03DAC6)
val Accent = Color(0xFFFF6B9D)
// DataStore // Background Colors
const val DATASTORE_NAME = "notes_prefs" val Background = Color(0xFF121212)
const val DEBOUNCE_DELAY = 500L val Surface = Color(0xFF1E1E1E)
val SurfaceVariant = Color(0xFF2A2A2A)
// UI Constants
const val MAX_NOTE_PREVIEW_LINES = 4
const val MAX_CHAT_PREVIEW_LINES = 2
const val GRID_COLUMNS = 2
// DARK THEME COLORS
object DarkColors {
val Background = Color(0xFF0A0A0A)
val Surface = Color(0xFF141414)
val SurfaceVariant = Color(0xFF1E1E1E)
val SurfaceElevated = Color(0xFF252525) val SurfaceElevated = Color(0xFF252525)
val Primary = Color(0xFF3B82F6)
val PrimaryVariant = Color(0xFF60A5FA)
val PrimaryContainer = Color(0xFF1E3A8A)
val Secondary = Color(0xFF8B5CF6)
val SecondaryVariant = Color(0xFFA78BFA)
val OnBackground = Color(0xFFFFFFFF)
val OnSurface = Color(0xFFE5E5E5)
val OnSurfaceVariant = Color(0xFF9CA3AF)
val OnSurfaceTertiary = Color(0xFF6B7280)
val Success = Color(0xFF10B981)
val Error = Color(0xFFEF4444)
val Warning = Color(0xFFFBBF24)
val Info = Color(0xFF3B82F6)
val Border = Color(0xFF2A2A2A)
val Divider = Color(0xFF1F1F1F)
val Overlay = Color(0xFF000000).copy(alpha = 0.5f)
val Shadow = Color(0xFF000000).copy(alpha = 0.3f)
}
// LIGHT THEME COLORS // Text Colors
object LightColors { val OnBackground = Color(0xFFE1E1E1)
val Background = Color(0xFFF8F9FA) val OnSurface = Color(0xFFCCCCCC)
val Surface = Color(0xFFFFFFFF) val OnSurfaceVariant = Color(0xFF9E9E9E)
val SurfaceVariant = Color(0xFFF1F3F5) val OnSurfaceTertiary = Color(0xFF757575)
val SurfaceElevated = Color(0xFFFFFFFF)
val Primary = Color(0xFF3B82F6)
val PrimaryVariant = Color(0xFF2563EB)
val PrimaryContainer = Color(0xFFDCEEFF)
val Secondary = Color(0xFF8B5CF6)
val SecondaryVariant = Color(0xFF7C3AED)
val OnBackground = Color(0xFF1F2937)
val OnSurface = Color(0xFF374151)
val OnSurfaceVariant = Color(0xFF6B7280)
val OnSurfaceTertiary = Color(0xFF9CA3AF)
val Success = Color(0xFF10B981)
val Error = Color(0xFFEF4444)
val Warning = Color(0xFFA16207)
val Info = Color(0xFF3B82F6)
val Border = Color(0xFFE5E7EB)
val Divider = Color(0xFFF3F4F6)
val Overlay = Color(0xFF000000).copy(alpha = 0.3f)
val Shadow = Color(0xFF000000).copy(alpha = 0.1f)
}
// Category Colors - Same for both themes // Utility Colors
val CategoryColors = listOf( val Error = Color(0xFFCF6679)
Pair(0xFF3B82F6L, 0xFF60A5FAL), // Blue val Warning = Color(0xFFFFB74D)
Pair(0xFF8B5CF6L, 0xFFA78BFAL), // Purple val Success = Color(0xFF81C784)
Pair(0xFF10B981L, 0xFF34D399L), // Green val Info = Color(0xFF64B5F6)
Pair(0xFFF59E0BL, 0xFFFBBF24L), // Amber
Pair(0xFFEF4444L, 0xFFF87171L), // Red // Border & Divider
Pair(0xFF06B6D4L, 0xFF22D3EEL), // Cyan val Border = Color(0xFF3A3A3A)
Pair(0xFFEC4899L, 0xFFF472B6L), // Pink val Divider = Color(0xFF2E2E2E)
Pair(0xFF6366F1L, 0xFF818CF8L) // Indigo }
)
object Constants {
// Animation Durations // Animation Durations
const val ANIMATION_DURATION_SHORT = 150 const val ANIMATION_DURATION_SHORT = 150
const val ANIMATION_DURATION_MEDIUM = 300 const val ANIMATION_DURATION_MEDIUM = 300
const val ANIMATION_DURATION_LONG = 500 const val ANIMATION_DURATION_LONG = 500
const val FADE_IN_DURATION = 200
const val FADE_OUT_DURATION = 200
// Spacing System // Spacing values
object Spacing { object Spacing {
const val ExtraSmall = 4 const val ExtraSmall = 4
const val Small = 8 const val Small = 8
const val Medium = 16 const val Medium = 12
const val Large = 24 const val Large = 16
const val ExtraLarge = 32 const val ExtraLarge = 24
const val XXLarge = 48 const val ExtraExtraLarge = 32
} }
// Corner Radius // Border Radius values
object Radius { object Radius {
const val Small = 8 const val Small = 8
const val Medium = 12 const val Medium = 12
const val Large = 16 const val Large = 16
const val ExtraLarge = 20 const val ExtraLarge = 24
const val Round = 999 const val ExtraExtraLarge = 32
} }
// Elevation // Elevation values
object Elevation { object Elevation {
const val None = 0 const val None = 0
const val Small = 2 const val Small = 2
const val Medium = 4 const val Medium = 4
const val Large = 8 const val Large = 8
const val ExtraLarge = 16 const val ExtraLarge = 12
}
}
// REACTIVE APP COLORS - Using Compose State
object AppColors {
// Internal state
private var _isDark by mutableStateOf(true)
// Public setter
fun setTheme(isDark: Boolean) {
_isDark = isDark
} }
// All colors are now reactive via mutableStateOf // Reference to AppColors for compatibility
val Background: Color val AppColors = com.example.notesai.util.AppColors
get() = if (_isDark) Constants.DarkColors.Background else Constants.LightColors.Background
val Surface: Color // Category gradient colors
get() = if (_isDark) Constants.DarkColors.Surface else Constants.LightColors.Surface val CategoryColors = listOf(
// Purple gradients
0xFF6750A4L to 0xFF7E57C2L,
0xFF9C27B0L to 0xFFE91E63L,
val SurfaceVariant: Color // Blue gradients
get() = if (_isDark) Constants.DarkColors.SurfaceVariant else Constants.LightColors.SurfaceVariant 0xFF2196F3L to 0xFF03A9F4L,
0xFF1976D2L to 0xFF4FC3F7L,
val SurfaceElevated: Color // Green gradients
get() = if (_isDark) Constants.DarkColors.SurfaceElevated else Constants.LightColors.SurfaceElevated 0xFF4CAF50L to 0xFF8BC34AL,
0xFF009688L to 0xFF00BCD4L,
val Primary: Color // Orange gradients
get() = if (_isDark) Constants.DarkColors.Primary else Constants.LightColors.Primary 0xFFFF9800L to 0xFFFFB74DL,
0xFFFF5722L to 0xFFFF7043L,
val PrimaryVariant: Color // Red gradients
get() = if (_isDark) Constants.DarkColors.PrimaryVariant else Constants.LightColors.PrimaryVariant 0xFFF44336L to 0xFFE91E63L,
0xFFD32F2FL to 0xFFFF5252L,
val PrimaryContainer: Color // Teal gradients
get() = if (_isDark) Constants.DarkColors.PrimaryContainer else Constants.LightColors.PrimaryContainer 0xFF009688L to 0xFF26A69AL,
0xFF00897BL to 0xFF4DB6ACL,
val Secondary: Color // Indigo gradients
get() = if (_isDark) Constants.DarkColors.Secondary else Constants.LightColors.Secondary 0xFF3F51B5L to 0xFF5C6BC0L,
0xFF303F9FL to 0xFF7986CBL,
val SecondaryVariant: Color // Amber gradients
get() = if (_isDark) Constants.DarkColors.SecondaryVariant else Constants.LightColors.SecondaryVariant 0xFFFFC107L to 0xFFFFD54FL,
0xFFFFB300L to 0xFFFFCA28L,
val OnBackground: Color // Pink gradients
get() = if (_isDark) Constants.DarkColors.OnBackground else Constants.LightColors.OnBackground 0xFFE91E63L to 0xFFF06292L,
0xFFC2185BL to 0xFFEC407AL,
val OnSurface: Color // Cyan gradients
get() = if (_isDark) Constants.DarkColors.OnSurface else Constants.LightColors.OnSurface 0xFF00BCD4L to 0xFF26C6DAL,
0xFF0097A7L to 0xFF00ACC1L,
val OnSurfaceVariant: Color // Deep Purple gradients
get() = if (_isDark) Constants.DarkColors.OnSurfaceVariant else Constants.LightColors.OnSurfaceVariant 0xFF673AB7L to 0xFF9575CDL,
0xFF512DA8L to 0xFF7E57C2L,
val OnSurfaceTertiary: Color // Lime gradients
get() = if (_isDark) Constants.DarkColors.OnSurfaceTertiary else Constants.LightColors.OnSurfaceTertiary 0xFFCDDC39L to 0xFFD4E157L,
0xFFAFB42BL to 0xFFC0CA33L
val Success: Color )
get() = if (_isDark) Constants.DarkColors.Success else Constants.LightColors.Success
val Error: Color
get() = if (_isDark) Constants.DarkColors.Error else Constants.LightColors.Error
val Warning: Color
get() = if (_isDark) Constants.DarkColors.Warning else Constants.LightColors.Warning
val Info: Color
get() = if (_isDark) Constants.DarkColors.Info else Constants.LightColors.Info
val Border: Color
get() = if (_isDark) Constants.DarkColors.Border else Constants.LightColors.Border
val Divider: Color
get() = if (_isDark) Constants.DarkColors.Divider else Constants.LightColors.Divider
val Overlay: Color
get() = if (_isDark) Constants.DarkColors.Overlay else Constants.LightColors.Overlay
val Shadow: Color
get() = if (_isDark) Constants.DarkColors.Shadow else Constants.LightColors.Shadow
} }