2514 lines
94 KiB
Kotlin
2514 lines
94 KiB
Kotlin
package com.example.notesai
|
|
|
|
import android.os.Bundle
|
|
import androidx.activity.ComponentActivity
|
|
import androidx.activity.compose.setContent
|
|
import androidx.compose.animation.*
|
|
import androidx.compose.animation.core.*
|
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
import androidx.compose.foundation.background
|
|
import androidx.compose.foundation.clickable
|
|
import androidx.compose.foundation.combinedClickable
|
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
import androidx.compose.foundation.layout.*
|
|
import androidx.compose.foundation.lazy.LazyColumn
|
|
import androidx.compose.foundation.lazy.items
|
|
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
|
|
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
|
|
import androidx.compose.foundation.lazy.staggeredgrid.items
|
|
import androidx.compose.foundation.shape.CircleShape
|
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
import androidx.compose.material.icons.Icons
|
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
|
import androidx.compose.material.icons.filled.*
|
|
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.draw.shadow
|
|
import androidx.compose.ui.graphics.Brush
|
|
import androidx.compose.ui.graphics.Color
|
|
import androidx.compose.ui.graphics.vector.ImageVector
|
|
import androidx.compose.ui.text.font.FontWeight
|
|
import androidx.compose.ui.unit.dp
|
|
import androidx.compose.ui.unit.sp
|
|
import kotlinx.coroutines.launch
|
|
import java.text.SimpleDateFormat
|
|
import java.util.Date
|
|
import java.util.Locale
|
|
import java.util.UUID
|
|
import androidx.compose.material.icons.outlined.Star
|
|
import androidx.compose.foundation.rememberScrollState
|
|
import androidx.compose.foundation.verticalScroll
|
|
import androidx.compose.material3.HorizontalDivider as Divider
|
|
import androidx.compose.material.icons.filled.ArrowDropDown
|
|
import androidx.compose.material.icons.filled.Send
|
|
import androidx.compose.material3.DropdownMenu
|
|
import androidx.compose.material3.DropdownMenuItem
|
|
import com.google.ai.client.generativeai.GenerativeModel
|
|
import com.google.ai.client.generativeai.type.generationConfig
|
|
import androidx.compose.ui.platform.LocalClipboardManager
|
|
import androidx.compose.ui.text.AnnotatedString
|
|
import androidx.compose.material.icons.filled.ContentCopy
|
|
import androidx.compose.material.icons.outlined.StarBorder
|
|
import androidx.compose.ui.platform.LocalContext
|
|
import androidx.compose.ui.text.style.TextAlign
|
|
import androidx.compose.ui.text.style.TextOverflow
|
|
import androidx.compose.ui.zIndex
|
|
import kotlinx.coroutines.delay
|
|
|
|
// Data Classes
|
|
data class Category(
|
|
val id: String = UUID.randomUUID().toString(),
|
|
val name: String,
|
|
val gradientStart: Long,
|
|
val gradientEnd: Long,
|
|
val timestamp: Long = System.currentTimeMillis()
|
|
)
|
|
|
|
data class Note(
|
|
val id: String = UUID.randomUUID().toString(),
|
|
val categoryId: String,
|
|
val title: String,
|
|
val content: String,
|
|
val timestamp: Long = System.currentTimeMillis(),
|
|
val isArchived: Boolean = false,
|
|
val isDeleted: Boolean = false,
|
|
val isPinned: Boolean = false
|
|
)
|
|
|
|
data class ChatMessage(
|
|
val id: String = UUID.randomUUID().toString(),
|
|
val message: String,
|
|
val isUser: Boolean,
|
|
val timestamp: Long = System.currentTimeMillis()
|
|
)
|
|
|
|
class MainActivity : ComponentActivity() {
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
super.onCreate(savedInstanceState)
|
|
setContent {
|
|
MaterialTheme(
|
|
colorScheme = darkColorScheme(
|
|
primary = Color(0xFF6366F1),
|
|
secondary = Color(0xFFA855F7),
|
|
background = Color(0xFF0F172A),
|
|
surface = Color(0xFF1E293B),
|
|
onPrimary = Color.White,
|
|
onSecondary = Color.White,
|
|
onBackground = Color(0xFFE2E8F0),
|
|
onSurface = Color(0xFFE2E8F0)
|
|
)
|
|
) {
|
|
Surface(
|
|
modifier = Modifier.fillMaxSize(),
|
|
color = MaterialTheme.colorScheme.background
|
|
) {
|
|
NotesApp()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@OptIn(ExperimentalMaterial3Api::class)
|
|
@Composable
|
|
fun NotesApp() {
|
|
val context = LocalContext.current
|
|
val dataStoreManager = remember { DataStoreManager(context) }
|
|
val scope = rememberCoroutineScope()
|
|
|
|
var categories by remember { mutableStateOf(listOf<Category>()) }
|
|
var notes by remember { mutableStateOf(listOf<Note>()) }
|
|
var selectedCategory by remember { mutableStateOf<Category?>(null) }
|
|
var currentScreen by remember { mutableStateOf("main") }
|
|
var drawerState by remember { mutableStateOf(false) }
|
|
var showCategoryDialog by remember { mutableStateOf(false) }
|
|
var showNoteDialog by remember { mutableStateOf(false) }
|
|
var editingNote by remember { mutableStateOf<Note?>(null) }
|
|
var searchQuery by remember { mutableStateOf("") }
|
|
var showSearch by remember { mutableStateOf(false) }
|
|
var showFullScreenNote by remember { mutableStateOf(false) }
|
|
var fullScreenNote by remember { mutableStateOf<Note?>(null) }
|
|
|
|
// Load data dari DataStore
|
|
LaunchedEffect(Unit) {
|
|
try {
|
|
dataStoreManager.categoriesFlow.collect { loadedCategories ->
|
|
categories = loadedCategories
|
|
}
|
|
} catch (e: Exception) {
|
|
e.printStackTrace()
|
|
}
|
|
}
|
|
|
|
LaunchedEffect(Unit) {
|
|
try {
|
|
dataStoreManager.notesFlow.collect { loadedNotes ->
|
|
notes = loadedNotes
|
|
}
|
|
} catch (e: Exception) {
|
|
e.printStackTrace()
|
|
}
|
|
}
|
|
|
|
// Simpan categories dengan debounce
|
|
LaunchedEffect(categories.size) {
|
|
if (categories.isNotEmpty()) {
|
|
delay(500)
|
|
try {
|
|
dataStoreManager.saveCategories(categories)
|
|
} catch (e: Exception) {
|
|
e.printStackTrace()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Simpan notes dengan debounce
|
|
LaunchedEffect(notes.size) {
|
|
if (notes.isNotEmpty()) {
|
|
delay(500)
|
|
try {
|
|
dataStoreManager.saveNotes(notes)
|
|
} catch (e: Exception) {
|
|
e.printStackTrace()
|
|
}
|
|
}
|
|
}
|
|
|
|
Box(modifier = Modifier.fillMaxSize()) {
|
|
Scaffold(
|
|
topBar = {
|
|
if (!showFullScreenNote) {
|
|
ModernTopBar(
|
|
title = when(currentScreen) {
|
|
"main" -> if (selectedCategory != null) selectedCategory!!.name else "AI Notes"
|
|
"ai" -> "AI Helper"
|
|
"starred" -> "Berbintang"
|
|
"archive" -> "Arsip"
|
|
"trash" -> "Sampah"
|
|
else -> "AI Notes"
|
|
},
|
|
showBackButton = (selectedCategory != null && currentScreen == "main") || currentScreen == "ai" || currentScreen == "starred",
|
|
onBackClick = {
|
|
if (currentScreen == "ai" || currentScreen == "starred") {
|
|
currentScreen = "main"
|
|
} else {
|
|
selectedCategory = null
|
|
}
|
|
},
|
|
onMenuClick = { drawerState = !drawerState },
|
|
onSearchClick = { showSearch = !showSearch },
|
|
searchQuery = searchQuery,
|
|
onSearchQueryChange = { searchQuery = it },
|
|
showSearch = showSearch && currentScreen == "main"
|
|
)
|
|
}
|
|
},
|
|
floatingActionButton = {
|
|
AnimatedVisibility(
|
|
visible = currentScreen == "main" && !showFullScreenNote,
|
|
enter = scaleIn() + fadeIn(),
|
|
exit = scaleOut() + fadeOut()
|
|
) {
|
|
FloatingActionButton(
|
|
onClick = {
|
|
if (selectedCategory != null) {
|
|
editingNote = null
|
|
showNoteDialog = true
|
|
} else {
|
|
showCategoryDialog = true
|
|
}
|
|
},
|
|
containerColor = Color.Transparent,
|
|
modifier = Modifier
|
|
.shadow(8.dp, CircleShape)
|
|
.background(
|
|
brush = Brush.linearGradient(
|
|
colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
|
|
),
|
|
shape = CircleShape
|
|
)
|
|
) {
|
|
Icon(
|
|
Icons.Default.Add,
|
|
contentDescription = if (selectedCategory != null) "Tambah Catatan" else "Tambah Kategori",
|
|
tint = Color.White
|
|
)
|
|
}
|
|
}
|
|
},
|
|
bottomBar = {
|
|
if (!showFullScreenNote) {
|
|
ModernBottomBar(
|
|
currentScreen = currentScreen,
|
|
onHomeClick = {
|
|
currentScreen = "main"
|
|
selectedCategory = null
|
|
},
|
|
onAIClick = { currentScreen = "ai" }
|
|
)
|
|
}
|
|
}
|
|
) { padding ->
|
|
Box(modifier = Modifier.fillMaxSize()) {
|
|
if (showFullScreenNote && fullScreenNote != null) {
|
|
EditableFullScreenNoteView(
|
|
note = fullScreenNote!!,
|
|
onBack = {
|
|
showFullScreenNote = false
|
|
fullScreenNote = null
|
|
},
|
|
onSave = { title, content ->
|
|
notes = notes.map {
|
|
if (it.id == fullScreenNote!!.id) it.copy(
|
|
title = title,
|
|
content = content,
|
|
timestamp = System.currentTimeMillis()
|
|
)
|
|
else it
|
|
}
|
|
fullScreenNote = fullScreenNote!!.copy(title = title, content = content)
|
|
},
|
|
onArchive = {
|
|
notes = notes.map {
|
|
if (it.id == fullScreenNote!!.id) it.copy(isArchived = true)
|
|
else it
|
|
}
|
|
showFullScreenNote = false
|
|
fullScreenNote = null
|
|
},
|
|
onDelete = {
|
|
notes = notes.map {
|
|
if (it.id == fullScreenNote!!.id) it.copy(isDeleted = true)
|
|
else it
|
|
}
|
|
showFullScreenNote = false
|
|
fullScreenNote = null
|
|
},
|
|
onPinToggle = {
|
|
notes = notes.map {
|
|
if (it.id == fullScreenNote!!.id) it.copy(isPinned = !it.isPinned)
|
|
else it
|
|
}
|
|
fullScreenNote = fullScreenNote!!.copy(isPinned = !fullScreenNote!!.isPinned)
|
|
}
|
|
)
|
|
} else {
|
|
Column(
|
|
modifier = Modifier
|
|
.padding(padding)
|
|
.fillMaxSize()
|
|
) {
|
|
when (currentScreen) {
|
|
"main" -> MainScreen(
|
|
categories = categories,
|
|
notes = notes,
|
|
selectedCategory = selectedCategory,
|
|
searchQuery = searchQuery,
|
|
onCategoryClick = { selectedCategory = it },
|
|
onNoteClick = { note ->
|
|
fullScreenNote = note
|
|
showFullScreenNote = true
|
|
},
|
|
onPinToggle = { note ->
|
|
notes = notes.map {
|
|
if (it.id == note.id) it.copy(isPinned = !it.isPinned)
|
|
else it
|
|
}
|
|
},
|
|
onCategoryDelete = { category ->
|
|
// Delete kategori dan semua catatan di dalamnya
|
|
categories = categories.filter { it.id != category.id }
|
|
notes = notes.filter { it.categoryId != category.id }
|
|
selectedCategory = null
|
|
}
|
|
)
|
|
"starred" -> StarredNotesScreen(
|
|
notes = notes,
|
|
categories = categories,
|
|
onNoteClick = { note ->
|
|
fullScreenNote = note
|
|
showFullScreenNote = true
|
|
},
|
|
onMenuClick = { drawerState = true },
|
|
onBack = { currentScreen = "main" },
|
|
onUnpin = { note ->
|
|
notes = notes.map {
|
|
if (it.id == note.id) it.copy(isPinned = false)
|
|
else it
|
|
}
|
|
}
|
|
)
|
|
"ai" -> AIHelperScreen(
|
|
categories = categories,
|
|
notes = notes.filter { !it.isDeleted }
|
|
)
|
|
"archive" -> ArchiveScreen(
|
|
notes = notes.filter { it.isArchived && !it.isDeleted },
|
|
categories = categories,
|
|
onRestore = { note ->
|
|
notes = notes.map {
|
|
if (it.id == note.id) it.copy(isArchived = false)
|
|
else it
|
|
}
|
|
},
|
|
onDelete = { note ->
|
|
notes = notes.map {
|
|
if (it.id == note.id) it.copy(isDeleted = true)
|
|
else it
|
|
}
|
|
}
|
|
)
|
|
"trash" -> TrashScreen(
|
|
notes = notes.filter { it.isDeleted },
|
|
categories = categories,
|
|
onRestore = { note ->
|
|
notes = notes.map {
|
|
if (it.id == note.id) it.copy(isDeleted = false, isArchived = false)
|
|
else it
|
|
}
|
|
},
|
|
onDeletePermanent = { note ->
|
|
notes = notes.filter { it.id != note.id }
|
|
}
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Dialogs
|
|
if (showCategoryDialog) {
|
|
CategoryDialog(
|
|
onDismiss = { showCategoryDialog = false },
|
|
onSave = { name, gradientStart, gradientEnd ->
|
|
categories = categories + Category(
|
|
name = name,
|
|
gradientStart = gradientStart,
|
|
gradientEnd = gradientEnd
|
|
)
|
|
showCategoryDialog = false
|
|
}
|
|
)
|
|
}
|
|
|
|
if (showNoteDialog && selectedCategory != null) {
|
|
NoteDialog(
|
|
note = editingNote,
|
|
onDismiss = {
|
|
showNoteDialog = false
|
|
editingNote = null
|
|
},
|
|
onSave = { title, content ->
|
|
if (editingNote != null) {
|
|
notes = notes.map {
|
|
if (it.id == editingNote!!.id)
|
|
it.copy(
|
|
title = title,
|
|
content = content,
|
|
timestamp = System.currentTimeMillis()
|
|
)
|
|
else it
|
|
}
|
|
} else {
|
|
notes = notes + Note(
|
|
categoryId = selectedCategory!!.id,
|
|
title = title,
|
|
content = content
|
|
)
|
|
}
|
|
showNoteDialog = false
|
|
editingNote = null
|
|
},
|
|
onDelete = if (editingNote != null) {
|
|
{
|
|
notes = notes.map {
|
|
if (it.id == editingNote!!.id) it.copy(isDeleted = true)
|
|
else it
|
|
}
|
|
showNoteDialog = false
|
|
editingNote = null
|
|
}
|
|
} else null
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Drawer with Animation - DI LUAR SCAFFOLD agar di atas semua
|
|
AnimatedVisibility(
|
|
visible = drawerState,
|
|
enter = fadeIn() + slideInHorizontally(
|
|
initialOffsetX = { -it }
|
|
),
|
|
exit = fadeOut() + slideOutHorizontally(
|
|
targetOffsetX = { -it }
|
|
),
|
|
modifier = Modifier.zIndex(100f) // Z-index tinggi
|
|
) {
|
|
DrawerMenu(
|
|
currentScreen = currentScreen,
|
|
onDismiss = { drawerState = false },
|
|
onItemClick = { screen ->
|
|
currentScreen = screen
|
|
selectedCategory = null
|
|
drawerState = false
|
|
showSearch = false
|
|
searchQuery = ""
|
|
}
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
@OptIn(ExperimentalMaterial3Api::class)
|
|
@Composable
|
|
fun EditableFullScreenNoteView(
|
|
note: Note,
|
|
onBack: () -> Unit,
|
|
onSave: (String, String) -> Unit,
|
|
onArchive: () -> Unit,
|
|
onDelete: () -> Unit,
|
|
onPinToggle: () -> Unit
|
|
) {
|
|
var title by remember { mutableStateOf(note.title) }
|
|
var content by remember { mutableStateOf(note.content) }
|
|
var showArchiveDialog by remember { mutableStateOf(false) }
|
|
var showDeleteDialog by remember { mutableStateOf(false) }
|
|
val dateFormat = remember { SimpleDateFormat("dd MMMM yyyy, HH:mm", Locale("id", "ID")) }
|
|
|
|
// Dialog Konfirmasi Arsip
|
|
if (showArchiveDialog) {
|
|
AlertDialog(
|
|
onDismissRequest = { showArchiveDialog = false },
|
|
title = {
|
|
Text(
|
|
text = "Arsipkan Catatan?",
|
|
style = MaterialTheme.typography.titleLarge,
|
|
fontWeight = FontWeight.Bold
|
|
)
|
|
},
|
|
text = {
|
|
Text(
|
|
text = "Catatan ini akan dipindahkan ke arsip. Anda masih bisa mengaksesnya nanti.",
|
|
style = MaterialTheme.typography.bodyMedium
|
|
)
|
|
},
|
|
confirmButton = {
|
|
TextButton(
|
|
onClick = {
|
|
if (title.isNotBlank()) {
|
|
onSave(title, content)
|
|
}
|
|
onArchive()
|
|
showArchiveDialog = false
|
|
}
|
|
) {
|
|
Text("Arsipkan", color = MaterialTheme.colorScheme.primary)
|
|
}
|
|
},
|
|
dismissButton = {
|
|
TextButton(onClick = { showArchiveDialog = false }) {
|
|
Text("Batal", color = MaterialTheme.colorScheme.onSurface)
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
// Dialog Konfirmasi Hapus
|
|
if (showDeleteDialog) {
|
|
AlertDialog(
|
|
onDismissRequest = { showDeleteDialog = false },
|
|
title = {
|
|
Text(
|
|
text = "Hapus Catatan?",
|
|
style = MaterialTheme.typography.titleLarge,
|
|
fontWeight = FontWeight.Bold
|
|
)
|
|
},
|
|
text = {
|
|
Text(
|
|
text = "Catatan ini akan dihapus permanen dan tidak bisa dikembalikan.",
|
|
style = MaterialTheme.typography.bodyMedium
|
|
)
|
|
},
|
|
confirmButton = {
|
|
TextButton(
|
|
onClick = {
|
|
onDelete()
|
|
showDeleteDialog = false
|
|
}
|
|
) {
|
|
Text("Hapus", color = Color(0xFFEF4444))
|
|
}
|
|
},
|
|
dismissButton = {
|
|
TextButton(onClick = { showDeleteDialog = false }) {
|
|
Text("Batal", color = MaterialTheme.colorScheme.onSurface)
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
Scaffold(
|
|
containerColor = MaterialTheme.colorScheme.background,
|
|
topBar = {
|
|
TopAppBar(
|
|
title = { },
|
|
navigationIcon = {
|
|
IconButton(onClick = {
|
|
if (title.isNotBlank()) {
|
|
onSave(title, content)
|
|
}
|
|
onBack()
|
|
}) {
|
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Kembali", tint = MaterialTheme.colorScheme.onBackground)
|
|
}
|
|
},
|
|
actions = {
|
|
IconButton(onClick = {
|
|
if (title.isNotBlank()) {
|
|
onSave(title, content)
|
|
}
|
|
onPinToggle()
|
|
}) {
|
|
Icon(
|
|
if (note.isPinned) Icons.Filled.Star else Icons.Outlined.StarBorder,
|
|
contentDescription = "Pin Catatan",
|
|
tint = if (note.isPinned) Color(0xFFFBBF24) else MaterialTheme.colorScheme.onSurface
|
|
)
|
|
}
|
|
IconButton(onClick = { showArchiveDialog = true }) {
|
|
Icon(Icons.Default.Archive, contentDescription = "Arsipkan", tint = MaterialTheme.colorScheme.onSurface)
|
|
}
|
|
IconButton(onClick = { showDeleteDialog = true }) {
|
|
Icon(Icons.Default.Delete, contentDescription = "Hapus", tint = MaterialTheme.colorScheme.onSurface)
|
|
}
|
|
},
|
|
colors = TopAppBarDefaults.topAppBarColors(
|
|
containerColor = Color.Transparent
|
|
)
|
|
)
|
|
}
|
|
) { padding ->
|
|
Column(
|
|
modifier = Modifier
|
|
.fillMaxSize()
|
|
.padding(padding)
|
|
.verticalScroll(rememberScrollState())
|
|
.padding(horizontal = 20.dp)
|
|
) {
|
|
TextField(
|
|
value = title,
|
|
onValueChange = { title = it },
|
|
textStyle = MaterialTheme.typography.headlineLarge.copy(
|
|
fontWeight = FontWeight.Bold,
|
|
color = MaterialTheme.colorScheme.onBackground
|
|
),
|
|
placeholder = {
|
|
Text(
|
|
"Judul",
|
|
style = MaterialTheme.typography.headlineLarge,
|
|
color = Color(0xFF64748B)
|
|
)
|
|
},
|
|
colors = TextFieldDefaults.colors(
|
|
focusedContainerColor = Color.Transparent,
|
|
unfocusedContainerColor = Color.Transparent,
|
|
focusedIndicatorColor = Color.Transparent,
|
|
unfocusedIndicatorColor = Color.Transparent,
|
|
cursorColor = Color(0xFFA855F7)
|
|
),
|
|
modifier = Modifier.fillMaxWidth()
|
|
)
|
|
|
|
Spacer(modifier = Modifier.height(12.dp))
|
|
|
|
Text(
|
|
text = "Terakhir diubah: ${dateFormat.format(Date(note.timestamp))}",
|
|
style = MaterialTheme.typography.bodySmall,
|
|
color = Color(0xFF64748B)
|
|
)
|
|
|
|
Divider(
|
|
modifier = Modifier.padding(vertical = 20.dp),
|
|
color = MaterialTheme.colorScheme.surface
|
|
)
|
|
|
|
TextField(
|
|
value = content,
|
|
onValueChange = { content = it },
|
|
textStyle = MaterialTheme.typography.bodyLarge.copy(
|
|
color = MaterialTheme.colorScheme.onSurface,
|
|
lineHeight = 28.sp
|
|
),
|
|
placeholder = {
|
|
Text(
|
|
"Mulai menulis...",
|
|
style = MaterialTheme.typography.bodyLarge,
|
|
color = Color(0xFF64748B)
|
|
)
|
|
},
|
|
colors = TextFieldDefaults.colors(
|
|
focusedContainerColor = Color.Transparent,
|
|
unfocusedContainerColor = Color.Transparent,
|
|
focusedIndicatorColor = Color.Transparent,
|
|
unfocusedIndicatorColor = Color.Transparent,
|
|
cursorColor = Color(0xFFA855F7)
|
|
),
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.heightIn(min = 400.dp)
|
|
)
|
|
|
|
Spacer(modifier = Modifier.height(100.dp))
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
@OptIn(ExperimentalMaterial3Api::class)
|
|
@Composable
|
|
fun ModernTopBar(
|
|
title: String,
|
|
showBackButton: Boolean,
|
|
onBackClick: () -> Unit,
|
|
onMenuClick: () -> Unit,
|
|
onSearchClick: () -> Unit,
|
|
searchQuery: String,
|
|
onSearchQueryChange: (String) -> Unit,
|
|
showSearch: Boolean
|
|
) {
|
|
TopAppBar(
|
|
title = {
|
|
if (showSearch) {
|
|
TextField(
|
|
value = searchQuery,
|
|
onValueChange = onSearchQueryChange,
|
|
placeholder = { Text("Cari catatan...", color = Color.White.copy(0.6f)) },
|
|
colors = TextFieldDefaults.colors(
|
|
focusedContainerColor = Color.Transparent,
|
|
unfocusedContainerColor = Color.Transparent,
|
|
focusedTextColor = Color.White,
|
|
unfocusedTextColor = Color.White,
|
|
cursorColor = Color.White,
|
|
focusedIndicatorColor = Color.Transparent,
|
|
unfocusedIndicatorColor = Color.Transparent
|
|
),
|
|
modifier = Modifier.fillMaxWidth()
|
|
)
|
|
} else {
|
|
Text(
|
|
title,
|
|
fontWeight = FontWeight.Bold,
|
|
fontSize = 22.sp
|
|
)
|
|
}
|
|
},
|
|
navigationIcon = {
|
|
IconButton(onClick = if (showBackButton) onBackClick else onMenuClick) {
|
|
Icon(
|
|
if (showBackButton) Icons.AutoMirrored.Filled.ArrowBack else Icons.Default.Menu,
|
|
contentDescription = null,
|
|
tint = Color.White
|
|
)
|
|
}
|
|
},
|
|
actions = {
|
|
IconButton(onClick = onSearchClick) {
|
|
Icon(
|
|
if (showSearch) Icons.Default.Close else Icons.Default.Search,
|
|
contentDescription = "Search",
|
|
tint = Color.White
|
|
)
|
|
}
|
|
},
|
|
colors = TopAppBarDefaults.topAppBarColors(
|
|
containerColor = Color.Transparent
|
|
),
|
|
modifier = Modifier
|
|
.background(
|
|
brush = Brush.horizontalGradient(
|
|
colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
|
|
)
|
|
)
|
|
)
|
|
}
|
|
|
|
@Composable
|
|
fun ModernBottomBar(
|
|
currentScreen: String,
|
|
onHomeClick: () -> Unit,
|
|
onAIClick: () -> Unit
|
|
) {
|
|
BottomAppBar(
|
|
containerColor = Color.Transparent,
|
|
modifier = Modifier
|
|
.shadow(8.dp, RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp))
|
|
.background(
|
|
brush = Brush.verticalGradient(
|
|
colors = listOf(
|
|
Color(0xFF1E293B).copy(0.95f),
|
|
Color(0xFF334155).copy(0.95f)
|
|
)
|
|
),
|
|
shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp)
|
|
)
|
|
) {
|
|
Row(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(horizontal = 16.dp),
|
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
Column(
|
|
horizontalAlignment = Alignment.CenterHorizontally,
|
|
modifier = Modifier
|
|
.weight(1f)
|
|
.clickable(onClick = onHomeClick)
|
|
.padding(vertical = 8.dp)
|
|
) {
|
|
Icon(
|
|
Icons.Default.Home,
|
|
contentDescription = "Home",
|
|
tint = if (currentScreen == "main") Color(0xFF6366F1) else Color(0xFF94A3B8),
|
|
modifier = Modifier.size(24.dp)
|
|
)
|
|
Spacer(modifier = Modifier.height(4.dp))
|
|
Text(
|
|
"Home",
|
|
color = if (currentScreen == "main") Color(0xFF6366F1) else Color(0xFF94A3B8),
|
|
style = MaterialTheme.typography.bodySmall,
|
|
fontWeight = if (currentScreen == "main") FontWeight.Bold else FontWeight.Normal
|
|
)
|
|
}
|
|
|
|
Column(
|
|
horizontalAlignment = Alignment.CenterHorizontally,
|
|
modifier = Modifier
|
|
.weight(1f)
|
|
.clickable(onClick = onAIClick)
|
|
.padding(vertical = 8.dp)
|
|
) {
|
|
Icon(
|
|
Icons.Default.Star,
|
|
contentDescription = "AI Helper",
|
|
tint = if (currentScreen == "ai") Color(0xFFFBBF24) else Color(0xFF94A3B8),
|
|
modifier = Modifier.size(24.dp)
|
|
)
|
|
Spacer(modifier = Modifier.height(4.dp))
|
|
Text(
|
|
"AI Helper",
|
|
color = if (currentScreen == "ai") Color(0xFFFBBF24) else Color(0xFF94A3B8),
|
|
style = MaterialTheme.typography.bodySmall,
|
|
fontWeight = if (currentScreen == "ai") FontWeight.Bold else FontWeight.Normal
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@OptIn(ExperimentalFoundationApi::class)
|
|
@Composable
|
|
fun MainScreen(
|
|
categories: List<Category>,
|
|
notes: List<Note>,
|
|
selectedCategory: Category?,
|
|
searchQuery: String,
|
|
onCategoryClick: (Category) -> Unit,
|
|
onNoteClick: (Note) -> Unit,
|
|
onPinToggle: (Note) -> Unit,
|
|
onCategoryDelete: (Category) -> Unit
|
|
) {
|
|
Column(modifier = Modifier.fillMaxSize()) {
|
|
if (selectedCategory == null) {
|
|
// Beranda: Tampilkan kategori dengan search filtering
|
|
if (categories.isEmpty()) {
|
|
EmptyState(
|
|
icon = Icons.Default.Create,
|
|
message = "Buat kategori pertama Anda",
|
|
subtitle = "Tekan tombol + untuk memulai"
|
|
)
|
|
} else {
|
|
// Filter kategori berdasarkan searchQuery
|
|
val filteredCategories = if (searchQuery.isEmpty()) {
|
|
categories
|
|
} else {
|
|
categories.filter {
|
|
it.name.contains(searchQuery, ignoreCase = true)
|
|
}
|
|
}
|
|
|
|
if (filteredCategories.isEmpty()) {
|
|
EmptyState(
|
|
icon = Icons.Default.Search,
|
|
message = "Kategori tidak ditemukan",
|
|
subtitle = "Coba kata kunci lain"
|
|
)
|
|
} else {
|
|
LazyVerticalStaggeredGrid(
|
|
columns = StaggeredGridCells.Fixed(2),
|
|
contentPadding = PaddingValues(16.dp),
|
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
|
verticalItemSpacing = 12.dp,
|
|
modifier = Modifier.fillMaxSize()
|
|
) {
|
|
items(filteredCategories) { category ->
|
|
CategoryCard(
|
|
category = category,
|
|
noteCount = notes.count { it.categoryId == category.id && !it.isDeleted && !it.isArchived },
|
|
onClick = { onCategoryClick(category) },
|
|
onDelete = {
|
|
onCategoryDelete(category)
|
|
}
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
val categoryNotes = notes
|
|
.filter {
|
|
it.categoryId == selectedCategory.id &&
|
|
!it.isDeleted &&
|
|
!it.isArchived &&
|
|
(searchQuery.isEmpty() ||
|
|
it.title.contains(searchQuery, ignoreCase = true) ||
|
|
it.content.contains(searchQuery, ignoreCase = true))
|
|
}
|
|
.sortedWith(compareByDescending<Note> { it.isPinned }.thenByDescending { it.timestamp })
|
|
|
|
if (categoryNotes.isEmpty()) {
|
|
EmptyState(
|
|
icon = Icons.Default.Add,
|
|
message = if (searchQuery.isEmpty()) "Belum ada catatan" else "Tidak ada hasil",
|
|
subtitle = if (searchQuery.isEmpty()) "Tekan tombol + untuk membuat catatan" else "Coba kata kunci lain"
|
|
)
|
|
} else {
|
|
LazyVerticalStaggeredGrid(
|
|
columns = StaggeredGridCells.Fixed(2),
|
|
contentPadding = PaddingValues(16.dp),
|
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
|
verticalItemSpacing = 12.dp,
|
|
modifier = Modifier.fillMaxSize()
|
|
) {
|
|
items(categoryNotes) { note ->
|
|
NoteCard(
|
|
note = note,
|
|
onClick = { onNoteClick(note) },
|
|
onPinClick = { onPinToggle(note) },
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@OptIn(ExperimentalFoundationApi::class)
|
|
@Composable
|
|
fun CategoryCard(
|
|
category: Category,
|
|
noteCount: Int,
|
|
onClick: () -> Unit,
|
|
onDelete: () -> Unit = {}
|
|
) {
|
|
var showDeleteConfirm by remember { mutableStateOf(false) }
|
|
|
|
// Delete confirmation dialog
|
|
if (showDeleteConfirm) {
|
|
AlertDialog(
|
|
onDismissRequest = { showDeleteConfirm = false },
|
|
title = { Text("Hapus Kategori?", color = Color.White) },
|
|
text = {
|
|
Text("Kategori '${category.name}' dan semua catatan di dalamnya akan dihapus. Tindakan ini tidak dapat dibatalkan.", color = Color.White)
|
|
},
|
|
confirmButton = {
|
|
Button(
|
|
onClick = {
|
|
onDelete()
|
|
showDeleteConfirm = false
|
|
},
|
|
colors = ButtonDefaults.buttonColors(
|
|
containerColor = Color(0xFFEF4444)
|
|
)
|
|
) {
|
|
Text("Hapus", color = Color.White)
|
|
}
|
|
},
|
|
dismissButton = {
|
|
Button(
|
|
onClick = { showDeleteConfirm = false },
|
|
colors = ButtonDefaults.buttonColors(
|
|
containerColor = Color(0xFF64748B)
|
|
)
|
|
) {
|
|
Text("Batal", color = Color.White)
|
|
}
|
|
},
|
|
containerColor = Color(0xFF1E293B)
|
|
)
|
|
}
|
|
|
|
Card(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.clickable(onClick = onClick),
|
|
shape = RoundedCornerShape(20.dp),
|
|
colors = CardDefaults.cardColors(containerColor = Color.Transparent),
|
|
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
|
|
) {
|
|
Box(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.background(
|
|
brush = Brush.linearGradient(
|
|
colors = listOf(
|
|
Color(category.gradientStart),
|
|
Color(category.gradientEnd)
|
|
)
|
|
)
|
|
)
|
|
.padding(20.dp)
|
|
) {
|
|
Column {
|
|
Icon(
|
|
Icons.Default.Folder,
|
|
contentDescription = null,
|
|
tint = Color.White.copy(0.9f),
|
|
modifier = Modifier.size(32.dp)
|
|
)
|
|
Spacer(modifier = Modifier.height(12.dp))
|
|
Text(
|
|
category.name,
|
|
style = MaterialTheme.typography.titleMedium,
|
|
fontWeight = FontWeight.Bold,
|
|
color = Color.White
|
|
)
|
|
Spacer(modifier = Modifier.height(4.dp))
|
|
Text(
|
|
"$noteCount catatan",
|
|
style = MaterialTheme.typography.bodySmall,
|
|
color = Color.White.copy(0.8f)
|
|
)
|
|
}
|
|
|
|
// Delete button di top-right corner
|
|
IconButton(
|
|
onClick = {
|
|
showDeleteConfirm = true
|
|
},
|
|
modifier = Modifier
|
|
.align(Alignment.TopEnd)
|
|
.size(40.dp)
|
|
) {
|
|
Icon(
|
|
Icons.Default.Close,
|
|
contentDescription = "Hapus kategori",
|
|
tint = Color.White.copy(0.7f),
|
|
modifier = Modifier.size(20.dp)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@OptIn(ExperimentalFoundationApi::class)
|
|
@Composable
|
|
fun NoteCard(
|
|
note: Note,
|
|
onClick: () -> Unit,
|
|
onPinClick: () -> Unit
|
|
) {
|
|
val dateFormat = SimpleDateFormat("dd MMM, HH:mm", Locale("id", "ID"))
|
|
|
|
Card(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.combinedClickable(
|
|
onClick = onClick,
|
|
),
|
|
shape = RoundedCornerShape(16.dp),
|
|
colors = CardDefaults.cardColors(
|
|
containerColor = Color(0xFF1E293B)
|
|
),
|
|
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
|
) {
|
|
Column(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(16.dp)
|
|
) {
|
|
Row(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
horizontalArrangement = Arrangement.SpaceBetween,
|
|
verticalAlignment = Alignment.Top
|
|
) {
|
|
// Judul
|
|
Text(
|
|
note.title,
|
|
style = MaterialTheme.typography.titleLarge,
|
|
fontWeight = FontWeight.Bold,
|
|
color = Color.White,
|
|
modifier = Modifier.weight(1f),
|
|
maxLines = 2,
|
|
overflow = TextOverflow.Ellipsis
|
|
)
|
|
IconButton(
|
|
onClick = onPinClick,
|
|
modifier = Modifier.size(24.dp)
|
|
) {
|
|
Icon(
|
|
if (note.isPinned) Icons.Filled.Star else Icons.Outlined.StarBorder,
|
|
contentDescription = "Pin",
|
|
tint = if (note.isPinned) Color(0xFFFBBF24) else Color.Gray,
|
|
modifier = Modifier.size(18.dp)
|
|
)
|
|
}
|
|
}
|
|
|
|
// Deskripsi
|
|
if (note.content.isNotEmpty()) {
|
|
Spacer(modifier = Modifier.height(12.dp))
|
|
Text(
|
|
text = "Deskripsi",
|
|
style = MaterialTheme.typography.labelSmall,
|
|
color = Color(0xFF94A3B8),
|
|
fontWeight = FontWeight.SemiBold
|
|
)
|
|
Spacer(modifier = Modifier.height(4.dp))
|
|
Text(
|
|
note.content,
|
|
style = MaterialTheme.typography.bodyMedium,
|
|
maxLines = 4,
|
|
overflow = TextOverflow.Ellipsis,
|
|
color = Color(0xFFCBD5E1),
|
|
lineHeight = 20.sp
|
|
)
|
|
}
|
|
|
|
Spacer(modifier = Modifier.height(12.dp))
|
|
|
|
Divider(
|
|
color = Color(0xFF334155),
|
|
thickness = 1.dp
|
|
)
|
|
|
|
Spacer(modifier = Modifier.height(8.dp))
|
|
|
|
// Timestamp
|
|
Text(
|
|
dateFormat.format(Date(note.timestamp)),
|
|
style = MaterialTheme.typography.bodySmall,
|
|
color = Color(0xFF64748B)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun DrawerMenu(
|
|
currentScreen: String,
|
|
onDismiss: () -> Unit,
|
|
onItemClick: (String) -> Unit
|
|
) {
|
|
Box(
|
|
modifier = Modifier
|
|
.fillMaxSize()
|
|
.statusBarsPadding() // Padding untuk status bar
|
|
.background(Color.Black.copy(alpha = 0.5f))
|
|
.clickable(
|
|
onClick = onDismiss,
|
|
indication = null,
|
|
interactionSource = remember { MutableInteractionSource() }
|
|
)
|
|
) {
|
|
Card(
|
|
modifier = Modifier
|
|
.fillMaxHeight()
|
|
.width(250.dp)
|
|
.align(Alignment.CenterStart)
|
|
.clickable(
|
|
onClick = {},
|
|
indication = null,
|
|
interactionSource = remember { MutableInteractionSource() }
|
|
),
|
|
shape = RoundedCornerShape(topEnd = 0.dp, bottomEnd = 0.dp),
|
|
colors = CardDefaults.cardColors(
|
|
containerColor = Color(0xFF1E293B)
|
|
),
|
|
elevation = CardDefaults.cardElevation(defaultElevation = 16.dp)
|
|
) {
|
|
Column(modifier = Modifier.fillMaxSize()) {
|
|
// Header Drawer dengan tombol close
|
|
Box(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.background(
|
|
brush = Brush.verticalGradient(
|
|
colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
|
|
)
|
|
)
|
|
.padding(24.dp)
|
|
) {
|
|
Row(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
horizontalArrangement = Arrangement.SpaceBetween,
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
Column {
|
|
Icon(
|
|
Icons.Default.Create,
|
|
contentDescription = null,
|
|
tint = Color.White,
|
|
modifier = Modifier.size(36.dp)
|
|
)
|
|
Spacer(modifier = Modifier.height(12.dp))
|
|
Text(
|
|
"AI Notes",
|
|
style = MaterialTheme.typography.headlineMedium,
|
|
color = Color.White,
|
|
fontWeight = FontWeight.Bold
|
|
)
|
|
Text(
|
|
"Smart & Modern",
|
|
style = MaterialTheme.typography.bodyMedium,
|
|
color = Color.White.copy(0.8f)
|
|
)
|
|
}
|
|
|
|
// // Tombol Close
|
|
// IconButton(
|
|
// onClick = onDismiss,
|
|
// modifier = Modifier
|
|
// .size(40.dp)
|
|
// .background(
|
|
// Color.White.copy(alpha = 0.2f),
|
|
// shape = CircleShape
|
|
// )
|
|
// ) {
|
|
// Icon(
|
|
// Icons.Default.Close,
|
|
// contentDescription = "Tutup Menu",
|
|
// tint = Color.White,
|
|
// modifier = Modifier.size(24.dp)
|
|
// )
|
|
// }
|
|
}
|
|
}
|
|
|
|
Spacer(modifier = Modifier.height(16.dp))
|
|
|
|
// Menu Items
|
|
MenuItem(
|
|
icon = Icons.Default.Home,
|
|
text = "Beranda",
|
|
isSelected = currentScreen == "main"
|
|
) { onItemClick("main") }
|
|
|
|
MenuItem(
|
|
icon = Icons.Default.Star,
|
|
text = "Berbintang",
|
|
isSelected = currentScreen == "starred"
|
|
) { onItemClick("starred") }
|
|
|
|
MenuItem(
|
|
icon = Icons.Default.Archive,
|
|
text = "Arsip",
|
|
isSelected = currentScreen == "archive"
|
|
) { onItemClick("archive") }
|
|
|
|
MenuItem(
|
|
icon = Icons.Default.Delete,
|
|
text = "Sampah",
|
|
isSelected = currentScreen == "trash"
|
|
) { onItemClick("trash") }
|
|
|
|
Spacer(modifier = Modifier.weight(1f))
|
|
|
|
// Footer
|
|
Divider(
|
|
color = Color.White.copy(alpha = 0.1f),
|
|
modifier = Modifier.padding(horizontal = 16.dp)
|
|
)
|
|
|
|
Text(
|
|
text = "Version 1.0.0",
|
|
style = MaterialTheme.typography.bodySmall,
|
|
color = Color.White.copy(alpha = 0.5f),
|
|
modifier = Modifier.padding(16.dp)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun MenuItem(
|
|
icon: ImageVector,
|
|
text: String,
|
|
isSelected: Boolean,
|
|
onClick: () -> Unit
|
|
) {
|
|
Row(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.clickable(onClick = onClick)
|
|
.background(
|
|
if (isSelected) Color(0xFF334155) else Color.Transparent
|
|
)
|
|
.padding(horizontal = 20.dp, vertical = 16.dp),
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
Icon(
|
|
icon,
|
|
contentDescription = null,
|
|
tint = if (isSelected) Color(0xFFA855F7) else Color(0xFF94A3B8)
|
|
)
|
|
Spacer(modifier = Modifier.width(16.dp))
|
|
Text(
|
|
text,
|
|
style = MaterialTheme.typography.bodyLarge,
|
|
color = if (isSelected) Color.White else Color(0xFF94A3B8),
|
|
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal
|
|
)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun CategoryDialog(
|
|
onDismiss: () -> Unit,
|
|
onSave: (String, Long, Long) -> Unit
|
|
) {
|
|
var name by remember { mutableStateOf("") }
|
|
var selectedGradient by remember { mutableStateOf(0) }
|
|
|
|
val gradients = listOf(
|
|
Pair(0xFF6366F1L, 0xFFA855F7L),
|
|
Pair(0xFFEC4899L, 0xFFF59E0BL),
|
|
Pair(0xFF8B5CF6L, 0xFFEC4899L),
|
|
Pair(0xFF06B6D4L, 0xFF3B82F6L),
|
|
Pair(0xFF10B981L, 0xFF059669L),
|
|
Pair(0xFFF59E0BL, 0xFFEF4444L),
|
|
Pair(0xFF6366F1L, 0xFF8B5CF6L),
|
|
Pair(0xFFEF4444L, 0xFFDC2626L)
|
|
)
|
|
|
|
AlertDialog(
|
|
onDismissRequest = onDismiss,
|
|
containerColor = Color(0xFF1E293B),
|
|
title = {
|
|
Text(
|
|
"Buat Kategori Baru",
|
|
color = Color.White,
|
|
fontWeight = FontWeight.Bold
|
|
)
|
|
},
|
|
text = {
|
|
Column {
|
|
OutlinedTextField(
|
|
value = name,
|
|
onValueChange = { name = it },
|
|
label = { Text("Nama Kategori", color = Color(0xFF94A3B8)) },
|
|
modifier = Modifier.fillMaxWidth(),
|
|
colors = TextFieldDefaults.colors(
|
|
focusedTextColor = Color.White,
|
|
unfocusedTextColor = Color.White,
|
|
focusedContainerColor = Color(0xFF334155),
|
|
unfocusedContainerColor = Color(0xFF334155),
|
|
cursorColor = Color(0xFFA855F7),
|
|
focusedIndicatorColor = Color(0xFFA855F7),
|
|
unfocusedIndicatorColor = Color(0xFF64748B)
|
|
),
|
|
shape = RoundedCornerShape(12.dp)
|
|
)
|
|
|
|
Spacer(modifier = Modifier.height(20.dp))
|
|
Text(
|
|
"Pilih Gradient:",
|
|
style = MaterialTheme.typography.bodyMedium,
|
|
color = Color.White,
|
|
fontWeight = FontWeight.SemiBold
|
|
)
|
|
|
|
Spacer(modifier = Modifier.height(12.dp))
|
|
|
|
gradients.chunked(4).forEach { row ->
|
|
Row(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
) {
|
|
row.forEachIndexed { index, gradient ->
|
|
val globalIndex = gradients.indexOf(gradient)
|
|
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
|
|
) {
|
|
if (selectedGradient == globalIndex) {
|
|
Icon(
|
|
Icons.Default.Check,
|
|
contentDescription = null,
|
|
tint = Color.White,
|
|
modifier = Modifier.size(24.dp)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Spacer(modifier = Modifier.height(8.dp))
|
|
}
|
|
}
|
|
},
|
|
confirmButton = {
|
|
Button(
|
|
onClick = {
|
|
if (name.isNotBlank()) {
|
|
val gradient = gradients[selectedGradient]
|
|
onSave(name, gradient.first, gradient.second)
|
|
}
|
|
},
|
|
enabled = name.isNotBlank(),
|
|
colors = ButtonDefaults.buttonColors(
|
|
containerColor = Color.Transparent
|
|
),
|
|
modifier = Modifier.background(
|
|
brush = Brush.linearGradient(
|
|
colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
|
|
),
|
|
shape = RoundedCornerShape(8.dp)
|
|
)
|
|
) {
|
|
Text("Simpan", color = Color.White, fontWeight = FontWeight.Bold)
|
|
}
|
|
},
|
|
dismissButton = {
|
|
TextButton(onClick = onDismiss) {
|
|
Text("Batal", color = Color(0xFF94A3B8))
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
@Composable
|
|
fun NoteDialog(
|
|
note: Note?,
|
|
onDismiss: () -> Unit,
|
|
onSave: (String, String) -> Unit,
|
|
onDelete: (() -> Unit)?
|
|
) {
|
|
var title by remember { mutableStateOf(note?.title ?: "") }
|
|
var content by remember { mutableStateOf(note?.content ?: "") }
|
|
|
|
AlertDialog(
|
|
onDismissRequest = onDismiss,
|
|
containerColor = Color(0xFF1E293B),
|
|
title = {
|
|
Text(
|
|
if (note == null) "Catatan Baru" else "Edit Catatan",
|
|
color = Color.White,
|
|
fontWeight = FontWeight.Bold
|
|
)
|
|
},
|
|
text = {
|
|
Column {
|
|
OutlinedTextField(
|
|
value = title,
|
|
onValueChange = { title = it },
|
|
label = { Text("Judul", color = Color(0xFF94A3B8)) },
|
|
modifier = Modifier.fillMaxWidth(),
|
|
colors = TextFieldDefaults.colors(
|
|
focusedTextColor = Color.White,
|
|
unfocusedTextColor = Color.White,
|
|
focusedContainerColor = Color(0xFF334155),
|
|
unfocusedContainerColor = Color(0xFF334155),
|
|
cursorColor = Color(0xFFA855F7),
|
|
focusedIndicatorColor = Color(0xFFA855F7),
|
|
unfocusedIndicatorColor = Color(0xFF64748B)
|
|
),
|
|
shape = RoundedCornerShape(12.dp)
|
|
)
|
|
|
|
Spacer(modifier = Modifier.height(12.dp))
|
|
|
|
OutlinedTextField(
|
|
value = content,
|
|
onValueChange = { content = it },
|
|
label = { Text("Isi Catatan", color = Color(0xFF94A3B8)) },
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.height(200.dp),
|
|
maxLines = 10,
|
|
colors = TextFieldDefaults.colors(
|
|
focusedTextColor = Color.White,
|
|
unfocusedTextColor = Color.White,
|
|
focusedContainerColor = Color(0xFF334155),
|
|
unfocusedContainerColor = Color(0xFF334155),
|
|
cursorColor = Color(0xFFA855F7),
|
|
focusedIndicatorColor = Color(0xFFA855F7),
|
|
unfocusedIndicatorColor = Color(0xFF64748B)
|
|
),
|
|
shape = RoundedCornerShape(12.dp)
|
|
)
|
|
}
|
|
},
|
|
confirmButton = {
|
|
Row {
|
|
if (onDelete != null) {
|
|
TextButton(onClick = onDelete) {
|
|
Text("Hapus", color = Color(0xFFEF4444), fontWeight = FontWeight.Bold)
|
|
}
|
|
Spacer(modifier = Modifier.width(8.dp))
|
|
}
|
|
Button(
|
|
onClick = { if (title.isNotBlank()) onSave(title, content) },
|
|
enabled = title.isNotBlank(),
|
|
colors = ButtonDefaults.buttonColors(
|
|
containerColor = Color.Transparent
|
|
),
|
|
modifier = Modifier.background(
|
|
brush = Brush.linearGradient(
|
|
colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
|
|
),
|
|
shape = RoundedCornerShape(8.dp)
|
|
)
|
|
) {
|
|
Text("Simpan", color = Color.White, fontWeight = FontWeight.Bold)
|
|
}
|
|
}
|
|
},
|
|
dismissButton = {
|
|
TextButton(onClick = onDismiss) {
|
|
Text("Batal", color = Color(0xFF94A3B8))
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
@OptIn(ExperimentalMaterial3Api::class)
|
|
@Composable
|
|
fun AIHelperScreen(
|
|
categories: List<Category>,
|
|
notes: List<Note>
|
|
) {
|
|
var prompt by remember { mutableStateOf("") }
|
|
var isLoading by remember { mutableStateOf(false) }
|
|
var selectedCategory by remember { mutableStateOf<Category?>(null) }
|
|
var showCategoryDropdown by remember { mutableStateOf(false) }
|
|
var errorMessage by remember { mutableStateOf("") }
|
|
var chatMessages by remember { mutableStateOf(listOf<ChatMessage>()) }
|
|
var showCopiedMessage by remember { mutableStateOf(false) }
|
|
var copiedMessageId by remember { mutableStateOf("") }
|
|
|
|
val scope = rememberCoroutineScope()
|
|
val clipboardManager = LocalClipboardManager.current
|
|
val scrollState = rememberScrollState()
|
|
|
|
// Inisialisasi Gemini Model
|
|
val generativeModel = remember {
|
|
GenerativeModel(
|
|
modelName = "gemini-2.5-flash",
|
|
apiKey = APIKey.GEMINI_API_KEY,
|
|
generationConfig = generationConfig {
|
|
temperature = 0.8f
|
|
topK = 40
|
|
topP = 0.95f
|
|
maxOutputTokens = 4096
|
|
candidateCount = 1
|
|
}
|
|
)
|
|
}
|
|
|
|
// Auto scroll ke bawah saat ada pesan baru
|
|
LaunchedEffect(chatMessages.size) {
|
|
if (chatMessages.isNotEmpty()) {
|
|
delay(100)
|
|
scrollState.animateScrollTo(scrollState.maxValue)
|
|
}
|
|
}
|
|
|
|
Column(
|
|
modifier = Modifier
|
|
.fillMaxSize()
|
|
.background(MaterialTheme.colorScheme.background)
|
|
) {
|
|
// Header
|
|
Card(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
colors = CardDefaults.cardColors(containerColor = Color.Transparent),
|
|
shape = RoundedCornerShape(0.dp)
|
|
) {
|
|
Box(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.background(
|
|
brush = Brush.linearGradient(
|
|
colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
|
|
)
|
|
)
|
|
.padding(20.dp)
|
|
) {
|
|
Column {
|
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
Icon(
|
|
Icons.Default.Star,
|
|
contentDescription = null,
|
|
tint = Color(0xFFFBBF24),
|
|
modifier = Modifier.size(28.dp)
|
|
)
|
|
Spacer(modifier = Modifier.width(12.dp))
|
|
Column {
|
|
Text(
|
|
"AI Helper",
|
|
style = MaterialTheme.typography.titleLarge,
|
|
color = Color.White,
|
|
fontWeight = FontWeight.Bold
|
|
)
|
|
Text(
|
|
"Powered by Gemini AI",
|
|
style = MaterialTheme.typography.bodySmall,
|
|
color = Color.White.copy(0.8f)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Category Selector & Stats - Compact Version
|
|
Column(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.background(MaterialTheme.colorScheme.background)
|
|
.padding(16.dp)
|
|
) {
|
|
// Category Selector
|
|
Box {
|
|
Card(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.clickable { showCategoryDropdown = !showCategoryDropdown },
|
|
colors = CardDefaults.cardColors(containerColor = Color(0xFF1E293B)),
|
|
shape = RoundedCornerShape(12.dp)
|
|
) {
|
|
Row(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(12.dp),
|
|
horizontalArrangement = Arrangement.SpaceBetween,
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
Icon(
|
|
Icons.Default.Folder,
|
|
contentDescription = null,
|
|
tint = Color(0xFF6366F1),
|
|
modifier = Modifier.size(20.dp)
|
|
)
|
|
Spacer(modifier = Modifier.width(8.dp))
|
|
Text(
|
|
selectedCategory?.name ?: "Semua Kategori",
|
|
color = Color.White,
|
|
style = MaterialTheme.typography.bodyMedium
|
|
)
|
|
}
|
|
Icon(
|
|
Icons.Default.ArrowDropDown,
|
|
contentDescription = null,
|
|
tint = Color(0xFF94A3B8)
|
|
)
|
|
}
|
|
}
|
|
|
|
DropdownMenu(
|
|
expanded = showCategoryDropdown,
|
|
onDismissRequest = { showCategoryDropdown = false },
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.background(Color(0xFF1E293B))
|
|
) {
|
|
DropdownMenuItem(
|
|
text = { Text("Semua Kategori", color = Color.White) },
|
|
onClick = {
|
|
selectedCategory = null
|
|
showCategoryDropdown = false
|
|
}
|
|
)
|
|
categories.forEach { category ->
|
|
DropdownMenuItem(
|
|
text = { Text(category.name, color = Color.White) },
|
|
onClick = {
|
|
selectedCategory = category
|
|
showCategoryDropdown = false
|
|
}
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Stats - Compact
|
|
Spacer(modifier = Modifier.height(12.dp))
|
|
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(
|
|
label = "Total",
|
|
value = filteredNotes.size.toString(),
|
|
color = Color(0xFF6366F1)
|
|
)
|
|
CompactStatItem(
|
|
label = "Dipasang",
|
|
value = filteredNotes.count { it.isPinned }.toString(),
|
|
color = Color(0xFFFBBF24)
|
|
)
|
|
CompactStatItem(
|
|
label = "Kategori",
|
|
value = categories.size.toString(),
|
|
color = Color(0xFFA855F7)
|
|
)
|
|
}
|
|
}
|
|
|
|
Divider(color = Color(0xFF334155), thickness = 1.dp)
|
|
|
|
// Chat Area
|
|
Column(
|
|
modifier = Modifier
|
|
.weight(1f)
|
|
.fillMaxWidth()
|
|
) {
|
|
if (chatMessages.isEmpty()) {
|
|
// Welcome State
|
|
Column(
|
|
modifier = Modifier
|
|
.fillMaxSize()
|
|
.padding(32.dp),
|
|
horizontalAlignment = Alignment.CenterHorizontally,
|
|
verticalArrangement = Arrangement.Center
|
|
) {
|
|
Icon(
|
|
Icons.Default.Star,
|
|
contentDescription = null,
|
|
modifier = Modifier.size(64.dp),
|
|
tint = Color(0xFF6366F1).copy(0.5f)
|
|
)
|
|
Spacer(modifier = Modifier.height(16.dp))
|
|
Text(
|
|
"Mulai Percakapan",
|
|
style = MaterialTheme.typography.titleLarge,
|
|
color = Color.White,
|
|
fontWeight = FontWeight.Bold
|
|
)
|
|
Spacer(modifier = Modifier.height(8.dp))
|
|
Text(
|
|
"Tanyakan apa saja tentang catatan Anda",
|
|
style = MaterialTheme.typography.bodyMedium,
|
|
color = Color(0xFF94A3B8),
|
|
textAlign = TextAlign.Center
|
|
)
|
|
|
|
Spacer(modifier = Modifier.height(24.dp))
|
|
|
|
// Suggestion Chips
|
|
Column(
|
|
horizontalAlignment = Alignment.Start,
|
|
modifier = Modifier.fillMaxWidth(0.8f)
|
|
) {
|
|
Text(
|
|
"Contoh pertanyaan:",
|
|
style = MaterialTheme.typography.bodySmall,
|
|
color = Color(0xFF64748B),
|
|
modifier = Modifier.padding(bottom = 8.dp)
|
|
)
|
|
SuggestionChip("Analisis catatan saya", onSelect = { prompt = it })
|
|
SuggestionChip("Buat ringkasan", onSelect = { prompt = it })
|
|
SuggestionChip("Berikan saran organisasi", onSelect = { prompt = it })
|
|
}
|
|
}
|
|
} else {
|
|
// Chat Messages
|
|
Column(
|
|
modifier = Modifier
|
|
.fillMaxSize()
|
|
.verticalScroll(scrollState)
|
|
.padding(16.dp)
|
|
) {
|
|
chatMessages.forEach { message ->
|
|
ChatBubble(
|
|
message = message,
|
|
onCopy = {
|
|
clipboardManager.setText(AnnotatedString(message.message))
|
|
copiedMessageId = message.id
|
|
showCopiedMessage = true
|
|
scope.launch {
|
|
delay(2000)
|
|
showCopiedMessage = false
|
|
}
|
|
},
|
|
showCopied = showCopiedMessage && copiedMessageId == message.id
|
|
)
|
|
Spacer(modifier = Modifier.height(12.dp))
|
|
}
|
|
|
|
// Loading Indicator
|
|
if (isLoading) {
|
|
Row(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(vertical = 8.dp),
|
|
horizontalArrangement = Arrangement.Start
|
|
) {
|
|
Card(
|
|
colors = CardDefaults.cardColors(
|
|
containerColor = Color(0xFF1E293B)
|
|
),
|
|
shape = RoundedCornerShape(16.dp)
|
|
) {
|
|
Row(
|
|
modifier = Modifier.padding(16.dp),
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
CircularProgressIndicator(
|
|
modifier = Modifier.size(20.dp),
|
|
color = Color(0xFF6366F1),
|
|
strokeWidth = 2.dp
|
|
)
|
|
Spacer(modifier = Modifier.width(12.dp))
|
|
Text(
|
|
"AI sedang berpikir...",
|
|
color = Color(0xFF94A3B8),
|
|
style = MaterialTheme.typography.bodyMedium
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Error Message
|
|
if (errorMessage.isNotEmpty()) {
|
|
Card(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
colors = CardDefaults.cardColors(
|
|
containerColor = Color(0xFFEF4444).copy(0.2f)
|
|
),
|
|
shape = RoundedCornerShape(12.dp)
|
|
) {
|
|
Row(
|
|
modifier = Modifier.padding(12.dp),
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
Icon(
|
|
Icons.Default.Warning,
|
|
contentDescription = null,
|
|
tint = Color(0xFFEF4444),
|
|
modifier = Modifier.size(20.dp)
|
|
)
|
|
Spacer(modifier = Modifier.width(8.dp))
|
|
Text(
|
|
errorMessage,
|
|
color = Color(0xFFEF4444),
|
|
style = MaterialTheme.typography.bodySmall
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
Spacer(modifier = Modifier.height(80.dp))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Input Area
|
|
Card(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
colors = CardDefaults.cardColors(
|
|
containerColor = Color(0xFF1E293B)
|
|
),
|
|
shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp),
|
|
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
|
|
) {
|
|
Row(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(16.dp),
|
|
verticalAlignment = Alignment.Bottom
|
|
) {
|
|
OutlinedTextField(
|
|
value = prompt,
|
|
onValueChange = { prompt = it },
|
|
placeholder = {
|
|
Text(
|
|
"Ketik pesan...",
|
|
color = Color(0xFF64748B)
|
|
)
|
|
},
|
|
modifier = Modifier
|
|
.weight(1f)
|
|
.heightIn(min = 48.dp, max = 120.dp),
|
|
colors = TextFieldDefaults.colors(
|
|
focusedTextColor = Color.White,
|
|
unfocusedTextColor = Color.White,
|
|
focusedContainerColor = Color(0xFF334155),
|
|
unfocusedContainerColor = Color(0xFF334155),
|
|
cursorColor = Color(0xFFA855F7),
|
|
focusedIndicatorColor = Color(0xFF6366F1),
|
|
unfocusedIndicatorColor = Color(0xFF475569)
|
|
),
|
|
shape = RoundedCornerShape(24.dp),
|
|
maxLines = 4
|
|
)
|
|
|
|
Spacer(modifier = Modifier.width(12.dp))
|
|
|
|
// Send Button
|
|
FloatingActionButton(
|
|
onClick = {
|
|
if (prompt.isNotBlank() && !isLoading) {
|
|
scope.launch {
|
|
// Add user message
|
|
chatMessages = chatMessages + ChatMessage(
|
|
message = prompt,
|
|
isUser = true
|
|
)
|
|
|
|
val userPrompt = prompt
|
|
prompt = ""
|
|
isLoading = true
|
|
errorMessage = ""
|
|
|
|
try {
|
|
val filteredNotes = if (selectedCategory != null) {
|
|
notes.filter { it.categoryId == selectedCategory!!.id && !it.isArchived }
|
|
} else {
|
|
notes.filter { !it.isArchived }
|
|
}
|
|
|
|
val notesContext = buildString {
|
|
appendLine("Data catatan pengguna:")
|
|
appendLine("Total catatan: ${filteredNotes.size}")
|
|
appendLine("Kategori: ${selectedCategory?.name ?: "Semua"}")
|
|
appendLine()
|
|
appendLine("Daftar catatan:")
|
|
filteredNotes.take(10).forEach { note ->
|
|
appendLine("- Judul: ${note.title}")
|
|
appendLine(" Isi: ${note.content.take(100)}")
|
|
appendLine()
|
|
}
|
|
}
|
|
|
|
val fullPrompt = "$notesContext\n\nPertanyaan: $userPrompt\n\nBerikan jawaban dalam bahasa Indonesia yang natural dan membantu."
|
|
|
|
val result = generativeModel.generateContent(fullPrompt)
|
|
val response = result.text ?: "Tidak ada respons dari AI"
|
|
|
|
// Add AI response
|
|
chatMessages = chatMessages + ChatMessage(
|
|
message = response,
|
|
isUser = false
|
|
)
|
|
|
|
} catch (e: Exception) {
|
|
errorMessage = "Error: ${e.message}"
|
|
} finally {
|
|
isLoading = false
|
|
}
|
|
}
|
|
}
|
|
},
|
|
containerColor = Color.Transparent,
|
|
modifier = Modifier
|
|
.size(48.dp)
|
|
.background(
|
|
brush = Brush.linearGradient(
|
|
colors = listOf(Color(0xFF6366F1), Color(0xFFA855F7))
|
|
),
|
|
shape = CircleShape
|
|
)
|
|
) {
|
|
Icon(
|
|
Icons.Default.Send,
|
|
contentDescription = "Send",
|
|
tint = Color.White,
|
|
modifier = Modifier.size(24.dp)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun ChatBubble(
|
|
message: ChatMessage,
|
|
onCopy: () -> Unit,
|
|
showCopied: Boolean
|
|
) {
|
|
val dateFormat = remember { SimpleDateFormat("HH:mm", Locale("id", "ID")) }
|
|
|
|
Row(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
horizontalArrangement = if (message.isUser) Arrangement.End else Arrangement.Start
|
|
) {
|
|
if (!message.isUser) {
|
|
// Ganti ikon bintang dengan ikon robot/sparkles
|
|
Icon(
|
|
Icons.Default.AutoAwesome, // Atau bisa diganti dengan ikon lain seperti AutoAwesome
|
|
contentDescription = null,
|
|
tint = Color(0xFF6366F1), // Warna ungu/biru untuk AI
|
|
modifier = Modifier
|
|
.size(32.dp)
|
|
.padding(end = 8.dp)
|
|
)
|
|
}
|
|
|
|
Column(
|
|
modifier = Modifier.fillMaxWidth(0.85f),
|
|
horizontalAlignment = if (message.isUser) Alignment.End else Alignment.Start
|
|
) {
|
|
Card(
|
|
colors = CardDefaults.cardColors(
|
|
containerColor = if (message.isUser)
|
|
Color(0xFF6366F1)
|
|
else
|
|
Color(0xFF1E293B)
|
|
),
|
|
shape = RoundedCornerShape(
|
|
topStart = 16.dp,
|
|
topEnd = 16.dp,
|
|
bottomStart = if (message.isUser) 16.dp else 4.dp,
|
|
bottomEnd = if (message.isUser) 4.dp else 16.dp
|
|
)
|
|
) {
|
|
Column(modifier = Modifier.padding(12.dp)) {
|
|
Text(
|
|
message.message,
|
|
color = Color.White,
|
|
style = MaterialTheme.typography.bodyMedium,
|
|
lineHeight = 20.sp
|
|
)
|
|
|
|
Row(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
horizontalArrangement = Arrangement.SpaceBetween,
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
Text(
|
|
dateFormat.format(Date(message.timestamp)),
|
|
color = Color.White.copy(0.6f),
|
|
style = MaterialTheme.typography.bodySmall,
|
|
modifier = Modifier.padding(top = 4.dp)
|
|
)
|
|
|
|
if (!message.isUser) {
|
|
IconButton(
|
|
onClick = onCopy,
|
|
modifier = Modifier.size(32.dp)
|
|
) {
|
|
Icon(
|
|
Icons.Default.ContentCopy,
|
|
contentDescription = "Copy",
|
|
tint = Color.White.copy(0.7f),
|
|
modifier = Modifier.size(16.dp)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (showCopied && !message.isUser) {
|
|
Text(
|
|
"✓ Disalin",
|
|
color = Color(0xFF10B981),
|
|
style = MaterialTheme.typography.bodySmall,
|
|
modifier = Modifier.padding(top = 4.dp, start = 8.dp)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun CompactStatItem(label: String, value: String, color: Color) {
|
|
Row(
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
modifier = Modifier
|
|
.background(
|
|
color = Color(0xFF1E293B),
|
|
shape = RoundedCornerShape(8.dp)
|
|
)
|
|
.padding(horizontal = 12.dp, vertical = 8.dp)
|
|
) {
|
|
Text(
|
|
value,
|
|
style = MaterialTheme.typography.titleMedium,
|
|
color = color,
|
|
fontWeight = FontWeight.Bold
|
|
)
|
|
Spacer(modifier = Modifier.width(4.dp))
|
|
Text(
|
|
label,
|
|
style = MaterialTheme.typography.bodySmall,
|
|
color = Color(0xFF94A3B8)
|
|
)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun SuggestionChip(text: String, onSelect: (String) -> Unit) {
|
|
Card(
|
|
modifier = Modifier
|
|
.padding(vertical = 4.dp)
|
|
.clickable { onSelect(text) },
|
|
colors = CardDefaults.cardColors(
|
|
containerColor = Color(0xFF1E293B)
|
|
),
|
|
shape = RoundedCornerShape(12.dp)
|
|
) {
|
|
Row(
|
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
Icon(
|
|
Icons.Default.Star,
|
|
contentDescription = null,
|
|
tint = Color(0xFF6366F1),
|
|
modifier = Modifier.size(16.dp)
|
|
)
|
|
Spacer(modifier = Modifier.width(8.dp))
|
|
Text(
|
|
text,
|
|
color = Color.White,
|
|
style = MaterialTheme.typography.bodyMedium
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
@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)
|
|
)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun ArchiveScreen(
|
|
notes: List<Note>,
|
|
categories: List<Category>,
|
|
onRestore: (Note) -> Unit,
|
|
onDelete: (Note) -> Unit
|
|
) {
|
|
if (notes.isEmpty()) {
|
|
EmptyState(
|
|
icon = Icons.Default.Archive,
|
|
message = "Arsip kosong",
|
|
subtitle = "Catatan yang diarsipkan akan muncul di sini"
|
|
)
|
|
} else {
|
|
LazyColumn(
|
|
contentPadding = PaddingValues(16.dp),
|
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
|
) {
|
|
items(notes) { note ->
|
|
val category = categories.find { it.id == note.categoryId }
|
|
ArchiveNoteCard(
|
|
note = note,
|
|
categoryName = category?.name ?: "Unknown",
|
|
onRestore = { onRestore(note) },
|
|
onDelete = { onDelete(note) }
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun ArchiveNoteCard(
|
|
note: Note,
|
|
categoryName: String,
|
|
onRestore: () -> Unit,
|
|
onDelete: () -> Unit
|
|
) {
|
|
Card(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
shape = RoundedCornerShape(16.dp),
|
|
colors = CardDefaults.cardColors(
|
|
containerColor = Color(0xFF1E293B)
|
|
),
|
|
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
|
) {
|
|
Column(modifier = Modifier.padding(16.dp)) {
|
|
Row(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
horizontalArrangement = Arrangement.SpaceBetween
|
|
) {
|
|
Column(modifier = Modifier.weight(1f)) {
|
|
Text(
|
|
note.title,
|
|
fontWeight = FontWeight.Bold,
|
|
color = Color.White,
|
|
style = MaterialTheme.typography.titleMedium
|
|
)
|
|
Spacer(modifier = Modifier.height(4.dp))
|
|
Text(
|
|
categoryName,
|
|
color = Color(0xFF64748B),
|
|
style = MaterialTheme.typography.bodySmall
|
|
)
|
|
}
|
|
}
|
|
|
|
if (note.content.isNotEmpty()) {
|
|
Spacer(modifier = Modifier.height(8.dp))
|
|
Text(
|
|
note.content,
|
|
maxLines = 2,
|
|
color = Color(0xFF94A3B8),
|
|
style = MaterialTheme.typography.bodyMedium
|
|
)
|
|
}
|
|
|
|
Row(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(top = 12.dp),
|
|
horizontalArrangement = Arrangement.End
|
|
) {
|
|
TextButton(onClick = onRestore) {
|
|
Icon(
|
|
Icons.Default.AccountBox,
|
|
contentDescription = null,
|
|
modifier = Modifier.size(18.dp),
|
|
tint = Color(0xFF10B981)
|
|
)
|
|
Spacer(modifier = Modifier.width(4.dp))
|
|
Text("Pulihkan", color = Color(0xFF10B981), fontWeight = FontWeight.Bold)
|
|
}
|
|
Spacer(modifier = Modifier.width(8.dp))
|
|
TextButton(onClick = onDelete) {
|
|
Icon(
|
|
Icons.Default.Delete,
|
|
contentDescription = null,
|
|
modifier = Modifier.size(18.dp),
|
|
tint = Color(0xFFEF4444)
|
|
)
|
|
Spacer(modifier = Modifier.width(4.dp))
|
|
Text("Hapus", color = Color(0xFFEF4444), fontWeight = FontWeight.Bold)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun TrashScreen(
|
|
notes: List<Note>,
|
|
categories: List<Category>,
|
|
onRestore: (Note) -> Unit,
|
|
onDeletePermanent: (Note) -> Unit
|
|
) {
|
|
if (notes.isEmpty()) {
|
|
EmptyState(
|
|
icon = Icons.Default.Delete,
|
|
message = "Sampah kosong",
|
|
subtitle = "Catatan yang dihapus akan muncul di sini"
|
|
)
|
|
} else {
|
|
LazyColumn(
|
|
contentPadding = PaddingValues(16.dp),
|
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
|
) {
|
|
items(notes) { note ->
|
|
val category = categories.find { it.id == note.categoryId }
|
|
TrashNoteCard(
|
|
note = note,
|
|
categoryName = category?.name ?: "Unknown",
|
|
onRestore = { onRestore(note) },
|
|
onDeletePermanent = { onDeletePermanent(note) }
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun TrashNoteCard(
|
|
note: Note,
|
|
categoryName: String,
|
|
onRestore: () -> Unit,
|
|
onDeletePermanent: () -> Unit
|
|
) {
|
|
Card(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
shape = RoundedCornerShape(16.dp),
|
|
colors = CardDefaults.cardColors(
|
|
containerColor = Color(0xFF7F1D1D).copy(0.2f)
|
|
),
|
|
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
|
) {
|
|
Column(modifier = Modifier.padding(16.dp)) {
|
|
Row(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
horizontalArrangement = Arrangement.SpaceBetween
|
|
) {
|
|
Column(modifier = Modifier.weight(1f)) {
|
|
Text(
|
|
note.title,
|
|
fontWeight = FontWeight.Bold,
|
|
color = Color.White,
|
|
style = MaterialTheme.typography.titleMedium
|
|
)
|
|
Spacer(modifier = Modifier.height(4.dp))
|
|
Text(
|
|
categoryName,
|
|
color = Color(0xFF64748B),
|
|
style = MaterialTheme.typography.bodySmall
|
|
)
|
|
}
|
|
}
|
|
|
|
if (note.content.isNotEmpty()) {
|
|
Spacer(modifier = Modifier.height(8.dp))
|
|
Text(
|
|
note.content,
|
|
maxLines = 2,
|
|
color = Color(0xFF94A3B8),
|
|
style = MaterialTheme.typography.bodyMedium
|
|
)
|
|
}
|
|
|
|
Row(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(top = 12.dp),
|
|
horizontalArrangement = Arrangement.End
|
|
) {
|
|
TextButton(onClick = onRestore) {
|
|
Icon(
|
|
Icons.Default.AccountBox,
|
|
contentDescription = null,
|
|
modifier = Modifier.size(18.dp),
|
|
tint = Color(0xFF10B981)
|
|
)
|
|
Spacer(modifier = Modifier.width(4.dp))
|
|
Text("Pulihkan", color = Color(0xFF10B981), fontWeight = FontWeight.Bold)
|
|
}
|
|
Spacer(modifier = Modifier.width(8.dp))
|
|
TextButton(onClick = onDeletePermanent) {
|
|
Icon(
|
|
Icons.Default.Delete,
|
|
contentDescription = null,
|
|
modifier = Modifier.size(18.dp),
|
|
tint = Color(0xFFEF4444)
|
|
)
|
|
Spacer(modifier = Modifier.width(4.dp))
|
|
Text("Hapus Permanen", color = Color(0xFFEF4444), fontWeight = FontWeight.Bold)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
@OptIn(ExperimentalMaterial3Api::class)
|
|
@Composable
|
|
fun StarredNotesScreen(
|
|
notes: List<Note>,
|
|
categories: List<Category>,
|
|
onNoteClick: (Note) -> Unit,
|
|
onMenuClick: () -> Unit,
|
|
onBack: () -> Unit,
|
|
onUnpin: (Note) -> Unit
|
|
) {
|
|
val starredNotes = notes.filter { it.isPinned && !it.isArchived && !it.isDeleted }
|
|
|
|
if (starredNotes.isEmpty()) {
|
|
EmptyState(
|
|
icon = Icons.Default.Star,
|
|
message = "Belum ada catatan berbintang",
|
|
subtitle = "Catatan yang ditandai berbintang akan muncul di sini"
|
|
)
|
|
} else {
|
|
LazyColumn(
|
|
contentPadding = PaddingValues(16.dp),
|
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
|
) {
|
|
items(starredNotes) { note ->
|
|
val category = categories.find { it.id == note.categoryId }
|
|
StarredNoteCard(
|
|
note = note,
|
|
categoryName = category?.name ?: "Unknown",
|
|
onClick = { onNoteClick(note) },
|
|
onUnpin = { onUnpin(note) }
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun StarredNoteCard(
|
|
note: Note,
|
|
categoryName: String,
|
|
onClick: () -> Unit,
|
|
onUnpin: () -> Unit
|
|
) {
|
|
Card(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.clickable(onClick = onClick),
|
|
shape = RoundedCornerShape(16.dp),
|
|
colors = CardDefaults.cardColors(
|
|
containerColor = Color(0xFF1E293B)
|
|
),
|
|
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
|
) {
|
|
Column(modifier = Modifier.padding(16.dp)) {
|
|
Row(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
horizontalArrangement = Arrangement.SpaceBetween,
|
|
verticalAlignment = Alignment.Top
|
|
) {
|
|
Column(modifier = Modifier.weight(1f)) {
|
|
Row(
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
Icon(
|
|
Icons.Default.Star,
|
|
contentDescription = null,
|
|
tint = Color(0xFFFBBF24),
|
|
modifier = Modifier.size(16.dp)
|
|
)
|
|
Spacer(modifier = Modifier.width(8.dp))
|
|
Text(
|
|
note.title,
|
|
fontWeight = FontWeight.Bold,
|
|
color = Color.White,
|
|
style = MaterialTheme.typography.titleMedium
|
|
)
|
|
}
|
|
Spacer(modifier = Modifier.height(4.dp))
|
|
Text(
|
|
categoryName,
|
|
color = Color(0xFF64748B),
|
|
style = MaterialTheme.typography.bodySmall
|
|
)
|
|
}
|
|
}
|
|
|
|
if (note.content.isNotEmpty()) {
|
|
Spacer(modifier = Modifier.height(8.dp))
|
|
Text(
|
|
note.content,
|
|
maxLines = 2,
|
|
overflow = TextOverflow.Ellipsis,
|
|
color = Color(0xFF94A3B8),
|
|
style = MaterialTheme.typography.bodyMedium
|
|
)
|
|
}
|
|
|
|
Row(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(top = 12.dp),
|
|
horizontalArrangement = Arrangement.End
|
|
) {
|
|
TextButton(onClick = onClick) {
|
|
Icon(
|
|
Icons.Default.Info,
|
|
contentDescription = null,
|
|
modifier = Modifier.size(18.dp),
|
|
tint = Color(0xFF6366F1)
|
|
)
|
|
Spacer(modifier = Modifier.width(4.dp))
|
|
Text("Lihat Detail", color = Color(0xFF6366F1), fontWeight = FontWeight.Bold)
|
|
}
|
|
Spacer(modifier = Modifier.width(8.dp))
|
|
TextButton(onClick = onUnpin) {
|
|
Icon(
|
|
Icons.Outlined.StarBorder,
|
|
contentDescription = null,
|
|
modifier = Modifier.size(18.dp),
|
|
tint = Color(0xFFFBBF24)
|
|
)
|
|
Spacer(modifier = Modifier.width(4.dp))
|
|
Text("Hapus Bintang", color = Color(0xFFFBBF24), fontWeight = FontWeight.Bold)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun EmptyState(
|
|
icon: ImageVector,
|
|
message: String,
|
|
subtitle: String
|
|
) {
|
|
Box(
|
|
modifier = Modifier.fillMaxSize(),
|
|
contentAlignment = Alignment.Center
|
|
) {
|
|
Column(
|
|
horizontalAlignment = Alignment.CenterHorizontally,
|
|
modifier = Modifier.padding(32.dp)
|
|
) {
|
|
Icon(
|
|
icon,
|
|
contentDescription = null,
|
|
modifier = Modifier.size(80.dp),
|
|
tint = Color(0xFF475569)
|
|
)
|
|
Spacer(modifier = Modifier.height(16.dp))
|
|
Text(
|
|
message,
|
|
style = MaterialTheme.typography.titleLarge,
|
|
color = Color(0xFF94A3B8),
|
|
fontWeight = FontWeight.Bold
|
|
)
|
|
Spacer(modifier = Modifier.height(8.dp))
|
|
Text(
|
|
subtitle,
|
|
style = MaterialTheme.typography.bodyMedium,
|
|
color = Color(0xFF64748B)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|