Merge remote-tracking branch 'origin/1.1.0' into 1.1.0
This commit is contained in:
commit
b6a6b86411
4
.idea/deploymentTargetSelector.xml
generated
4
.idea/deploymentTargetSelector.xml
generated
@ -4,10 +4,10 @@
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<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">
|
||||
<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>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
|
||||
@ -16,8 +16,6 @@ android {
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
@ -33,65 +31,20 @@ android {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
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 {
|
||||
resources {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
@ -106,4 +59,59 @@ android {
|
||||
excludes += "/META-INF/*.kotlin_module"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
@ -44,11 +44,11 @@ class MainActivity : ComponentActivity() {
|
||||
colorScheme = darkColorScheme(
|
||||
primary = AppColors.Primary,
|
||||
onPrimary = Color.White,
|
||||
primaryContainer = AppColors.PrimaryContainer,
|
||||
primaryContainer = AppColors.Primary.copy(alpha = 0.3f),
|
||||
onPrimaryContainer = Color.White,
|
||||
secondary = AppColors.Secondary,
|
||||
onSecondary = Color.White,
|
||||
secondaryContainer = AppColors.SecondaryVariant,
|
||||
secondaryContainer = AppColors.Secondary.copy(alpha = 0.3f),
|
||||
onSecondaryContainer = Color.White,
|
||||
background = AppColors.Background,
|
||||
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)
|
||||
@Composable
|
||||
fun NotesApp() {
|
||||
@ -106,6 +97,7 @@ fun NotesApp() {
|
||||
val dataStoreManager = remember { DataStoreManager(context) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current
|
||||
|
||||
var categories by remember { mutableStateOf(listOf<Category>()) }
|
||||
var notes by remember { mutableStateOf(listOf<Note>()) }
|
||||
var selectedCategory by remember { mutableStateOf<Category?>(null) }
|
||||
@ -120,10 +112,17 @@ fun NotesApp() {
|
||||
var fullScreenNote by remember { mutableStateOf<Note?>(null) }
|
||||
var isDarkTheme by remember { mutableStateOf(true) }
|
||||
|
||||
// Guard flags to prevent race conditions
|
||||
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) {
|
||||
dataStoreManager.themeFlow.collect { theme ->
|
||||
isDarkTheme = theme == "dark"
|
||||
@ -131,7 +130,6 @@ fun NotesApp() {
|
||||
}
|
||||
}
|
||||
|
||||
// Load categories ONCE
|
||||
LaunchedEffect(Unit) {
|
||||
dataStoreManager.categoriesFlow.collect { loadedCategories ->
|
||||
if (!isDataLoaded) {
|
||||
@ -141,18 +139,16 @@ fun NotesApp() {
|
||||
}
|
||||
}
|
||||
|
||||
// Load notes ONCE
|
||||
LaunchedEffect(Unit) {
|
||||
dataStoreManager.notesFlow.collect { loadedNotes ->
|
||||
if (!isDataLoaded) {
|
||||
android.util.Log.d("NotesApp", "Loading ${loadedNotes.size} notes")
|
||||
notes = loadedNotes
|
||||
isDataLoaded = true // Mark as loaded
|
||||
isDataLoaded = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save categories when changed
|
||||
LaunchedEffect(categories) {
|
||||
if (isDataLoaded && categories.isNotEmpty()) {
|
||||
android.util.Log.d("NotesApp", "Saving ${categories.size} categories")
|
||||
@ -162,7 +158,6 @@ fun NotesApp() {
|
||||
}
|
||||
}
|
||||
|
||||
// Save notes when changed
|
||||
LaunchedEffect(notes) {
|
||||
if (isDataLoaded && notes.isNotEmpty()) {
|
||||
android.util.Log.d("NotesApp", "Saving ${notes.size} notes")
|
||||
@ -172,7 +167,6 @@ fun NotesApp() {
|
||||
}
|
||||
}
|
||||
|
||||
// Save on lifecycle events
|
||||
DisposableEffect(lifecycleOwner) {
|
||||
val observer = androidx.lifecycle.LifecycleEventObserver { _, event ->
|
||||
if (event == androidx.lifecycle.Lifecycle.Event.ON_PAUSE ||
|
||||
@ -319,7 +313,7 @@ fun NotesApp() {
|
||||
) {
|
||||
when (currentScreen) {
|
||||
"main" -> MainScreen(
|
||||
categories = categories.filter { !it.isDeleted },
|
||||
categories = sortCategories(categories),
|
||||
notes = notes,
|
||||
selectedCategory = selectedCategory,
|
||||
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 ->
|
||||
editingNote = note
|
||||
showNoteDialog = true
|
||||
@ -439,7 +439,6 @@ fun NotesApp() {
|
||||
}
|
||||
}
|
||||
|
||||
// Dialogs
|
||||
if (showCategoryDialog) {
|
||||
CategoryDialog(
|
||||
onDismiss = { showCategoryDialog = false },
|
||||
@ -457,6 +456,7 @@ fun NotesApp() {
|
||||
if (showNoteDialog && selectedCategory != null) {
|
||||
NoteDialog(
|
||||
note = editingNote,
|
||||
categoryId = selectedCategory!!.id,
|
||||
onDismiss = {
|
||||
showNoteDialog = false
|
||||
editingNote = null
|
||||
@ -498,7 +498,6 @@ fun NotesApp() {
|
||||
}
|
||||
}
|
||||
|
||||
// Drawer with Animation
|
||||
AnimatedVisibility(
|
||||
visible = drawerState,
|
||||
enter = fadeIn() + slideInHorizontally(
|
||||
@ -530,4 +529,6 @@ fun NotesApp() {
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun AppColors.setTheme(darkTheme: Boolean) {}
|
||||
@ -39,7 +39,7 @@ fun DrawerMenu(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(AppColors.Overlay)
|
||||
.background(Color.Black.copy(alpha = 0.5f))
|
||||
.clickable(
|
||||
onClick = onDismiss,
|
||||
indication = null,
|
||||
@ -99,7 +99,7 @@ fun DrawerMenu(
|
||||
Spacer(modifier = Modifier.height(Constants.Spacing.Medium.dp))
|
||||
|
||||
Text(
|
||||
Constants.APP_NAME,
|
||||
"AI Notes",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
color = AppColors.OnBackground,
|
||||
fontWeight = FontWeight.Bold,
|
||||
@ -179,7 +179,7 @@ fun DrawerMenu(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
"Version ${Constants.APP_VERSION}",
|
||||
"Version 1.0.0",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = AppColors.OnSurfaceTertiary,
|
||||
fontSize = 12.sp
|
||||
|
||||
@ -1,157 +1,168 @@
|
||||
// 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.core.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.notesai.util.AppColors
|
||||
import com.example.notesai.util.Constants
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.notesai.util.AppColors
|
||||
import com.example.notesai.util.Constants
|
||||
|
||||
@Composable
|
||||
fun CategoryDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onSave: (String, Long, Long) -> Unit
|
||||
) {
|
||||
var name by remember { mutableStateOf("") }
|
||||
var selectedGradient by remember { mutableStateOf(0) }
|
||||
@Composable
|
||||
fun CategoryDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onSave: (String, Long, Long) -> Unit
|
||||
) {
|
||||
var name by remember { mutableStateOf("") }
|
||||
var selectedGradient by remember { mutableStateOf(0) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
containerColor = AppColors.Surface,
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
title = {
|
||||
Text(
|
||||
"Buat Kategori Baru",
|
||||
color = AppColors.OnBackground,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 20.sp
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp)
|
||||
) {
|
||||
// Input Nama
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
label = {
|
||||
Text(
|
||||
"Nama Kategori",
|
||||
color = AppColors.OnSurfaceVariant
|
||||
)
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
"Contoh: Pekerjaan, Personal",
|
||||
color = AppColors.OnSurfaceTertiary,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedTextColor = AppColors.OnBackground,
|
||||
unfocusedTextColor = AppColors.OnSurface,
|
||||
focusedContainerColor = AppColors.SurfaceVariant,
|
||||
unfocusedContainerColor = AppColors.SurfaceVariant,
|
||||
cursorColor = AppColors.Primary,
|
||||
focusedBorderColor = AppColors.Primary,
|
||||
unfocusedBorderColor = Color.Transparent
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
// Gradient Selector
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
containerColor = AppColors.Surface,
|
||||
shape = RoundedCornerShape(Constants.Radius.Large.dp),
|
||||
title = {
|
||||
Text(
|
||||
"Kategori Baru",
|
||||
color = AppColors.OnBackground,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 20.sp
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Name Input
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
label = {
|
||||
Text(
|
||||
"Pilih Warna:",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = AppColors.OnSurface,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
"Nama Kategori",
|
||||
color = AppColors.OnSurfaceVariant
|
||||
)
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
"Contoh: Pekerjaan, Pribadi...",
|
||||
color = AppColors.OnSurfaceTertiary,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedTextColor = AppColors.OnBackground,
|
||||
unfocusedTextColor = AppColors.OnSurface,
|
||||
focusedContainerColor = AppColors.SurfaceVariant,
|
||||
unfocusedContainerColor = AppColors.SurfaceVariant,
|
||||
cursorColor = AppColors.Primary,
|
||||
focusedBorderColor = AppColors.Primary,
|
||||
unfocusedBorderColor = Color.Transparent
|
||||
),
|
||||
shape = RoundedCornerShape(Constants.Radius.Medium.dp),
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
Constants.CategoryColors.chunked(4).forEach { row ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Color picker title
|
||||
Text(
|
||||
"Pilih Warna:",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = AppColors.OnSurface,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Color Grid
|
||||
Constants.CategoryColors.chunked(4).forEach { row ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
row.forEachIndexed { _, gradient ->
|
||||
val globalIndex = Constants.CategoryColors.indexOf(gradient)
|
||||
val isSelected = selectedGradient == globalIndex
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.aspectRatio(1f)
|
||||
.clip(RoundedCornerShape(Constants.Radius.Medium.dp))
|
||||
.background(
|
||||
brush = Brush.linearGradient(
|
||||
colors = listOf(
|
||||
Color(gradient.first),
|
||||
Color(gradient.second)
|
||||
)
|
||||
)
|
||||
)
|
||||
.clickable { selectedGradient = globalIndex },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
row.forEach { gradient ->
|
||||
val globalIndex = Constants.CategoryColors.indexOf(gradient)
|
||||
val isSelected = selectedGradient == globalIndex
|
||||
|
||||
// Scale animation
|
||||
val scale by animateFloatAsState(
|
||||
targetValue = if (isSelected) 1.1f else 1f,
|
||||
// Checkmark with animation
|
||||
androidx.compose.animation.AnimatedVisibility(
|
||||
visible = isSelected,
|
||||
enter = scaleIn(
|
||||
animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||
stiffness = Spring.StiffnessMedium
|
||||
),
|
||||
label = "scale"
|
||||
)
|
||||
) + fadeIn(),
|
||||
exit = scaleOut(
|
||||
animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||
stiffness = Spring.StiffnessMedium
|
||||
)
|
||||
) + fadeOut()
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Check,
|
||||
contentDescription = "Selected",
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.aspectRatio(1f)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(
|
||||
brush = Brush.linearGradient(
|
||||
colors = listOf(
|
||||
Color(gradient.first),
|
||||
Color(gradient.second)
|
||||
)
|
||||
)
|
||||
)
|
||||
.clickable { selectedGradient = globalIndex },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
// Check icon dengan animation
|
||||
this@Row.AnimatedVisibility(
|
||||
visible = isSelected,
|
||||
enter = scaleIn() + fadeIn(),
|
||||
exit = scaleOut() + fadeOut()
|
||||
) {
|
||||
Surface(
|
||||
color = Color.White.copy(alpha = 0.9f),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
tint = Color(gradient.first),
|
||||
modifier = Modifier
|
||||
.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(
|
||||
onClick = {
|
||||
if (name.isNotBlank()) {
|
||||
@ -164,29 +175,17 @@
|
||||
containerColor = AppColors.Primary,
|
||||
disabledContainerColor = AppColors.Primary.copy(alpha = 0.5f)
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
shape = RoundedCornerShape(Constants.Radius.Medium.dp),
|
||||
modifier = Modifier.height(48.dp)
|
||||
) {
|
||||
Text(
|
||||
"Simpan",
|
||||
"Buat",
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 15.sp
|
||||
)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = onDismiss,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
modifier = Modifier.height(48.dp)
|
||||
) {
|
||||
Text(
|
||||
"Batal",
|
||||
color = AppColors.OnSurfaceVariant,
|
||||
fontSize = 15.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -17,10 +17,11 @@ import com.example.notesai.util.Constants
|
||||
|
||||
@Composable
|
||||
fun NoteDialog(
|
||||
note: Note?,
|
||||
categoryId: String, // Parameter untuk kategori ID
|
||||
note: Note? = null, // Null jika buat baru, isi jika edit
|
||||
onDismiss: () -> Unit,
|
||||
onSave: (String, String) -> Unit,
|
||||
onDelete: (() -> Unit)?
|
||||
onDelete: (() -> Unit)? = null
|
||||
) {
|
||||
var title by remember { mutableStateOf(note?.title ?: "") }
|
||||
var description by remember { mutableStateOf(note?.description ?: "") }
|
||||
@ -31,7 +32,7 @@ fun NoteDialog(
|
||||
AlertDialog(
|
||||
onDismissRequest = { showDeleteConfirm = false },
|
||||
containerColor = AppColors.Surface,
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
shape = RoundedCornerShape(Constants.Radius.Large.dp),
|
||||
title = {
|
||||
Text(
|
||||
"Hapus Catatan?",
|
||||
@ -54,7 +55,7 @@ fun NoteDialog(
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = AppColors.Error
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
shape = RoundedCornerShape(Constants.Radius.Medium.dp)
|
||||
) {
|
||||
Text("Hapus", color = Color.White, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
@ -62,7 +63,7 @@ fun NoteDialog(
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = { showDeleteConfirm = false },
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
shape = RoundedCornerShape(Constants.Radius.Medium.dp)
|
||||
) {
|
||||
Text("Batal", color = AppColors.OnSurfaceVariant)
|
||||
}
|
||||
@ -73,7 +74,7 @@ fun NoteDialog(
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
containerColor = AppColors.Surface,
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
shape = RoundedCornerShape(Constants.Radius.Large.dp),
|
||||
title = {
|
||||
Text(
|
||||
if (note == null) "Catatan Baru" else "Edit Catatan",
|
||||
@ -113,7 +114,7 @@ fun NoteDialog(
|
||||
focusedBorderColor = AppColors.Primary,
|
||||
unfocusedBorderColor = Color.Transparent
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
shape = RoundedCornerShape(Constants.Radius.Medium.dp),
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
@ -146,7 +147,7 @@ fun NoteDialog(
|
||||
focusedBorderColor = AppColors.Primary,
|
||||
unfocusedBorderColor = Color.Transparent
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
shape = RoundedCornerShape(Constants.Radius.Medium.dp),
|
||||
maxLines = 8
|
||||
)
|
||||
}
|
||||
@ -174,7 +175,7 @@ fun NoteDialog(
|
||||
// Cancel button
|
||||
TextButton(
|
||||
onClick = onDismiss,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
shape = RoundedCornerShape(Constants.Radius.Medium.dp),
|
||||
modifier = Modifier.height(48.dp)
|
||||
) {
|
||||
Text(
|
||||
@ -196,7 +197,7 @@ fun NoteDialog(
|
||||
containerColor = AppColors.Primary,
|
||||
disabledContainerColor = AppColors.Primary.copy(alpha = 0.5f)
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
shape = RoundedCornerShape(Constants.Radius.Medium.dp),
|
||||
modifier = Modifier.height(48.dp)
|
||||
) {
|
||||
Text(
|
||||
|
||||
@ -72,7 +72,7 @@ fun ChatHistoryDrawer(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(AppColors.Overlay)
|
||||
.background(Color.Black.copy(alpha = 0.5f))
|
||||
.clickable(
|
||||
onClick = onDismiss,
|
||||
indication = null,
|
||||
@ -883,6 +883,18 @@ private fun EmptyHistoryState(
|
||||
|
||||
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(
|
||||
if (searchQuery.isNotEmpty())
|
||||
"Coba kata kunci lain"
|
||||
|
||||
@ -28,6 +28,7 @@ fun MainScreen(
|
||||
onPinToggle: (Note) -> Unit,
|
||||
onCategoryDelete: (Category) -> Unit,
|
||||
onCategoryEdit: (Category, String, Long, Long) -> Unit,
|
||||
onCategoryPin: (Category) -> Unit, // NEW: Pin category callback
|
||||
onNoteEdit: (Note) -> Unit = {},
|
||||
onNoteDelete: (Note) -> Unit = {}
|
||||
) {
|
||||
@ -63,7 +64,7 @@ fun MainScreen(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
top = 16.dp,
|
||||
bottom = 100.dp // Extra space untuk floating bottom bar
|
||||
bottom = 100.dp
|
||||
),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalItemSpacing = 12.dp,
|
||||
@ -81,7 +82,8 @@ fun MainScreen(
|
||||
onDelete = { onCategoryDelete(category) },
|
||||
onEdit = { name, gradientStart, gradientEnd ->
|
||||
onCategoryEdit(category, name, gradientStart, gradientEnd)
|
||||
}
|
||||
},
|
||||
onPin = { onCategoryPin(category) } // NEW: Pass pin callback
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -112,7 +114,7 @@ fun MainScreen(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
top = 16.dp,
|
||||
bottom = 100.dp // Extra space untuk floating bottom bar
|
||||
bottom = 100.dp
|
||||
),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalItemSpacing = 12.dp,
|
||||
|
||||
@ -31,7 +31,8 @@ fun CategoryCard(
|
||||
noteCount: Int,
|
||||
onClick: () -> 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 showEditDialog by remember { mutableStateOf(false) }
|
||||
@ -159,74 +160,126 @@ fun CategoryCard(
|
||||
)
|
||||
}
|
||||
|
||||
// Menu Button
|
||||
Box {
|
||||
IconButton(
|
||||
onClick = { showMenu = true },
|
||||
modifier = Modifier.size(32.dp)
|
||||
// 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.MoreVert,
|
||||
contentDescription = "Menu",
|
||||
tint = AppColors.OnSurfaceVariant,
|
||||
Icons.Default.PushPin,
|
||||
contentDescription = "Pinned",
|
||||
tint = AppColors.Warning,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false },
|
||||
modifier = Modifier.background(AppColors.SurfaceElevated)
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(Constants.Spacing.Small.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Edit,
|
||||
contentDescription = null,
|
||||
tint = AppColors.Primary,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Text(
|
||||
"Edit Kategori",
|
||||
color = AppColors.OnSurface,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
showMenu = false
|
||||
showEditDialog = true
|
||||
}
|
||||
)
|
||||
// Menu Button
|
||||
Box {
|
||||
IconButton(
|
||||
onClick = { showMenu = true },
|
||||
modifier = Modifier.size(32.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.MoreVert,
|
||||
contentDescription = "Menu",
|
||||
tint = AppColors.OnSurfaceVariant,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(Constants.Spacing.Small.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
contentDescription = null,
|
||||
tint = AppColors.Error,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Text(
|
||||
"Pindah ke Sampah",
|
||||
color = AppColors.OnSurface,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false },
|
||||
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
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
showMenu = false
|
||||
showDeleteConfirm = true
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
HorizontalDivider(color = AppColors.Divider)
|
||||
|
||||
// Edit Menu Item
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(Constants.Spacing.Small.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Edit,
|
||||
contentDescription = null,
|
||||
tint = AppColors.Primary,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Text(
|
||||
"Edit Kategori",
|
||||
color = AppColors.OnSurface,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
showMenu = false
|
||||
showEditDialog = true
|
||||
}
|
||||
)
|
||||
|
||||
HorizontalDivider(color = AppColors.Divider)
|
||||
|
||||
// Delete Menu Item
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(Constants.Spacing.Small.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
contentDescription = null,
|
||||
tint = AppColors.Error,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Text(
|
||||
"Pindah ke Sampah",
|
||||
color = AppColors.OnSurface,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
showMenu = false
|
||||
showDeleteConfirm = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
// File: presentation/screens/main/components/NoteCard.kt
|
||||
package com.example.notesai.presentation.screens.main.components
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
@ -9,11 +10,11 @@ import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.outlined.StarBorder
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@ -50,6 +51,23 @@ fun NoteCard(
|
||||
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
|
||||
if (showDeleteConfirm) {
|
||||
AlertDialog(
|
||||
@ -58,7 +76,8 @@ fun NoteCard(
|
||||
Icon(
|
||||
Icons.Default.DeleteForever,
|
||||
contentDescription = null,
|
||||
tint = AppColors.Error
|
||||
tint = AppColors.Error,
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
},
|
||||
title = {
|
||||
@ -104,10 +123,13 @@ fun NoteCard(
|
||||
.combinedClickable(onClick = onClick),
|
||||
shape = RoundedCornerShape(Constants.Radius.Large.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = AppColors.SurfaceVariant
|
||||
containerColor = if (note.isPinned)
|
||||
AppColors.SurfaceVariant.copy(alpha = 0.95f)
|
||||
else
|
||||
AppColors.SurfaceVariant
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(
|
||||
defaultElevation = Constants.Elevation.Small.dp
|
||||
defaultElevation = elevation
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
@ -115,116 +137,166 @@ fun NoteCard(
|
||||
.fillMaxWidth()
|
||||
.padding(Constants.Spacing.Large.dp)
|
||||
) {
|
||||
// Header: Title + Actions (Vertical)
|
||||
// Header: Title + Menu
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.Top
|
||||
) {
|
||||
// Title - takes most space
|
||||
Text(
|
||||
note.title,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = AppColors.OnBackground,
|
||||
// Title with pin badge
|
||||
Row(
|
||||
modifier = Modifier.weight(1f),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
|
||||
// Vertical Actions Stack
|
||||
Column(
|
||||
horizontalAlignment = Alignment.End,
|
||||
verticalArrangement = Arrangement.spacedBy(0.dp)
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
// Menu Button
|
||||
Box {
|
||||
IconButton(
|
||||
onClick = { showMenu = true },
|
||||
modifier = Modifier.size(28.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.MoreVert,
|
||||
contentDescription = "Menu",
|
||||
tint = AppColors.OnSurfaceVariant,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
Text(
|
||||
note.title,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = AppColors.OnBackground,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
fontSize = 18.sp,
|
||||
modifier = Modifier.weight(1f, fill = false)
|
||||
)
|
||||
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false },
|
||||
modifier = Modifier.background(AppColors.SurfaceElevated)
|
||||
// Pin Badge next to title
|
||||
AnimatedVisibility(
|
||||
visible = note.isPinned,
|
||||
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)
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(Constants.Spacing.Small.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Edit,
|
||||
contentDescription = null,
|
||||
tint = AppColors.Primary,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Text(
|
||||
"Edit Catatan",
|
||||
color = AppColors.OnSurface,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
showMenu = false
|
||||
onEdit()
|
||||
}
|
||||
)
|
||||
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(Constants.Spacing.Small.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
contentDescription = null,
|
||||
tint = AppColors.Error,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Text(
|
||||
"Pindah ke Sampah",
|
||||
color = AppColors.OnSurface,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
showMenu = false
|
||||
showDeleteConfirm = true
|
||||
}
|
||||
)
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pin Button
|
||||
// Menu Button
|
||||
Box {
|
||||
IconButton(
|
||||
onClick = onPinClick,
|
||||
modifier = Modifier.size(28.dp)
|
||||
onClick = { showMenu = true },
|
||||
modifier = Modifier.size(32.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)
|
||||
Icons.Default.MoreVert,
|
||||
contentDescription = "Menu",
|
||||
tint = AppColors.OnSurfaceVariant,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false },
|
||||
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(
|
||||
text = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(Constants.Spacing.Small.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Edit,
|
||||
contentDescription = null,
|
||||
tint = AppColors.Primary,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Text(
|
||||
"Edit Catatan",
|
||||
color = AppColors.OnSurface,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
showMenu = false
|
||||
onEdit()
|
||||
}
|
||||
)
|
||||
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(Constants.Spacing.Small.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
contentDescription = null,
|
||||
tint = AppColors.Error,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Text(
|
||||
"Pindah ke Sampah",
|
||||
color = AppColors.OnSurface,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
showMenu = false
|
||||
showDeleteConfirm = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deskripsi Preview
|
||||
// Description Preview
|
||||
if (note.description.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
@ -238,7 +310,6 @@ fun NoteCard(
|
||||
fontSize = 14.sp
|
||||
)
|
||||
} else {
|
||||
// Tampilkan placeholder jika deskripsi kosong
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Text(
|
||||
@ -272,6 +343,22 @@ fun NoteCard(
|
||||
color = AppColors.OnSurfaceTertiary,
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3
app/src/main/java/com/example/notesai/util/AppColors.kt
Normal file
3
app/src/main/java/com/example/notesai/util/AppColors.kt
Normal file
@ -0,0 +1,3 @@
|
||||
import com.example.notesai.util.AppColors
|
||||
|
||||
annotation class AppColors
|
||||
@ -1,193 +1,122 @@
|
||||
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
|
||||
|
||||
object AppColors {
|
||||
// Primary Colors
|
||||
val Primary = Color(0xFF6C63FF)
|
||||
val Secondary = Color(0xFF03DAC6)
|
||||
val Accent = Color(0xFFFF6B9D)
|
||||
|
||||
// Background Colors
|
||||
val Background = Color(0xFF121212)
|
||||
val Surface = Color(0xFF1E1E1E)
|
||||
val SurfaceVariant = Color(0xFF2A2A2A)
|
||||
val SurfaceElevated = Color(0xFF252525)
|
||||
|
||||
// Text Colors
|
||||
val OnBackground = Color(0xFFE1E1E1)
|
||||
val OnSurface = Color(0xFFCCCCCC)
|
||||
val OnSurfaceVariant = Color(0xFF9E9E9E)
|
||||
val OnSurfaceTertiary = Color(0xFF757575)
|
||||
|
||||
// Utility Colors
|
||||
val Error = Color(0xFFCF6679)
|
||||
val Warning = Color(0xFFFFB74D)
|
||||
val Success = Color(0xFF81C784)
|
||||
val Info = Color(0xFF64B5F6)
|
||||
|
||||
// Border & Divider
|
||||
val Border = Color(0xFF3A3A3A)
|
||||
val Divider = Color(0xFF2E2E2E)
|
||||
}
|
||||
|
||||
object Constants {
|
||||
// App Info
|
||||
const val APP_NAME = "NotesAI"
|
||||
const val APP_VERSION = "1.1.0"
|
||||
|
||||
// DataStore
|
||||
const val DATASTORE_NAME = "notes_prefs"
|
||||
const val DEBOUNCE_DELAY = 500L
|
||||
|
||||
// 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 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
|
||||
object LightColors {
|
||||
val Background = Color(0xFFF8F9FA)
|
||||
val Surface = Color(0xFFFFFFFF)
|
||||
val SurfaceVariant = Color(0xFFF1F3F5)
|
||||
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
|
||||
val CategoryColors = listOf(
|
||||
Pair(0xFF3B82F6L, 0xFF60A5FAL), // Blue
|
||||
Pair(0xFF8B5CF6L, 0xFFA78BFAL), // Purple
|
||||
Pair(0xFF10B981L, 0xFF34D399L), // Green
|
||||
Pair(0xFFF59E0BL, 0xFFFBBF24L), // Amber
|
||||
Pair(0xFFEF4444L, 0xFFF87171L), // Red
|
||||
Pair(0xFF06B6D4L, 0xFF22D3EEL), // Cyan
|
||||
Pair(0xFFEC4899L, 0xFFF472B6L), // Pink
|
||||
Pair(0xFF6366F1L, 0xFF818CF8L) // Indigo
|
||||
)
|
||||
|
||||
// Animation Durations
|
||||
const val ANIMATION_DURATION_SHORT = 150
|
||||
const val ANIMATION_DURATION_MEDIUM = 300
|
||||
const val ANIMATION_DURATION_LONG = 500
|
||||
const val FADE_IN_DURATION = 200
|
||||
const val FADE_OUT_DURATION = 200
|
||||
|
||||
// Spacing System
|
||||
// Spacing values
|
||||
object Spacing {
|
||||
const val ExtraSmall = 4
|
||||
const val Small = 8
|
||||
const val Medium = 16
|
||||
const val Large = 24
|
||||
const val ExtraLarge = 32
|
||||
const val XXLarge = 48
|
||||
const val Medium = 12
|
||||
const val Large = 16
|
||||
const val ExtraLarge = 24
|
||||
const val ExtraExtraLarge = 32
|
||||
}
|
||||
|
||||
// Corner Radius
|
||||
// Border Radius values
|
||||
object Radius {
|
||||
const val Small = 8
|
||||
const val Medium = 12
|
||||
const val Large = 16
|
||||
const val ExtraLarge = 20
|
||||
const val Round = 999
|
||||
const val ExtraLarge = 24
|
||||
const val ExtraExtraLarge = 32
|
||||
}
|
||||
|
||||
// Elevation
|
||||
// Elevation values
|
||||
object Elevation {
|
||||
const val None = 0
|
||||
const val Small = 2
|
||||
const val Medium = 4
|
||||
const val Large = 8
|
||||
const val ExtraLarge = 16
|
||||
}
|
||||
}
|
||||
|
||||
// REACTIVE APP COLORS - Using Compose State
|
||||
object AppColors {
|
||||
// Internal state
|
||||
private var _isDark by mutableStateOf(true)
|
||||
|
||||
// Public setter
|
||||
fun setTheme(isDark: Boolean) {
|
||||
_isDark = isDark
|
||||
const val ExtraLarge = 12
|
||||
}
|
||||
|
||||
// All colors are now reactive via mutableStateOf
|
||||
val Background: Color
|
||||
get() = if (_isDark) Constants.DarkColors.Background else Constants.LightColors.Background
|
||||
// Reference to AppColors for compatibility
|
||||
val AppColors = com.example.notesai.util.AppColors
|
||||
|
||||
val Surface: Color
|
||||
get() = if (_isDark) Constants.DarkColors.Surface else Constants.LightColors.Surface
|
||||
// Category gradient colors
|
||||
val CategoryColors = listOf(
|
||||
// Purple gradients
|
||||
0xFF6750A4L to 0xFF7E57C2L,
|
||||
0xFF9C27B0L to 0xFFE91E63L,
|
||||
|
||||
val SurfaceVariant: Color
|
||||
get() = if (_isDark) Constants.DarkColors.SurfaceVariant else Constants.LightColors.SurfaceVariant
|
||||
// Blue gradients
|
||||
0xFF2196F3L to 0xFF03A9F4L,
|
||||
0xFF1976D2L to 0xFF4FC3F7L,
|
||||
|
||||
val SurfaceElevated: Color
|
||||
get() = if (_isDark) Constants.DarkColors.SurfaceElevated else Constants.LightColors.SurfaceElevated
|
||||
// Green gradients
|
||||
0xFF4CAF50L to 0xFF8BC34AL,
|
||||
0xFF009688L to 0xFF00BCD4L,
|
||||
|
||||
val Primary: Color
|
||||
get() = if (_isDark) Constants.DarkColors.Primary else Constants.LightColors.Primary
|
||||
// Orange gradients
|
||||
0xFFFF9800L to 0xFFFFB74DL,
|
||||
0xFFFF5722L to 0xFFFF7043L,
|
||||
|
||||
val PrimaryVariant: Color
|
||||
get() = if (_isDark) Constants.DarkColors.PrimaryVariant else Constants.LightColors.PrimaryVariant
|
||||
// Red gradients
|
||||
0xFFF44336L to 0xFFE91E63L,
|
||||
0xFFD32F2FL to 0xFFFF5252L,
|
||||
|
||||
val PrimaryContainer: Color
|
||||
get() = if (_isDark) Constants.DarkColors.PrimaryContainer else Constants.LightColors.PrimaryContainer
|
||||
// Teal gradients
|
||||
0xFF009688L to 0xFF26A69AL,
|
||||
0xFF00897BL to 0xFF4DB6ACL,
|
||||
|
||||
val Secondary: Color
|
||||
get() = if (_isDark) Constants.DarkColors.Secondary else Constants.LightColors.Secondary
|
||||
// Indigo gradients
|
||||
0xFF3F51B5L to 0xFF5C6BC0L,
|
||||
0xFF303F9FL to 0xFF7986CBL,
|
||||
|
||||
val SecondaryVariant: Color
|
||||
get() = if (_isDark) Constants.DarkColors.SecondaryVariant else Constants.LightColors.SecondaryVariant
|
||||
// Amber gradients
|
||||
0xFFFFC107L to 0xFFFFD54FL,
|
||||
0xFFFFB300L to 0xFFFFCA28L,
|
||||
|
||||
val OnBackground: Color
|
||||
get() = if (_isDark) Constants.DarkColors.OnBackground else Constants.LightColors.OnBackground
|
||||
// Pink gradients
|
||||
0xFFE91E63L to 0xFFF06292L,
|
||||
0xFFC2185BL to 0xFFEC407AL,
|
||||
|
||||
val OnSurface: Color
|
||||
get() = if (_isDark) Constants.DarkColors.OnSurface else Constants.LightColors.OnSurface
|
||||
// Cyan gradients
|
||||
0xFF00BCD4L to 0xFF26C6DAL,
|
||||
0xFF0097A7L to 0xFF00ACC1L,
|
||||
|
||||
val OnSurfaceVariant: Color
|
||||
get() = if (_isDark) Constants.DarkColors.OnSurfaceVariant else Constants.LightColors.OnSurfaceVariant
|
||||
// Deep Purple gradients
|
||||
0xFF673AB7L to 0xFF9575CDL,
|
||||
0xFF512DA8L to 0xFF7E57C2L,
|
||||
|
||||
val OnSurfaceTertiary: Color
|
||||
get() = if (_isDark) Constants.DarkColors.OnSurfaceTertiary else Constants.LightColors.OnSurfaceTertiary
|
||||
|
||||
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
|
||||
// Lime gradients
|
||||
0xFFCDDC39L to 0xFFD4E157L,
|
||||
0xFFAFB42BL to 0xFFC0CA33L
|
||||
)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user