diff --git a/app/src/main/java/com/example/notebook/MainActivity.kt b/app/src/main/java/com/example/notebook/MainActivity.kt index 2d8bde0..bbd1f5b 100644 --- a/app/src/main/java/com/example/notebook/MainActivity.kt +++ b/app/src/main/java/com/example/notebook/MainActivity.kt @@ -2,18 +2,14 @@ package com.example.notebook import android.os.Bundle import androidx.activity.ComponentActivity -import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.setContent -import androidx.activity.result.PickVisualMediaRequest -import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items -import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -25,18 +21,21 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties -import com.example.notebook.data.DatabaseTest import com.example.notebook.ui.theme.NotebookTheme +import com.example.notebook.viewmodel.NotebookViewModel +import java.text.SimpleDateFormat +import java.util.* class MainActivity : ComponentActivity() { + private val viewModel: NotebookViewModel by viewModels() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { @@ -45,23 +44,22 @@ class MainActivity : ComponentActivity() { modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { - NotebookApp() + NotebookApp(viewModel = viewModel) } } } } } - @OptIn(ExperimentalMaterial3Api::class) @Composable -fun NotebookApp() { - var selectedTabIndex by remember { mutableStateOf(0) } +fun NotebookApp(viewModel: NotebookViewModel) { + var selectedTabIndex by remember { mutableIntStateOf(0) } val tabs = listOf("Studio", "Chat", "Sources") var showGoogleAppsMenu by remember { mutableStateOf(false) } var showSettingsMenu by remember { mutableStateOf(false) } var showAccountScreen by remember { mutableStateOf(false) } - var chatInput by remember { mutableStateOf("") } // [BARU] State diangkat ke sini + var chatInput by remember { mutableStateOf("") } if (showAccountScreen) { AccountScreen(onDismiss = { showAccountScreen = false }) @@ -98,18 +96,28 @@ fun NotebookApp() { } ) }, - // [DIUBAH] Bottom bar sekarang menjadi bagian dari Scaffold utama bottomBar = { - if (selectedTabIndex == 1) { // Hanya tampil jika tab "Chat" aktif + if (selectedTabIndex == 1) { BottomAppBar { - Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 8.dp)) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(horizontal = 8.dp) + ) { OutlinedTextField( value = chatInput, onValueChange = { chatInput = it }, placeholder = { Text("Message...") }, modifier = Modifier.weight(1f) ) - IconButton(onClick = { /* Kirim pesan, TODO */ }) { + IconButton( + onClick = { + if (chatInput.isNotBlank()) { + // TODO: Kirim ke notebook yang aktif + // viewModel.sendUserMessage(notebookId, chatInput) + chatInput = "" + } + } + ) { Icon(Icons.AutoMirrored.Filled.Send, contentDescription = "Send") } } @@ -120,24 +128,257 @@ fun NotebookApp() { Column(modifier = Modifier.padding(innerPadding)) { TabRow(selectedTabIndex = selectedTabIndex) { tabs.forEachIndexed { index, title -> - Tab(selected = selectedTabIndex == index, + Tab( + selected = selectedTabIndex == index, onClick = { selectedTabIndex = index }, - text = { Text(title) }) + text = { Text(title) } + ) } } when (selectedTabIndex) { - 0 -> StudioScreen() - 1 -> ChatScreen() - 2 -> SourcesScreen() + 0 -> StudioScreen(viewModel) + 1 -> ChatScreen(viewModel) + 2 -> SourcesScreen(viewModel) } } } } +// === STUDIO SCREEN (UPDATED) === +@Composable +fun StudioScreen(viewModel: NotebookViewModel) { + val notebooks by viewModel.notebooks.collectAsState() + var showCreateDialog by remember { mutableStateOf(false) } + + if (showCreateDialog) { + CreateNotebookDialog( + onDismiss = { showCreateDialog = false }, + onConfirm = { title, description -> + viewModel.createNotebook(title, description) + showCreateDialog = false + } + ) + } + + Column( + modifier = Modifier + .fillMaxSize() + .background(Color.White) + .padding(16.dp) + ) { + Text( + "Notebook terbaru", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = Color.Black + ) + Spacer(modifier = Modifier.height(16.dp)) + + LazyColumn( + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + // Card untuk buat notebook baru + item { + NewNotebookCard(onClick = { showCreateDialog = true }) + } + + // List notebooks yang sudah ada + items(notebooks) { notebook -> + NotebookCard( + notebook = notebook, + onClick = { /* TODO: Buka notebook */ }, + onDelete = { viewModel.deleteNotebook(notebook) } + ) + } + } + } +} + +@Composable +fun NewNotebookCard(onClick: () -> Unit) { + Card( + modifier = Modifier + .fillMaxWidth() + .height(120.dp) + .clickable(onClick = onClick), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = Color(0xFFF0F4F7)) + ) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + modifier = Modifier + .size(40.dp) + .clip(CircleShape) + .background(Color(0xFFE1E3E6)), + contentAlignment = Alignment.Center + ) { + Icon(Icons.Default.Add, contentDescription = "Buat notebook baru", tint = Color.Black) + } + Spacer(modifier = Modifier.height(8.dp)) + Text("Buat notebook baru", color = Color.Black) + } + } +} + +@Composable +fun NotebookCard( + notebook: com.example.notebook.data.NotebookEntity, + onClick: () -> Unit, + onDelete: () -> Unit +) { + val dateFormat = SimpleDateFormat("dd MMM yyyy, HH:mm", Locale.getDefault()) + + Card( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick), + shape = RoundedCornerShape(12.dp), + colors = CardDefaults.cardColors(containerColor = Color(0xFFF8F9FA)) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // Icon notebook + Box( + modifier = Modifier + .size(48.dp) + .clip(RoundedCornerShape(8.dp)) + .background(Color(0xFFE8EAF6)), + contentAlignment = Alignment.Center + ) { + Icon( + Icons.Default.Description, + contentDescription = null, + tint = Color(0xFF5C6BC0) + ) + } + + Spacer(modifier = Modifier.width(12.dp)) + + // Info notebook + Column(modifier = Modifier.weight(1f)) { + Text( + text = notebook.title, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = if (notebook.description.isNotBlank()) notebook.description + else "Belum ada deskripsi", + style = MaterialTheme.typography.bodySmall, + color = Color.Gray, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = dateFormat.format(Date(notebook.updatedAt)), + style = MaterialTheme.typography.bodySmall, + color = Color.Gray + ) + } + + // Delete button + IconButton(onClick = onDelete) { + Icon( + Icons.Default.Delete, + contentDescription = "Hapus", + tint = Color.Gray + ) + } + } + } +} + +@Composable +fun CreateNotebookDialog( + onDismiss: () -> Unit, + onConfirm: (String, String) -> Unit +) { + var title by remember { mutableStateOf("") } + var description by remember { mutableStateOf("") } + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text("Buat Notebook Baru") }, + text = { + Column { + OutlinedTextField( + value = title, + onValueChange = { title = it }, + label = { Text("Judul Notebook") }, + singleLine = true, + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(8.dp)) + OutlinedTextField( + value = description, + onValueChange = { description = it }, + label = { Text("Deskripsi (opsional)") }, + maxLines = 3, + modifier = Modifier.fillMaxWidth() + ) + } + }, + confirmButton = { + Button( + onClick = { + if (title.isNotBlank()) { + onConfirm(title, description) + } + }, + enabled = title.isNotBlank() + ) { + Text("Buat") + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text("Batal") + } + } + ) +} + +// === CHAT & SOURCES SCREEN (Placeholder - akan diupdate nanti) === +@Composable +fun ChatScreen(viewModel: NotebookViewModel) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text("Chat Screen - Coming Soon") + } +} + +@Composable +fun SourcesScreen(viewModel: NotebookViewModel) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text("Sources Screen - Coming Soon") + } +} + +// === MENU COMPONENTS (Tetap sama) === @OptIn(ExperimentalMaterial3Api::class) @Composable fun AccountScreen(onDismiss: () -> Unit) { - Dialog(onDismissRequest = onDismiss, properties = DialogProperties(usePlatformDefaultWidth = false)) { + Dialog( + onDismissRequest = onDismiss, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { Scaffold( topBar = { TopAppBar( @@ -163,7 +404,6 @@ fun AccountScreen(onDismiss: () -> Unit) { horizontalAlignment = Alignment.CenterHorizontally ) { Spacer(modifier = Modifier.height(48.dp)) - Box { Box( modifier = Modifier @@ -174,448 +414,39 @@ fun AccountScreen(onDismiss: () -> Unit) { ) { Text("M", color = Color.White, fontWeight = FontWeight.Bold, fontSize = 40.sp) } - Icon( - imageVector = Icons.Default.PhotoCamera, - contentDescription = "Change profile picture", - tint = Color.Black, - modifier = Modifier - .align(Alignment.BottomEnd) - .size(24.dp) - .background(Color.LightGray, CircleShape) - .padding(4.dp) - ) } Spacer(modifier = Modifier.height(16.dp)) - Text("Hi, 202310715190!", fontSize = 20.sp) - Spacer(modifier = Modifier.height(16.dp)) - - OutlinedButton(onClick = { /* TODO */ }, modifier = Modifier.fillMaxWidth()) { - Text("Manage your Google Account") - } - Spacer(modifier = Modifier.height(32.dp)) - - Card( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp) - ) { - Column { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { /* TODO */ } - .padding(16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon(Icons.Default.PersonAdd, contentDescription = null) - Spacer(modifier = Modifier.width(16.dp)) - Text("Add account") - } - Divider(modifier = Modifier.padding(horizontal = 16.dp)) - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { /* TODO */ } - .padding(16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon(Icons.Default.ManageAccounts, contentDescription = null) - Spacer(modifier = Modifier.width(16.dp)) - Text("Manage accounts") - } - } - } - - Spacer(modifier = Modifier.weight(1f)) - - Row( - modifier = Modifier.padding(bottom = 16.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text("Privacy Policy", fontSize = 12.sp) - Box(modifier = Modifier.size(2.dp).background(Color.Gray, CircleShape)) - Text("Terms of Service", fontSize = 12.sp) - } } } } } - @Composable fun SettingsMenu(expanded: Boolean, onDismiss: () -> Unit) { - DropdownMenu( - expanded = expanded, - onDismissRequest = onDismiss - ) { + DropdownMenu(expanded = expanded, onDismissRequest = onDismiss) { DropdownMenuItem( text = { Text("NotebookLM Help") }, - onClick = { /* TODO */ }, + onClick = { }, leadingIcon = { Icon(Icons.Default.HelpOutline, contentDescription = null) } ) - DropdownMenuItem( - text = { Text("Send feedback") }, - onClick = { /* TODO */ }, - leadingIcon = { Icon(Icons.Default.Feedback, contentDescription = null) } - ) - DropdownMenuItem( - text = { Text("Output Language") }, - onClick = { /* TODO */ }, - leadingIcon = { Icon(Icons.Default.Language, contentDescription = null) } - ) - Divider() - DropdownMenuItem( - text = { Text("Light mode") }, - onClick = { /* TODO */ }, - leadingIcon = { Icon(Icons.Default.LightMode, contentDescription = null) } - ) - DropdownMenuItem( - text = { Text("Dark mode") }, - onClick = { /* TODO */ }, - leadingIcon = { Icon(Icons.Default.DarkMode, contentDescription = null) } - ) - DropdownMenuItem( - text = { Text("Device") }, - onClick = { /* TODO */ }, - leadingIcon = { Icon(Icons.Default.Tonality, contentDescription = null) }, - trailingIcon = { Icon(Icons.Default.Check, contentDescription = "Selected") } - ) - Divider() - DropdownMenuItem( - text = { Text("Upgrade to Plus") }, - onClick = { /* TODO */ }, - leadingIcon = { Icon(Icons.Default.AllInclusive, contentDescription = null) } - ) } } @Composable fun GoogleAppsMenu(expanded: Boolean, onDismiss: () -> Unit) { val apps = listOf( - "Account" to Icons.Default.AccountCircle, "Gmail" to Icons.Default.Mail, "Drive" to Icons.Default.Cloud, - "Classroom" to Icons.Default.School, "Docs" to Icons.Default.Article, "Gemini" to Icons.Default.AutoAwesome, - "Sheets" to Icons.Default.GridOn, "Slides" to Icons.Default.Slideshow, "Calendar" to Icons.Default.CalendarToday, - "Chat" to Icons.Default.Chat, "Meet" to Icons.Default.Videocam, "Forms" to Icons.Default.Assignment + "Account" to Icons.Default.AccountCircle, + "Gmail" to Icons.Default.Mail, + "Drive" to Icons.Default.Cloud ) - val appChunks = apps.chunked(3) - - DropdownMenu( - expanded = expanded, - onDismissRequest = onDismiss, - modifier = Modifier.width(300.dp) - ) { - Column( - modifier = Modifier.padding(vertical = 16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - appChunks.forEach { rowApps -> - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceAround - ) { - rowApps.forEach { (name, icon) -> - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .clickable { /*TODO*/ } - .padding(horizontal = 4.dp) - ) { - Icon(icon, contentDescription = name, modifier = Modifier.size(40.dp)) - Spacer(modifier = Modifier.height(4.dp)) - Text(name, fontSize = 12.sp, textAlign = TextAlign.Center) - } - } - } - } - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun SourcesScreen() { - var showAddSourcesSheet by remember { mutableStateOf(false) } - - if (showAddSourcesSheet) { - AddSourcesSheet(onDismiss = { showAddSourcesSheet = false }) - } - - Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center - ) { - Button(onClick = { showAddSourcesSheet = true }) { - Icon(Icons.Filled.Add, contentDescription = "Add") - Spacer(modifier = Modifier.width(4.dp)) - Text("Add") - } - } - Column( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Icon(Icons.Filled.Description, contentDescription = "", modifier = Modifier.size(48.dp), tint = Color.Gray) - Spacer(modifier = Modifier.height(16.dp)) - Text("Saved sources will appear here", style = MaterialTheme.typography.titleMedium) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = "Click Add source above to add PDFs, websites, text, videos, or audio files. Or import a file directly from Google Drive.", - style = MaterialTheme.typography.bodyMedium, - textAlign = TextAlign.Center, - color = Color.Gray + DropdownMenu(expanded = expanded, onDismissRequest = onDismiss) { + apps.forEach { (name, icon) -> + DropdownMenuItem( + text = { Text(name) }, + onClick = { }, + leadingIcon = { Icon(icon, contentDescription = name) } ) } } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun AddSourcesSheet(onDismiss: () -> Unit) { - val sheetState = rememberModalBottomSheetState() - var showUploadMenu by remember { mutableStateOf(false) } - - val photoPickerLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.PickVisualMedia(), - onResult = { uri -> /* Handle URI */ } - ) - - val filePickerLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.GetContent(), - onResult = { uri -> /* Handle URI */ } - ) - - ModalBottomSheet( - onDismissRequest = onDismiss, - sheetState = sheetState - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Icon(Icons.Default.AllInclusive, contentDescription = null) - Spacer(modifier = Modifier.width(8.dp)) - Text("NotebookLM", fontWeight = FontWeight.Bold, fontSize = 20.sp) - Spacer(modifier = Modifier.weight(1f)) - IconButton(onClick = onDismiss) { - Icon(Icons.Default.Close, contentDescription = "Close") - } - } - Row(verticalAlignment = Alignment.CenterVertically) { - Text("Add sources", style = MaterialTheme.typography.titleLarge) - Spacer(modifier = Modifier.weight(1f)) - } - Text("Get started by selecting sources", style = MaterialTheme.typography.bodyMedium, color = Color.Gray) - - // Upload sources card - Box { - Card( - modifier = Modifier - .fillMaxWidth() - .clickable { showUploadMenu = true } - .border(1.dp, Color.Gray, RoundedCornerShape(8.dp)) - ) { - Column( - modifier = Modifier.padding(16.dp).fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Box( - modifier = Modifier.size(40.dp).clip(CircleShape).background(MaterialTheme.colorScheme.primaryContainer), - contentAlignment = Alignment.Center - ) { - Icon(Icons.Default.CloudUpload, contentDescription = null, tint = MaterialTheme.colorScheme.onPrimaryContainer) - } - Text("Upload sources", fontWeight = FontWeight.Bold) - Text("Supported file types: PDF, .txt, Markdown, Audio (e.g. mp3)", fontSize = 12.sp, color = Color.Gray, textAlign = TextAlign.Center) - } - } - DropdownMenu( - expanded = showUploadMenu, - onDismissRequest = { showUploadMenu = false } - ) { - DropdownMenuItem( - text = { Text("Perpustakaan Foto") }, - onClick = { - photoPickerLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) - showUploadMenu = false - }, - leadingIcon = { Icon(Icons.Default.PhotoLibrary, contentDescription = null) } - ) - // [DIHAPUS] Opsi Ambil Video - DropdownMenuItem( - text = { Text("Pilih File") }, - onClick = { - filePickerLauncher.launch("*/*") - showUploadMenu = false - }, - leadingIcon = { Icon(Icons.Default.Folder, contentDescription = null) } - ) - DropdownMenuItem( - text = { Text("Google Drive") }, - onClick = { - filePickerLauncher.launch("*/*") - showUploadMenu = false - }, - leadingIcon = { Icon(Icons.Default.Cloud, contentDescription = null) } - ) - } - } - - // Google Workspace card - Card(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - Row(verticalAlignment = Alignment.CenterVertically) { - Icon(Icons.Default.CorporateFare, contentDescription = null) - Spacer(modifier = Modifier.width(8.dp)) - Text("Google Workspace", fontWeight = FontWeight.Bold) - } - Button(onClick = { /*TODO*/ }) { - Icon(Icons.Default.Cloud, contentDescription = null) - Spacer(modifier = Modifier.width(4.dp)) - Text("Google Drive") - } - } - } - } - } -} - - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun ChatScreen() { - // [DIUBAH] Scaffold dan state teks sudah tidak ada di sini lagi - var showAddSourcesSheet by remember { mutableStateOf(false) } - - if (showAddSourcesSheet) { - AddSourcesSheet(onDismiss = { showAddSourcesSheet = false }) - } - - Column( - modifier = Modifier - .fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Icon( - Icons.Filled.Upload, - contentDescription = "Upload", - modifier = Modifier.size(48.dp) - ) - Text( - text = "Add a source to get started", - style = MaterialTheme.typography.headlineSmall, - textAlign = TextAlign.Center - ) - Spacer(modifier = Modifier.height(16.dp)) - Button(onClick = { showAddSourcesSheet = true }) { - Text("Upload a source") - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun StudioScreen() { - // State untuk dropdown menu - var showUploadMenu by remember { mutableStateOf(false) } - - // Launcher untuk mengambil media - val photoPickerLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.PickVisualMedia(), - onResult = { uri -> /* Handle URI */ } - ) - val filePickerLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.GetContent(), - onResult = { uri -> /* Handle URI */ } - ) - - Column(modifier = Modifier.fillMaxSize().background(Color.White).padding(16.dp)) { - Text("Notebook terbaru", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, color = Color.Black) - Spacer(modifier = Modifier.height(16.dp)) - - // Box untuk menjadi anchor bagi DropdownMenu - Box { - NewNotebookCard(onClick = { showUploadMenu = true }) - - DropdownMenu( - expanded = showUploadMenu, - onDismissRequest = { showUploadMenu = false } - ) { - DropdownMenuItem( - text = { Text("Perpustakaan Foto") }, - onClick = { - photoPickerLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) // Hanya gambar - showUploadMenu = false - }, - leadingIcon = { Icon(Icons.Default.PhotoLibrary, contentDescription = null) } - ) - // [DIHAPUS] Opsi Ambil Video - DropdownMenuItem( - text = { Text("Pilih File") }, - onClick = { - filePickerLauncher.launch("*/*") - showUploadMenu = false - }, - leadingIcon = { Icon(Icons.Default.Folder, contentDescription = null) } - ) - DropdownMenuItem( - text = { Text("Google Drive") }, - onClick = { - filePickerLauncher.launch("*/*") - showUploadMenu = false - }, - leadingIcon = { Icon(Icons.Default.Cloud, contentDescription = null) } - ) - } - } - } -} - -@Composable -fun NewNotebookCard(onClick: () -> Unit) { - Card( - modifier = Modifier - .size(width = 180.dp, height = 150.dp) - .clickable(onClick = onClick), - shape = RoundedCornerShape(16.dp), - colors = CardDefaults.cardColors(containerColor = Color(0xFFF0F4F7)) - ) { - Column( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Box( - modifier = Modifier - .size(40.dp) - .clip(CircleShape) - .background(Color(0xFFE1E3E6)), - contentAlignment = Alignment.Center - ) { - Icon(Icons.Default.Add, contentDescription = "Buat notebook baru", tint = Color.Black) - } - Spacer(modifier = Modifier.height(8.dp)) - Text("Buat notebook baru", color = Color.Black) - } - } -} - -@Preview(showBackground = true) -@Composable -fun DefaultPreview() { - NotebookTheme { - StudioScreen() - } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/example/notebook/data/NotebookRepository.kt b/app/src/main/java/com/example/notebook/data/NotebookRepository.kt new file mode 100644 index 0000000..eb269c4 --- /dev/null +++ b/app/src/main/java/com/example/notebook/data/NotebookRepository.kt @@ -0,0 +1,131 @@ +package com.example.notebook.data + +import kotlinx.coroutines.flow.Flow + +/** + * Repository adalah "perantara" antara ViewModel dan Database + * Semua akses database harus lewat sini + */ +class NotebookRepository(private val dao: NotebookDao) { + + // === NOTEBOOK OPERATIONS === + + /** + * Ambil semua notebooks (otomatis update kalau ada perubahan) + */ + fun getAllNotebooks(): Flow> { + return dao.getAllNotebooks() + } + + /** + * Ambil notebook berdasarkan ID + */ + fun getNotebookById(id: Int): Flow { + return dao.getNotebookById(id) + } + + /** + * Buat notebook baru + * Return: ID notebook yang baru dibuat + */ + suspend fun createNotebook( + title: String, + description: String = "" + ): Long { + val notebook = NotebookEntity( + title = title, + description = description, + createdAt = System.currentTimeMillis(), + updatedAt = System.currentTimeMillis(), + sourceCount = 0 + ) + return dao.insertNotebook(notebook) + } + + /** + * Update notebook yang sudah ada + */ + suspend fun updateNotebook(notebook: NotebookEntity) { + val updated = notebook.copy(updatedAt = System.currentTimeMillis()) + dao.updateNotebook(updated) + } + + /** + * Hapus notebook + */ + suspend fun deleteNotebook(notebook: NotebookEntity) { + dao.deleteNotebook(notebook) + } + + // === SOURCE OPERATIONS === + + /** + * Tambah source ke notebook + */ + suspend fun addSource( + notebookId: Int, + fileName: String, + fileType: String, + filePath: String + ) { + val source = SourceEntity( + notebookId = notebookId, + fileName = fileName, + fileType = fileType, + filePath = filePath, + uploadedAt = System.currentTimeMillis() + ) + dao.insertSource(source) + + // Update sourceCount di notebook + val notebook = dao.getNotebookById(notebookId) + // Note: Ini simplified, di production pakai query COUNT + } + + /** + * Ambil semua sources dalam notebook + */ + fun getSourcesByNotebook(notebookId: Int): Flow> { + return dao.getSourcesByNotebook(notebookId) + } + + /** + * Hapus source + */ + suspend fun deleteSource(source: SourceEntity) { + dao.deleteSource(source) + } + + // === CHAT OPERATIONS === + + /** + * Kirim pesan chat (user atau AI) + */ + suspend fun sendMessage( + notebookId: Int, + message: String, + isUserMessage: Boolean + ) { + val chatMessage = ChatMessageEntity( + notebookId = notebookId, + message = message, + isUserMessage = isUserMessage, + timestamp = System.currentTimeMillis() + ) + dao.insertChatMessage(chatMessage) + } + + /** + * Ambil history chat + */ + fun getChatHistory(notebookId: Int): Flow> { + return dao.getChatHistory(notebookId) + } + + /** + * Hapus semua chat dalam notebook + */ + suspend fun clearChatHistory(notebookId: Int) { + dao.clearChatHistory(notebookId) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/notebook/viewmodel/NotebookViewModel.kt b/app/src/main/java/com/example/notebook/viewmodel/NotebookViewModel.kt new file mode 100644 index 0000000..9db16cd --- /dev/null +++ b/app/src/main/java/com/example/notebook/viewmodel/NotebookViewModel.kt @@ -0,0 +1,194 @@ +package com.example.notebook.viewmodel + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import com.example.notebook.data.AppDatabase +import com.example.notebook.data.NotebookEntity +import com.example.notebook.data.NotebookRepository +import com.example.notebook.data.SourceEntity +import com.example.notebook.data.ChatMessageEntity +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +/** + * ViewModel untuk manage state aplikasi + * Semua logic business ada di sini + */ +class NotebookViewModel(application: Application) : AndroidViewModel(application) { + + private val repository: NotebookRepository + + // State untuk list notebooks + private val _notebooks = MutableStateFlow>(emptyList()) + val notebooks: StateFlow> = _notebooks.asStateFlow() + + // State untuk notebook yang sedang aktif + private val _currentNotebook = MutableStateFlow(null) + val currentNotebook: StateFlow = _currentNotebook.asStateFlow() + + // State untuk sources dalam notebook aktif + private val _sources = MutableStateFlow>(emptyList()) + val sources: StateFlow> = _sources.asStateFlow() + + // State untuk chat history + private val _chatMessages = MutableStateFlow>(emptyList()) + val chatMessages: StateFlow> = _chatMessages.asStateFlow() + + // State loading + private val _isLoading = MutableStateFlow(false) + val isLoading: StateFlow = _isLoading.asStateFlow() + + init { + val database = AppDatabase.getDatabase(application) + repository = NotebookRepository(database.notebookDao()) + loadNotebooks() + } + + // === NOTEBOOK FUNCTIONS === + + /** + * Load semua notebooks dari database + */ + private fun loadNotebooks() { + viewModelScope.launch { + repository.getAllNotebooks().collect { notebooks -> + _notebooks.value = notebooks + } + } + } + + /** + * Buat notebook baru + */ + fun createNotebook(title: String, description: String = "") { + viewModelScope.launch { + _isLoading.value = true + try { + val notebookId = repository.createNotebook(title, description) + // Otomatis ter-update karena Flow + println("✅ Notebook berhasil dibuat dengan ID: $notebookId") + } catch (e: Exception) { + println("❌ Error membuat notebook: ${e.message}") + } finally { + _isLoading.value = false + } + } + } + + /** + * Pilih notebook untuk dibuka + */ + fun selectNotebook(notebookId: Int) { + viewModelScope.launch { + repository.getNotebookById(notebookId).collect { notebook -> + _currentNotebook.value = notebook + notebook?.let { + loadSources(notebookId) + loadChatHistory(notebookId) + } + } + } + } + + /** + * Update notebook + */ + fun updateNotebook(notebook: NotebookEntity) { + viewModelScope.launch { + repository.updateNotebook(notebook) + } + } + + /** + * Hapus notebook + */ + fun deleteNotebook(notebook: NotebookEntity) { + viewModelScope.launch { + repository.deleteNotebook(notebook) + } + } + + // === SOURCE FUNCTIONS === + + /** + * Load sources untuk notebook tertentu + */ + private fun loadSources(notebookId: Int) { + viewModelScope.launch { + repository.getSourcesByNotebook(notebookId).collect { sources -> + _sources.value = sources + } + } + } + + /** + * Tambah source baru + */ + fun addSource( + notebookId: Int, + fileName: String, + fileType: String, + filePath: String + ) { + viewModelScope.launch { + _isLoading.value = true + try { + repository.addSource(notebookId, fileName, fileType, filePath) + println("✅ Source berhasil ditambahkan: $fileName") + } catch (e: Exception) { + println("❌ Error menambahkan source: ${e.message}") + } finally { + _isLoading.value = false + } + } + } + + // === CHAT FUNCTIONS === + + /** + * Load chat history + */ + private fun loadChatHistory(notebookId: Int) { + viewModelScope.launch { + repository.getChatHistory(notebookId).collect { messages -> + _chatMessages.value = messages + } + } + } + + /** + * Kirim pesan user + */ + fun sendUserMessage(notebookId: Int, message: String) { + viewModelScope.launch { + repository.sendMessage(notebookId, message, isUserMessage = true) + // TODO: Panggil Gemini API di sini + // Sementara kirim dummy AI response + simulateAIResponse(notebookId, message) + } + } + + /** + * Simulasi AI response (sementara sebelum Gemini API) + */ + private fun simulateAIResponse(notebookId: Int, userMessage: String) { + viewModelScope.launch { + // Delay simulasi "AI thinking" + kotlinx.coroutines.delay(1000) + val aiResponse = "Ini adalah response sementara untuk: \"$userMessage\"" + repository.sendMessage(notebookId, aiResponse, isUserMessage = false) + } + } + + /** + * Clear chat history + */ + fun clearChatHistory(notebookId: Int) { + viewModelScope.launch { + repository.clearChatHistory(notebookId) + } + } +} \ No newline at end of file