Compare commits

...

2 Commits

Author SHA1 Message Date
793600dd5a Room Database Setup 2025-11-13 10:29:10 +07:00
7190b1574d Room Database Setup 2025-11-13 10:22:03 +07:00
4 changed files with 603 additions and 478 deletions

View File

@ -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()
}
}
}

View File

@ -1,31 +0,0 @@
package com.example.notebook.data
import android.content.Context
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
/**
* Helper untuk test database
* Panggil fungsi ini dari MainActivity untuk test
*/
class DatabaseTest(private val context: Context) {
private val database = AppDatabase.getDatabase(context)
private val dao = database.notebookDao()
fun testInsertNotebook() {
CoroutineScope(Dispatchers.IO).launch {
val testNotebook = NotebookEntity(
title = "Notebook Test",
description = "Ini adalah test database",
createdAt = System.currentTimeMillis(),
updatedAt = System.currentTimeMillis(),
sourceCount = 0
)
val id = dao.insertNotebook(testNotebook)
println("✅ Database Test: Notebook berhasil disimpan dengan ID: $id")
}
}
}

View File

@ -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<List<NotebookEntity>> {
return dao.getAllNotebooks()
}
/**
* Ambil notebook berdasarkan ID
*/
fun getNotebookById(id: Int): Flow<NotebookEntity?> {
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<List<SourceEntity>> {
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<List<ChatMessageEntity>> {
return dao.getChatHistory(notebookId)
}
/**
* Hapus semua chat dalam notebook
*/
suspend fun clearChatHistory(notebookId: Int) {
dao.clearChatHistory(notebookId)
}
}

View File

@ -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<List<NotebookEntity>>(emptyList())
val notebooks: StateFlow<List<NotebookEntity>> = _notebooks.asStateFlow()
// State untuk notebook yang sedang aktif
private val _currentNotebook = MutableStateFlow<NotebookEntity?>(null)
val currentNotebook: StateFlow<NotebookEntity?> = _currentNotebook.asStateFlow()
// State untuk sources dalam notebook aktif
private val _sources = MutableStateFlow<List<SourceEntity>>(emptyList())
val sources: StateFlow<List<SourceEntity>> = _sources.asStateFlow()
// State untuk chat history
private val _chatMessages = MutableStateFlow<List<ChatMessageEntity>>(emptyList())
val chatMessages: StateFlow<List<ChatMessageEntity>> = _chatMessages.asStateFlow()
// State loading
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _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)
}
}
}