623 lines
27 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.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.zIndex
import com.example.notesai.data.local.DataStoreManager
import com.example.notesai.presentation.components.DrawerMenu
import com.example.notesai.presentation.components.ModernBottomBar
import com.example.notesai.presentation.components.ModernTopBar
import com.example.notesai.presentation.dialogs.CategoryDialog
import com.example.notesai.presentation.dialogs.NoteDialog
import com.example.notesai.presentation.screens.ai.AIHelperScreen
import com.example.notesai.presentation.screens.archive.ArchiveScreen
import com.example.notesai.presentation.screens.main.MainScreen
import com.example.notesai.presentation.screens.note.EditableFullScreenNoteView
import com.example.notesai.presentation.screens.starred.StarredNotesScreen
import com.example.notesai.presentation.screens.trash.TrashScreen
import com.example.notesai.data.model.Note
import com.example.notesai.data.model.Category
import com.example.notesai.util.AppColors
import com.example.notesai.util.Constants
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
NotesAppTheme()
}
}
}
@Composable
fun NotesAppTheme(content: @Composable () -> Unit = { NotesApp() }) {
val context = LocalContext.current
val dataStoreManager = remember { DataStoreManager(context) }
var isDarkTheme by remember { mutableStateOf(true) }
// Load theme preference
LaunchedEffect(Unit) {
dataStoreManager.themeFlow.collect { theme ->
isDarkTheme = theme == "dark"
AppColors.setTheme(isDarkTheme)
}
}
// Create dynamic color scheme based on theme
val colorScheme = if (isDarkTheme) {
darkColorScheme(
primary = AppColors.Primary,
onPrimary = Color.White,
primaryContainer = AppColors.Primary.copy(alpha = 0.3f),
onPrimaryContainer = Color.White,
secondary = AppColors.Secondary,
onSecondary = Color.White,
secondaryContainer = AppColors.Secondary.copy(alpha = 0.3f),
onSecondaryContainer = Color.White,
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
)
} else {
lightColorScheme(
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()
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NotesApp() {
val context = LocalContext.current
val dataStoreManager = remember { DataStoreManager(context) }
val scope = rememberCoroutineScope()
val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current
var categories by remember { mutableStateOf(listOf<Category>()) }
var notes by remember { mutableStateOf(listOf<Note>()) }
var selectedCategory by remember { mutableStateOf<Category?>(null) }
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) }
var isDarkTheme by remember { mutableStateOf(true) }
// STATE UNTUK AI
var showAIDrawer by remember { mutableStateOf(false) }
var aiSelectedCategory by remember { mutableStateOf<Category?>(null) }
var currentChatId by remember { mutableStateOf<String?>(null) }
var isDataLoaded by remember { mutableStateOf(false) }
// Load chat histories dari DataStore
val chatHistories by dataStoreManager.chatHistoryFlow.collectAsState(initial = emptyList())
fun sortCategories(categories: List<Category>): List<Category> {
return categories
.filter { !it.isDeleted }
.sortedWith(
compareByDescending<Category> { it.isPinned }
.thenByDescending { it.timestamp }
)
}
LaunchedEffect(Unit) {
dataStoreManager.themeFlow.collect { theme ->
isDarkTheme = theme == "dark"
AppColors.setTheme(isDarkTheme)
}
}
LaunchedEffect(Unit) {
dataStoreManager.categoriesFlow.collect { loadedCategories ->
if (!isDataLoaded) {
android.util.Log.d("NotesApp", "Loading ${loadedCategories.size} categories")
categories = loadedCategories
}
}
}
LaunchedEffect(Unit) {
dataStoreManager.notesFlow.collect { loadedNotes ->
if (!isDataLoaded) {
android.util.Log.d("NotesApp", "Loading ${loadedNotes.size} notes")
notes = loadedNotes
isDataLoaded = true
}
}
}
LaunchedEffect(categories) {
if (isDataLoaded && categories.isNotEmpty()) {
android.util.Log.d("NotesApp", "Saving ${categories.size} categories")
scope.launch {
dataStoreManager.saveCategories(categories)
}
}
}
LaunchedEffect(notes) {
if (isDataLoaded && notes.isNotEmpty()) {
android.util.Log.d("NotesApp", "Saving ${notes.size} notes")
scope.launch {
dataStoreManager.saveNotes(notes)
}
}
}
DisposableEffect(lifecycleOwner) {
val observer = androidx.lifecycle.LifecycleEventObserver { _, event ->
if (event == androidx.lifecycle.Lifecycle.Event.ON_PAUSE ||
event == androidx.lifecycle.Lifecycle.Event.ON_STOP) {
android.util.Log.d("NotesApp", "Lifecycle ${event.name}: Saving data")
scope.launch {
if (categories.isNotEmpty()) {
dataStoreManager.saveCategories(categories)
}
if (notes.isNotEmpty()) {
dataStoreManager.saveNotes(notes)
}
}
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
Box(modifier = Modifier.fillMaxSize()) {
// LAYER 1: Main Content (Scaffold)
Scaffold(
containerColor = AppColors.Background,
topBar = {
if (!showFullScreenNote && currentScreen != "ai") {
ModernTopBar(
title = when(currentScreen) {
"main" -> if (selectedCategory != null) selectedCategory!!.name else "AI Notes"
"starred" -> "Berbintang"
"archive" -> "Arsip"
"trash" -> "Sampah"
else -> "AI Notes"
},
showBackButton = (selectedCategory != null && currentScreen == "main"),
onBackClick = {
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(
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
) + fadeIn(),
exit = scaleOut() + fadeOut()
) {
FloatingActionButton(
onClick = {
if (selectedCategory != null) {
editingNote = null
showNoteDialog = true
} else {
showCategoryDialog = true
}
},
containerColor = AppColors.Primary,
contentColor = Color.White,
elevation = FloatingActionButtonDefaults.elevation(
defaultElevation = 8.dp,
pressedElevation = 12.dp
),
modifier = Modifier.size(64.dp)
) {
Icon(
Icons.Default.Add,
contentDescription = if (selectedCategory != null) "Tambah Catatan" else "Tambah Kategori",
modifier = Modifier.size(28.dp)
)
}
}
},
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 = sortCategories(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 ->
categories = categories.map {
if (it.id == category.id) it.copy(isDeleted = true)
else it
}
notes = notes.map {
if (it.categoryId == category.id) it.copy(isDeleted = true)
else it
}
selectedCategory = null
},
onCategoryEdit = { category, newName, newGradientStart, newGradientEnd ->
categories = categories.map {
if (it.id == category.id) {
it.copy(
name = newName,
gradientStart = newGradientStart,
gradientEnd = newGradientEnd,
timestamp = System.currentTimeMillis()
)
} else {
it
}
}
},
onCategoryPin = { category ->
categories = categories.map {
if (it.id == category.id) it.copy(isPinned = !it.isPinned)
else it
}
},
onNoteEdit = { note ->
editingNote = note
showNoteDialog = true
},
onNoteDelete = { note ->
notes = notes.map {
if (it.id == note.id) it.copy(isDeleted = true)
else it
}
}
)
"trash" -> TrashScreen(
notes = notes.filter { it.isDeleted },
categories = categories,
onRestoreNote = { note ->
notes = notes.map {
if (it.id == note.id) it.copy(isDeleted = false, isArchived = false)
else it
}
},
onDeleteNotePermanent = { note ->
notes = notes.filter { it.id != note.id }
},
onRestoreCategory = { category ->
categories = categories.map {
if (it.id == category.id) it.copy(isDeleted = false)
else it
}
notes = notes.map {
if (it.categoryId == category.id) it.copy(isDeleted = false, isArchived = false)
else it
}
},
onDeleteCategoryPermanent = { category ->
categories = categories.filter { it.id != category.id }
notes = notes.filter { it.categoryId != category.id }
}
)
"starred" -> StarredNotesScreen(
notes = notes,
categories = categories.filter { !it.isDeleted },
onNoteClick = { note ->
fullScreenNote = note
showFullScreenNote = true
},
onUnpin = { note ->
notes = notes.map {
if (it.id == note.id) it.copy(isPinned = false)
else it
}
}
)
"archive" -> ArchiveScreen(
notes = notes.filter { it.isArchived && !it.isDeleted },
categories = categories.filter { !it.isDeleted },
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
}
}
)
"ai" -> AIHelperScreen(
categories = categories.filter { !it.isDeleted },
notes = notes.filter { !it.isDeleted },
onShowDrawer = { showAIDrawer = true }
)
}
}
}
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,
categoryId = selectedCategory!!.id,
onDismiss = {
showNoteDialog = false
editingNote = null
},
onSave = { title, description ->
if (editingNote != null) {
notes = notes.map {
if (it.id == editingNote!!.id)
it.copy(
title = title,
description = description,
timestamp = System.currentTimeMillis()
)
else it
}
} else {
notes = notes + Note(
categoryId = selectedCategory!!.id,
title = title,
description = description,
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
)
}
}
}
// LAYER 2: Main Drawer (z-index 150)
AnimatedVisibility(
visible = drawerState,
enter = fadeIn() + slideInHorizontally(initialOffsetX = { -it }),
exit = fadeOut() + slideOutHorizontally(targetOffsetX = { -it }),
modifier = Modifier.zIndex(150f)
) {
DrawerMenu(
currentScreen = currentScreen,
isDarkTheme = isDarkTheme,
onDismiss = { drawerState = false },
onItemClick = { screen ->
currentScreen = screen
selectedCategory = null
drawerState = false
showSearch = false
searchQuery = ""
},
onThemeToggle = {
isDarkTheme = !isDarkTheme
AppColors.setTheme(isDarkTheme)
scope.launch {
dataStoreManager.saveTheme(if (isDarkTheme) "dark" else "light")
}
}
)
}
// LAYER 3: AI History Drawer (z-index 200 - PALING ATAS)
AnimatedVisibility(
visible = showAIDrawer,
enter = fadeIn() + slideInHorizontally(initialOffsetX = { -it }),
exit = fadeOut() + slideOutHorizontally(targetOffsetX = { -it }),
modifier = Modifier.zIndex(200f)
) {
com.example.notesai.presentation.screens.ai.components.ChatHistoryDrawer(
chatHistories = chatHistories, // GUNAKAN chatHistories dari collectAsState
categories = categories.filter { !it.isDeleted },
notes = notes.filter { !it.isDeleted },
selectedCategory = aiSelectedCategory,
onDismiss = { showAIDrawer = false },
onHistoryClick = { history ->
// Load chat history
aiSelectedCategory = categories.find { it.id == history.categoryId }
currentChatId = history.id
showAIDrawer = false
// Anda perlu cara untuk pass data ini ke AIHelperScreen
},
onDeleteHistory = { historyId ->
scope.launch {
dataStoreManager.deleteChatHistory(historyId)
}
},
onCategorySelected = { category ->
aiSelectedCategory = category
},
onNewChat = {
aiSelectedCategory = null
currentChatId = null
showAIDrawer = false
},
onEditHistoryTitle = { historyId, newTitle ->
scope.launch {
dataStoreManager.updateChatHistoryTitle(historyId, newTitle)
}
}
)
}
}
}