Merge remote-tracking branch 'origin/1.1.0' into 1.1.0
This commit is contained in:
commit
9e7c273ec7
@ -301,6 +301,4 @@
|
|||||||
|
|
||||||
## **Features for Sprint 5 v1.1.0**
|
## **Features for Sprint 5 v1.1.0**
|
||||||
* Fungsi AI (Upload File) (ok)
|
* Fungsi AI (Upload File) (ok)
|
||||||
* Fitur Sematkan Category, otomatis paling atas (ok)
|
* Fitur Sematkan Category, otomatis paling atas (ok)
|
||||||
|
|
||||||
## **Unit Testing is Coming**
|
|
||||||
@ -101,9 +101,6 @@ dependencies {
|
|||||||
// PDF Parser
|
// PDF Parser
|
||||||
implementation("com.tom-roush:pdfbox-android:2.0.27.0")
|
implementation("com.tom-roush:pdfbox-android:2.0.27.0")
|
||||||
|
|
||||||
// FIREBASE SUDAH DIHAPUS - baris ini yang menyebabkan error
|
|
||||||
// implementation(libs.firebase.firestore.ktx)
|
|
||||||
|
|
||||||
// Testing
|
// Testing
|
||||||
testImplementation("junit:junit:4.13.2")
|
testImplementation("junit:junit:4.13.2")
|
||||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||||
|
|||||||
@ -40,52 +40,95 @@ class MainActivity : ComponentActivity() {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContent {
|
setContent {
|
||||||
MaterialTheme(
|
NotesAppTheme()
|
||||||
colorScheme = darkColorScheme(
|
}
|
||||||
primary = AppColors.Primary,
|
}
|
||||||
onPrimary = Color.White,
|
}
|
||||||
primaryContainer = AppColors.Primary.copy(alpha = 0.3f),
|
|
||||||
onPrimaryContainer = Color.White,
|
@Composable
|
||||||
secondary = AppColors.Secondary,
|
fun NotesAppTheme(content: @Composable () -> Unit = { NotesApp() }) {
|
||||||
onSecondary = Color.White,
|
val context = LocalContext.current
|
||||||
secondaryContainer = AppColors.Secondary.copy(alpha = 0.3f),
|
val dataStoreManager = remember { DataStoreManager(context) }
|
||||||
onSecondaryContainer = Color.White,
|
var isDarkTheme by remember { mutableStateOf(true) }
|
||||||
background = AppColors.Background,
|
|
||||||
onBackground = AppColors.OnBackground,
|
// Load theme preference
|
||||||
surface = AppColors.Surface,
|
LaunchedEffect(Unit) {
|
||||||
onSurface = AppColors.OnSurface,
|
dataStoreManager.themeFlow.collect { theme ->
|
||||||
surfaceVariant = AppColors.SurfaceVariant,
|
isDarkTheme = theme == "dark"
|
||||||
onSurfaceVariant = AppColors.OnSurfaceVariant,
|
AppColors.setTheme(isDarkTheme)
|
||||||
error = AppColors.Error,
|
}
|
||||||
onError = Color.White,
|
}
|
||||||
outline = AppColors.Border,
|
|
||||||
outlineVariant = AppColors.Divider
|
// Create dynamic color scheme based on theme
|
||||||
),
|
val colorScheme = if (isDarkTheme) {
|
||||||
typography = Typography(
|
darkColorScheme(
|
||||||
displayLarge = MaterialTheme.typography.displayLarge.copy(
|
primary = AppColors.Primary,
|
||||||
fontWeight = FontWeight.Bold
|
onPrimary = Color.White,
|
||||||
),
|
primaryContainer = AppColors.Primary.copy(alpha = 0.3f),
|
||||||
headlineLarge = MaterialTheme.typography.headlineLarge.copy(
|
onPrimaryContainer = Color.White,
|
||||||
fontWeight = FontWeight.Bold
|
secondary = AppColors.Secondary,
|
||||||
),
|
onSecondary = Color.White,
|
||||||
titleLarge = MaterialTheme.typography.titleLarge.copy(
|
secondaryContainer = AppColors.Secondary.copy(alpha = 0.3f),
|
||||||
fontWeight = FontWeight.SemiBold
|
onSecondaryContainer = Color.White,
|
||||||
),
|
background = AppColors.Background,
|
||||||
bodyLarge = MaterialTheme.typography.bodyLarge.copy(
|
onBackground = AppColors.OnBackground,
|
||||||
lineHeight = 24.sp
|
surface = AppColors.Surface,
|
||||||
),
|
onSurface = AppColors.OnSurface,
|
||||||
bodyMedium = MaterialTheme.typography.bodyMedium.copy(
|
surfaceVariant = AppColors.SurfaceVariant,
|
||||||
lineHeight = 20.sp
|
onSurfaceVariant = AppColors.OnSurfaceVariant,
|
||||||
)
|
error = AppColors.Error,
|
||||||
)
|
onError = Color.White,
|
||||||
) {
|
outline = AppColors.Border,
|
||||||
Surface(
|
outlineVariant = AppColors.Divider
|
||||||
modifier = Modifier.fillMaxSize(),
|
)
|
||||||
color = MaterialTheme.colorScheme.background
|
} else {
|
||||||
) {
|
lightColorScheme(
|
||||||
NotesApp()
|
primary = AppColors.Primary,
|
||||||
}
|
onPrimary = Color.White,
|
||||||
}
|
primaryContainer = AppColors.Primary.copy(alpha = 0.1f),
|
||||||
|
onPrimaryContainer = AppColors.Primary,
|
||||||
|
secondary = AppColors.Secondary,
|
||||||
|
onSecondary = Color.White,
|
||||||
|
secondaryContainer = AppColors.Secondary.copy(alpha = 0.1f),
|
||||||
|
onSecondaryContainer = AppColors.Secondary,
|
||||||
|
background = AppColors.Background,
|
||||||
|
onBackground = AppColors.OnBackground,
|
||||||
|
surface = AppColors.Surface,
|
||||||
|
onSurface = AppColors.OnSurface,
|
||||||
|
surfaceVariant = AppColors.SurfaceVariant,
|
||||||
|
onSurfaceVariant = AppColors.OnSurfaceVariant,
|
||||||
|
error = AppColors.Error,
|
||||||
|
onError = Color.White,
|
||||||
|
outline = AppColors.Border,
|
||||||
|
outlineVariant = AppColors.Divider
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = colorScheme,
|
||||||
|
typography = Typography(
|
||||||
|
displayLarge = MaterialTheme.typography.displayLarge.copy(
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
),
|
||||||
|
headlineLarge = MaterialTheme.typography.headlineLarge.copy(
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
),
|
||||||
|
titleLarge = MaterialTheme.typography.titleLarge.copy(
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
),
|
||||||
|
bodyLarge = MaterialTheme.typography.bodyLarge.copy(
|
||||||
|
lineHeight = 24.sp
|
||||||
|
),
|
||||||
|
bodyMedium = MaterialTheme.typography.bodyMedium.copy(
|
||||||
|
lineHeight = 20.sp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
color = MaterialTheme.colorScheme.background
|
||||||
|
) {
|
||||||
|
content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -529,6 +572,4 @@ fun NotesApp() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun AppColors.setTheme(darkTheme: Boolean) {}
|
|
||||||
@ -1,8 +1,9 @@
|
|||||||
// File: data/model/ChatMessage.kt
|
|
||||||
package com.example.notesai.data.model
|
package com.example.notesai.data.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
|
@Serializable
|
||||||
data class ChatMessage(
|
data class ChatMessage(
|
||||||
val id: String = UUID.randomUUID().toString(),
|
val id: String = UUID.randomUUID().toString(),
|
||||||
val message: String,
|
val message: String,
|
||||||
|
|||||||
@ -11,7 +11,7 @@ data class SerializableCategory(
|
|||||||
val gradientEnd: Long,
|
val gradientEnd: Long,
|
||||||
val timestamp: Long,
|
val timestamp: Long,
|
||||||
val isDeleted: Boolean = false,
|
val isDeleted: Boolean = false,
|
||||||
val isPinned: Boolean = false // NEW: Tambahkan ini
|
val isPinned: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
@SuppressLint("UnsafeOptInUsageError")
|
||||||
|
|||||||
@ -89,11 +89,11 @@ fun CategoryDialog(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
// Color Grid
|
// Color Grid - 2 rows of 4 colors
|
||||||
Constants.CategoryColors.chunked(4).forEach { row ->
|
Constants.CategoryColors.chunked(4).forEach { row ->
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
) {
|
) {
|
||||||
row.forEachIndexed { _, gradient ->
|
row.forEachIndexed { _, gradient ->
|
||||||
val globalIndex = Constants.CategoryColors.indexOf(gradient)
|
val globalIndex = Constants.CategoryColors.indexOf(gradient)
|
||||||
@ -135,13 +135,13 @@ fun CategoryDialog(
|
|||||||
Icons.Default.Check,
|
Icons.Default.Check,
|
||||||
contentDescription = "Selected",
|
contentDescription = "Selected",
|
||||||
tint = Color.White,
|
tint = Color.White,
|
||||||
modifier = Modifier.size(24.dp)
|
modifier = Modifier.size(28.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -64,7 +64,7 @@ fun AIHelperScreen(
|
|||||||
var showHistoryDrawer by remember { mutableStateOf(false) }
|
var showHistoryDrawer by remember { mutableStateOf(false) }
|
||||||
var currentChatId by remember { mutableStateOf<String?>(null) }
|
var currentChatId by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
// NEW: File Upload States
|
// File Upload States
|
||||||
var uploadedFile by remember { mutableStateOf<FileParseResult.Success?>(null) }
|
var uploadedFile by remember { mutableStateOf<FileParseResult.Success?>(null) }
|
||||||
var isGeneratingSummary by remember { mutableStateOf(false) }
|
var isGeneratingSummary by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
@ -145,19 +145,21 @@ fun AIHelperScreen(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(AppColors.Background)
|
.background(AppColors.Background)
|
||||||
) {
|
) {
|
||||||
// Top Bar with History Button & Stats
|
// UPDATED: Floating Top Bar
|
||||||
Surface(
|
Box(
|
||||||
color = AppColors.Surface,
|
modifier = Modifier
|
||||||
shadowElevation = 2.dp
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
) {
|
) {
|
||||||
Column(
|
Surface(
|
||||||
modifier = Modifier
|
color = AppColors.SurfaceElevated,
|
||||||
.fillMaxWidth()
|
shape = RoundedCornerShape(Constants.Radius.ExtraLarge.dp),
|
||||||
.padding(16.dp)
|
shadowElevation = Constants.Elevation.Large.dp
|
||||||
) {
|
) {
|
||||||
// Top Row - Menu & New Chat
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
@ -234,41 +236,9 @@ fun AIHelperScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
|
|
||||||
// Stats - Compact
|
|
||||||
val filteredNotes = if (selectedCategory != null) {
|
|
||||||
notes.filter { it.categoryId == selectedCategory!!.id && !it.isArchived }
|
|
||||||
} else {
|
|
||||||
notes.filter { !it.isArchived }
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly
|
|
||||||
) {
|
|
||||||
CompactStatItem(
|
|
||||||
icon = Icons.Default.Description,
|
|
||||||
value = filteredNotes.size.toString(),
|
|
||||||
label = "Catatan"
|
|
||||||
)
|
|
||||||
CompactStatItem(
|
|
||||||
icon = Icons.Default.Star,
|
|
||||||
value = filteredNotes.count { it.isPinned }.toString(),
|
|
||||||
label = "Dipasang"
|
|
||||||
)
|
|
||||||
CompactStatItem(
|
|
||||||
icon = Icons.Default.Folder,
|
|
||||||
value = categories.size.toString(),
|
|
||||||
label = "Kategori"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HorizontalDivider(color = AppColors.Divider)
|
|
||||||
|
|
||||||
// Chat Area
|
// Chat Area
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@ -276,18 +246,20 @@ fun AIHelperScreen(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
if (chatMessages.isEmpty()) {
|
if (chatMessages.isEmpty()) {
|
||||||
// Welcome State
|
// Welcome State - Optimized Layout
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(32.dp)
|
.padding(horizontal = 24.dp, vertical = 16.dp)
|
||||||
.verticalScroll(rememberScrollState()),
|
.verticalScroll(rememberScrollState()),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
|
Spacer(modifier = Modifier.weight(0.5f))
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(80.dp)
|
.size(64.dp)
|
||||||
.background(
|
.background(
|
||||||
color = AppColors.Primary.copy(alpha = 0.1f),
|
color = AppColors.Primary.copy(alpha = 0.1f),
|
||||||
shape = CircleShape
|
shape = CircleShape
|
||||||
@ -297,50 +269,49 @@ fun AIHelperScreen(
|
|||||||
Icon(
|
Icon(
|
||||||
Icons.Default.AutoAwesome,
|
Icons.Default.AutoAwesome,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(40.dp),
|
modifier = Modifier.size(32.dp),
|
||||||
tint = AppColors.Primary
|
tint = AppColors.Primary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
"AI Assistant",
|
"AI Assistant",
|
||||||
style = MaterialTheme.typography.headlineMedium,
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
color = AppColors.OnBackground,
|
color = AppColors.OnBackground,
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 24.sp
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
"Tanyakan apa saja tentang catatan Anda",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = AppColors.OnSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
fontSize = 14.sp
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
Text(
|
// Suggestion Chips - Lebih besar
|
||||||
"Tanyakan apa saja tentang catatan Anda",
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
color = AppColors.OnSurfaceVariant,
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
|
||||||
|
|
||||||
// Suggestion Chips
|
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
modifier = Modifier.fillMaxWidth(0.85f)
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
"Contoh pertanyaan:",
|
"Contoh pertanyaan:",
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = AppColors.OnSurfaceTertiary
|
color = AppColors.OnSurfaceTertiary,
|
||||||
|
fontSize = 13.sp
|
||||||
)
|
)
|
||||||
SuggestionChip("Analisis catatan saya") { prompt = it }
|
SuggestionChip("Analisis catatan saya") { prompt = it }
|
||||||
SuggestionChip("Buat ringkasan") { prompt = it }
|
SuggestionChip("Buat ringkasan") { prompt = it }
|
||||||
SuggestionChip("Berikan saran organisasi") { prompt = it }
|
SuggestionChip("Berikan saran organisasi") { prompt = it }
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
// NEW: File Upload Button
|
// File Upload Button - PENTING: Jangan dihapus!
|
||||||
FileUploadButton(
|
FileUploadButton(
|
||||||
onFileSelected = { fileResult ->
|
onFileSelected = { fileResult ->
|
||||||
uploadedFile = fileResult
|
uploadedFile = fileResult
|
||||||
@ -401,14 +372,16 @@ fun AIHelperScreen(
|
|||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.weight(0.5f))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Chat Messages
|
// Chat Messages - UPDATED: Bottom padding dari 100dp ke 120dp
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.verticalScroll(scrollState)
|
.verticalScroll(scrollState)
|
||||||
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 100.dp)
|
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 120.dp)
|
||||||
) {
|
) {
|
||||||
chatMessages.forEach { message ->
|
chatMessages.forEach { message ->
|
||||||
ChatBubble(
|
ChatBubble(
|
||||||
@ -427,7 +400,7 @@ fun AIHelperScreen(
|
|||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loading Indicator (untuk chat biasa DAN file summary)
|
// Loading Indicator
|
||||||
if (isLoading || isGeneratingSummary) {
|
if (isLoading || isGeneratingSummary) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@ -489,7 +462,7 @@ fun AIHelperScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Input Area - Minimalist
|
// Input Area
|
||||||
Surface(
|
Surface(
|
||||||
color = AppColors.Surface,
|
color = AppColors.Surface,
|
||||||
shadowElevation = 8.dp,
|
shadowElevation = 8.dp,
|
||||||
@ -501,7 +474,7 @@ fun AIHelperScreen(
|
|||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
// NEW: Upload File Button (di atas input text)
|
// Upload File Button (di atas input text)
|
||||||
if (chatMessages.isNotEmpty() && !isGeneratingSummary && !isLoading) {
|
if (chatMessages.isNotEmpty() && !isGeneratingSummary && !isLoading) {
|
||||||
FileUploadButton(
|
FileUploadButton(
|
||||||
onFileSelected = { fileResult ->
|
onFileSelected = { fileResult ->
|
||||||
@ -533,7 +506,6 @@ fun AIHelperScreen(
|
|||||||
isUser = true
|
isUser = true
|
||||||
)
|
)
|
||||||
|
|
||||||
// Scroll to show loading
|
|
||||||
delay(100)
|
delay(100)
|
||||||
scrollState.animateScrollTo(scrollState.maxValue)
|
scrollState.animateScrollTo(scrollState.maxValue)
|
||||||
|
|
||||||
|
|||||||
@ -1,63 +0,0 @@
|
|||||||
package com.example.notesai.presentation.screens.ai.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import com.example.notesai.util.AppColors
|
|
||||||
import com.example.notesai.util.Constants
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun CompactStatItem(
|
|
||||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
|
||||||
value: String,
|
|
||||||
label: String
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
|
||||||
modifier = Modifier
|
|
||||||
.background(
|
|
||||||
color = AppColors.SurfaceVariant,
|
|
||||||
shape = RoundedCornerShape(8.dp)
|
|
||||||
)
|
|
||||||
.padding(horizontal = 12.dp, vertical = 8.dp)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
icon,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = AppColors.Primary,
|
|
||||||
modifier = Modifier.size(16.dp)
|
|
||||||
)
|
|
||||||
Column {
|
|
||||||
Text(
|
|
||||||
value,
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
color = AppColors.OnBackground,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
fontSize = 16.sp
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
label,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = AppColors.OnSurfaceTertiary,
|
|
||||||
fontSize = 11.sp
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
package com.example.notesai.presentation.screens.ai.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun StatItem(label: String, value: String, color: Color) {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
modifier = Modifier.padding(8.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
value,
|
|
||||||
style = MaterialTheme.typography.headlineMedium,
|
|
||||||
color = color,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
label,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = Color(0xFF94A3B8)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -32,7 +32,7 @@ fun CategoryCard(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onDelete: () -> Unit = {},
|
onDelete: () -> Unit = {},
|
||||||
onEdit: (String, Long, Long) -> Unit = { _, _, _ -> },
|
onEdit: (String, Long, Long) -> Unit = { _, _, _ -> },
|
||||||
onPin: () -> Unit = {} // NEW: Pin callback
|
onPin: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
var showDeleteConfirm by remember { mutableStateOf(false) }
|
var showDeleteConfirm by remember { mutableStateOf(false) }
|
||||||
var showEditDialog by remember { mutableStateOf(false) }
|
var showEditDialog by remember { mutableStateOf(false) }
|
||||||
@ -160,7 +160,7 @@ fun CategoryCard(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NEW: Pin Indicator & Menu Button
|
// Pin Indicator & Menu Button
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
@ -198,7 +198,7 @@ fun CategoryCard(
|
|||||||
onDismissRequest = { showMenu = false },
|
onDismissRequest = { showMenu = false },
|
||||||
modifier = Modifier.background(AppColors.SurfaceElevated)
|
modifier = Modifier.background(AppColors.SurfaceElevated)
|
||||||
) {
|
) {
|
||||||
// NEW: Pin/Unpin Menu Item
|
// Pin/Unpin Menu Item
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = {
|
text = {
|
||||||
Row(
|
Row(
|
||||||
@ -382,10 +382,11 @@ fun EditCategoryDialog(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
// 8 colors in 2 rows
|
||||||
Constants.CategoryColors.chunked(4).forEach { row ->
|
Constants.CategoryColors.chunked(4).forEach { row ->
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
) {
|
) {
|
||||||
row.forEachIndexed { _, gradient ->
|
row.forEachIndexed { _, gradient ->
|
||||||
val globalIndex = Constants.CategoryColors.indexOf(gradient)
|
val globalIndex = Constants.CategoryColors.indexOf(gradient)
|
||||||
@ -416,13 +417,13 @@ fun EditCategoryDialog(
|
|||||||
Icons.Default.Check,
|
Icons.Default.Check,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = Color.White,
|
tint = Color.White,
|
||||||
modifier = Modifier.size(24.dp)
|
modifier = Modifier.size(28.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -20,7 +20,7 @@ import androidx.compose.ui.focus.focusRequester
|
|||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.SolidColor // ✅ ADD
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.layout.onSizeChanged
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
import androidx.compose.ui.platform.*
|
import androidx.compose.ui.platform.*
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
@ -57,7 +57,6 @@ fun EditableFullScreenNoteView(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
@ -78,7 +77,7 @@ fun EditableFullScreenNoteView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔥 AUTO SAVE SAAT APP BACKGROUND / KELUAR
|
// AUTO SAVE SAAT APP BACKGROUND / KELUAR
|
||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
|
|
||||||
DisposableEffect(lifecycleOwner) {
|
DisposableEffect(lifecycleOwner) {
|
||||||
@ -95,7 +94,6 @@ fun EditableFullScreenNoteView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
val dateFormat = remember {
|
val dateFormat = remember {
|
||||||
SimpleDateFormat("dd MMMM yyyy, HH:mm", Locale("id", "ID"))
|
SimpleDateFormat("dd MMMM yyyy, HH:mm", Locale("id", "ID"))
|
||||||
}
|
}
|
||||||
@ -177,21 +175,32 @@ fun EditableFullScreenNoteView(
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
|
||||||
TextField(
|
// FIXED: BasicTextField untuk judul agar sejajar dengan konten
|
||||||
|
BasicTextField(
|
||||||
value = title,
|
value = title,
|
||||||
onValueChange = { title = it },
|
onValueChange = { title = it },
|
||||||
textStyle = MaterialTheme.typography.headlineLarge.copy(
|
textStyle = MaterialTheme.typography.headlineLarge.copy(
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = MaterialTheme.colorScheme.onBackground
|
color = MaterialTheme.colorScheme.onBackground
|
||||||
),
|
),
|
||||||
placeholder = { Text("Judul") },
|
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
colors = TextFieldDefaults.colors(
|
.fillMaxWidth()
|
||||||
focusedContainerColor = Color.Transparent,
|
.padding(vertical = 8.dp),
|
||||||
unfocusedContainerColor = Color.Transparent,
|
decorationBox = { innerTextField ->
|
||||||
focusedIndicatorColor = Color.Transparent,
|
Box {
|
||||||
unfocusedIndicatorColor = Color.Transparent
|
if (title.isEmpty()) {
|
||||||
)
|
Text(
|
||||||
|
"Judul",
|
||||||
|
style = MaterialTheme.typography.headlineLarge.copy(
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.4f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
innerTextField()
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(Modifier.height(12.dp))
|
Spacer(Modifier.height(12.dp))
|
||||||
@ -204,7 +213,7 @@ fun EditableFullScreenNoteView(
|
|||||||
|
|
||||||
HorizontalDivider(Modifier.padding(vertical = 20.dp))
|
HorizontalDivider(Modifier.padding(vertical = 20.dp))
|
||||||
|
|
||||||
// ✅ FIX UTAMA: set cursorBrush agar insertion cursor muncul
|
// Konten editor
|
||||||
BasicTextField(
|
BasicTextField(
|
||||||
value = editorState.value,
|
value = editorState.value,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
@ -213,7 +222,7 @@ fun EditableFullScreenNoteView(
|
|||||||
bringIntoViewRequester.bringIntoView()
|
bringIntoViewRequester.bringIntoView()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
cursorBrush = SolidColor(Color(0xFFA855F7)), // ✅ ADD THIS
|
cursorBrush = SolidColor(Color(0xFFA855F7)),
|
||||||
textStyle = MaterialTheme.typography.bodyLarge.copy(
|
textStyle = MaterialTheme.typography.bodyLarge.copy(
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
lineHeight = 28.sp
|
lineHeight = 28.sp
|
||||||
@ -285,9 +294,9 @@ fun EditableFullScreenNoteView(
|
|||||||
editorState.toggleItalic()
|
editorState.toggleItalic()
|
||||||
},
|
},
|
||||||
onUnderline = { ensureFocus(); editorState.toggleUnderline() },
|
onUnderline = { ensureFocus(); editorState.toggleUnderline() },
|
||||||
onHeading = { ensureFocus(); editorState.setHeading(1) }, // sementara H1
|
onHeading = { ensureFocus(); editorState.setHeading(1) },
|
||||||
onBullet = { ensureFocus(); editorState.toggleBulletList() }
|
onBullet = { ensureFocus(); editorState.toggleBulletList() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import com.example.notesai.util.AppColors
|
|
||||||
|
|
||||||
annotation class AppColors
|
|
||||||
@ -1,34 +1,77 @@
|
|||||||
package com.example.notesai.util
|
package com.example.notesai.util
|
||||||
|
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
object AppColors {
|
object AppColors {
|
||||||
|
// Theme state
|
||||||
|
private var isDarkTheme by mutableStateOf(true)
|
||||||
|
|
||||||
// Primary Colors
|
// Primary Colors
|
||||||
val Primary = Color(0xFF6C63FF)
|
val Primary: Color
|
||||||
val Secondary = Color(0xFF03DAC6)
|
get() = if (isDarkTheme) Color(0xFF6C63FF) else Color(0xFF5A52D5)
|
||||||
val Accent = Color(0xFFFF6B9D)
|
|
||||||
|
val Secondary: Color
|
||||||
|
get() = if (isDarkTheme) Color(0xFF03DAC6) else Color(0xFF018786)
|
||||||
|
|
||||||
|
val Accent: Color
|
||||||
|
get() = if (isDarkTheme) Color(0xFFFF6B9D) else Color(0xFFE91E63)
|
||||||
|
|
||||||
// Background Colors
|
// Background Colors
|
||||||
val Background = Color(0xFF121212)
|
val Background: Color
|
||||||
val Surface = Color(0xFF1E1E1E)
|
get() = if (isDarkTheme) Color(0xFF121212) else Color(0xFFFAFAFA)
|
||||||
val SurfaceVariant = Color(0xFF2A2A2A)
|
|
||||||
val SurfaceElevated = Color(0xFF252525)
|
val Surface: Color
|
||||||
|
get() = if (isDarkTheme) Color(0xFF1E1E1E) else Color(0xFFFFFFFF)
|
||||||
|
|
||||||
|
val SurfaceVariant: Color
|
||||||
|
get() = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5)
|
||||||
|
|
||||||
|
val SurfaceElevated: Color
|
||||||
|
get() = if (isDarkTheme) Color(0xFF252525) else Color(0xFFFFFFFF)
|
||||||
|
|
||||||
// Text Colors
|
// Text Colors
|
||||||
val OnBackground = Color(0xFFE1E1E1)
|
val OnBackground: Color
|
||||||
val OnSurface = Color(0xFFCCCCCC)
|
get() = if (isDarkTheme) Color(0xFFE1E1E1) else Color(0xFF1C1B1F)
|
||||||
val OnSurfaceVariant = Color(0xFF9E9E9E)
|
|
||||||
val OnSurfaceTertiary = Color(0xFF757575)
|
val OnSurface: Color
|
||||||
|
get() = if (isDarkTheme) Color(0xFFCCCCCC) else Color(0xFF1C1B1F)
|
||||||
|
|
||||||
|
val OnSurfaceVariant: Color
|
||||||
|
get() = if (isDarkTheme) Color(0xFF9E9E9E) else Color(0xFF49454F)
|
||||||
|
|
||||||
|
val OnSurfaceTertiary: Color
|
||||||
|
get() = if (isDarkTheme) Color(0xFF757575) else Color(0xFF79747E)
|
||||||
|
|
||||||
// Utility Colors
|
// Utility Colors
|
||||||
val Error = Color(0xFFCF6679)
|
val Error: Color
|
||||||
val Warning = Color(0xFFFFB74D)
|
get() = if (isDarkTheme) Color(0xFFCF6679) else Color(0xFFB3261E)
|
||||||
val Success = Color(0xFF81C784)
|
|
||||||
val Info = Color(0xFF64B5F6)
|
val Warning: Color
|
||||||
|
get() = if (isDarkTheme) Color(0xFFFFB74D) else Color(0xFFF57C00)
|
||||||
|
|
||||||
|
val Success: Color
|
||||||
|
get() = if (isDarkTheme) Color(0xFF81C784) else Color(0xFF388E3C)
|
||||||
|
|
||||||
|
val Info: Color
|
||||||
|
get() = if (isDarkTheme) Color(0xFF64B5F6) else Color(0xFF1976D2)
|
||||||
|
|
||||||
// Border & Divider
|
// Border & Divider
|
||||||
val Border = Color(0xFF3A3A3A)
|
val Border: Color
|
||||||
val Divider = Color(0xFF2E2E2E)
|
get() = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE0E0E0)
|
||||||
|
|
||||||
|
val Divider: Color
|
||||||
|
get() = if (isDarkTheme) Color(0xFF2E2E2E) else Color(0xFFE0E0E0)
|
||||||
|
|
||||||
|
// Function to update theme
|
||||||
|
fun setTheme(darkTheme: Boolean) {
|
||||||
|
isDarkTheme = darkTheme
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current theme state
|
||||||
|
fun isDark(): Boolean = isDarkTheme
|
||||||
}
|
}
|
||||||
|
|
||||||
object Constants {
|
object Constants {
|
||||||
@ -69,54 +112,30 @@ object Constants {
|
|||||||
// Reference to AppColors for compatibility
|
// Reference to AppColors for compatibility
|
||||||
val AppColors = com.example.notesai.util.AppColors
|
val AppColors = com.example.notesai.util.AppColors
|
||||||
|
|
||||||
// Category gradient colors
|
// Category gradient colors - 8 pilihan warna
|
||||||
val CategoryColors = listOf(
|
val CategoryColors = listOf(
|
||||||
// Purple gradients
|
// Purple gradient
|
||||||
0xFF6750A4L to 0xFF7E57C2L,
|
0xFF6750A4L to 0xFF7E57C2L,
|
||||||
|
|
||||||
|
// Pink gradient
|
||||||
0xFF9C27B0L to 0xFFE91E63L,
|
0xFF9C27B0L to 0xFFE91E63L,
|
||||||
|
|
||||||
// Blue gradients
|
// Blue gradient
|
||||||
0xFF2196F3L to 0xFF03A9F4L,
|
0xFF2196F3L to 0xFF03A9F4L,
|
||||||
0xFF1976D2L to 0xFF4FC3F7L,
|
|
||||||
|
|
||||||
// Green gradients
|
// Green gradient
|
||||||
0xFF4CAF50L to 0xFF8BC34AL,
|
0xFF4CAF50L to 0xFF8BC34AL,
|
||||||
0xFF009688L to 0xFF00BCD4L,
|
|
||||||
|
|
||||||
// Orange gradients
|
// Orange gradient
|
||||||
0xFFFF9800L to 0xFFFFB74DL,
|
0xFFFF9800L to 0xFFFFB74DL,
|
||||||
0xFFFF5722L to 0xFFFF7043L,
|
|
||||||
|
|
||||||
// Red gradients
|
// Red gradient
|
||||||
0xFFF44336L to 0xFFE91E63L,
|
0xFFF44336L to 0xFFE91E63L,
|
||||||
0xFFD32F2FL to 0xFFFF5252L,
|
|
||||||
|
|
||||||
// Teal gradients
|
// Teal gradient
|
||||||
0xFF009688L to 0xFF26A69AL,
|
0xFF009688L to 0xFF26A69AL,
|
||||||
0xFF00897BL to 0xFF4DB6ACL,
|
|
||||||
|
|
||||||
// Indigo gradients
|
// Indigo gradient
|
||||||
0xFF3F51B5L to 0xFF5C6BC0L,
|
0xFF3F51B5L to 0xFF5C6BC0L
|
||||||
0xFF303F9FL to 0xFF7986CBL,
|
|
||||||
|
|
||||||
// Amber gradients
|
|
||||||
0xFFFFC107L to 0xFFFFD54FL,
|
|
||||||
0xFFFFB300L to 0xFFFFCA28L,
|
|
||||||
|
|
||||||
// Pink gradients
|
|
||||||
0xFFE91E63L to 0xFFF06292L,
|
|
||||||
0xFFC2185BL to 0xFFEC407AL,
|
|
||||||
|
|
||||||
// Cyan gradients
|
|
||||||
0xFF00BCD4L to 0xFF26C6DAL,
|
|
||||||
0xFF0097A7L to 0xFF00ACC1L,
|
|
||||||
|
|
||||||
// Deep Purple gradients
|
|
||||||
0xFF673AB7L to 0xFF9575CDL,
|
|
||||||
0xFF512DA8L to 0xFF7E57C2L,
|
|
||||||
|
|
||||||
// Lime gradients
|
|
||||||
0xFFCDDC39L to 0xFFD4E157L,
|
|
||||||
0xFFAFB42BL to 0xFFC0CA33L
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user