Compare commits
No commits in common. "793600dd5a78dc97bb76a384228b5a47df33f4f0" and "b3022237a345c6b944c8aae08485207789bf268e" have entirely different histories.
793600dd5a
...
b3022237a3
@ -2,14 +2,18 @@ package com.example.notebook
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.result.PickVisualMediaRequest
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
|
import androidx.compose.foundation.selection.selectable
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@ -21,21 +25,18 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
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.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import com.example.notebook.data.DatabaseTest
|
||||||
import com.example.notebook.ui.theme.NotebookTheme
|
import com.example.notebook.ui.theme.NotebookTheme
|
||||||
import com.example.notebook.viewmodel.NotebookViewModel
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
private val viewModel: NotebookViewModel by viewModels()
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContent {
|
setContent {
|
||||||
@ -44,22 +45,23 @@ class MainActivity : ComponentActivity() {
|
|||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
color = MaterialTheme.colorScheme.background
|
color = MaterialTheme.colorScheme.background
|
||||||
) {
|
) {
|
||||||
NotebookApp(viewModel = viewModel)
|
NotebookApp()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun NotebookApp(viewModel: NotebookViewModel) {
|
fun NotebookApp() {
|
||||||
var selectedTabIndex by remember { mutableIntStateOf(0) }
|
var selectedTabIndex by remember { mutableStateOf(0) }
|
||||||
val tabs = listOf("Studio", "Chat", "Sources")
|
val tabs = listOf("Studio", "Chat", "Sources")
|
||||||
var showGoogleAppsMenu by remember { mutableStateOf(false) }
|
var showGoogleAppsMenu by remember { mutableStateOf(false) }
|
||||||
var showSettingsMenu by remember { mutableStateOf(false) }
|
var showSettingsMenu by remember { mutableStateOf(false) }
|
||||||
var showAccountScreen by remember { mutableStateOf(false) }
|
var showAccountScreen by remember { mutableStateOf(false) }
|
||||||
var chatInput by remember { mutableStateOf("") }
|
var chatInput by remember { mutableStateOf("") } // [BARU] State diangkat ke sini
|
||||||
|
|
||||||
if (showAccountScreen) {
|
if (showAccountScreen) {
|
||||||
AccountScreen(onDismiss = { showAccountScreen = false })
|
AccountScreen(onDismiss = { showAccountScreen = false })
|
||||||
@ -96,28 +98,18 @@ fun NotebookApp(viewModel: NotebookViewModel) {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
// [DIUBAH] Bottom bar sekarang menjadi bagian dari Scaffold utama
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
if (selectedTabIndex == 1) {
|
if (selectedTabIndex == 1) { // Hanya tampil jika tab "Chat" aktif
|
||||||
BottomAppBar {
|
BottomAppBar {
|
||||||
Row(
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 8.dp)) {
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.padding(horizontal = 8.dp)
|
|
||||||
) {
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = chatInput,
|
value = chatInput,
|
||||||
onValueChange = { chatInput = it },
|
onValueChange = { chatInput = it },
|
||||||
placeholder = { Text("Message...") },
|
placeholder = { Text("Message...") },
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
IconButton(
|
IconButton(onClick = { /* Kirim pesan, TODO */ }) {
|
||||||
onClick = {
|
|
||||||
if (chatInput.isNotBlank()) {
|
|
||||||
// TODO: Kirim ke notebook yang aktif
|
|
||||||
// viewModel.sendUserMessage(notebookId, chatInput)
|
|
||||||
chatInput = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Icon(Icons.AutoMirrored.Filled.Send, contentDescription = "Send")
|
Icon(Icons.AutoMirrored.Filled.Send, contentDescription = "Send")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -128,257 +120,24 @@ fun NotebookApp(viewModel: NotebookViewModel) {
|
|||||||
Column(modifier = Modifier.padding(innerPadding)) {
|
Column(modifier = Modifier.padding(innerPadding)) {
|
||||||
TabRow(selectedTabIndex = selectedTabIndex) {
|
TabRow(selectedTabIndex = selectedTabIndex) {
|
||||||
tabs.forEachIndexed { index, title ->
|
tabs.forEachIndexed { index, title ->
|
||||||
Tab(
|
Tab(selected = selectedTabIndex == index,
|
||||||
selected = selectedTabIndex == index,
|
|
||||||
onClick = { selectedTabIndex = index },
|
onClick = { selectedTabIndex = index },
|
||||||
text = { Text(title) }
|
text = { Text(title) })
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
when (selectedTabIndex) {
|
when (selectedTabIndex) {
|
||||||
0 -> StudioScreen(viewModel)
|
0 -> StudioScreen()
|
||||||
1 -> ChatScreen(viewModel)
|
1 -> ChatScreen()
|
||||||
2 -> SourcesScreen(viewModel)
|
2 -> SourcesScreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// === 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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AccountScreen(onDismiss: () -> Unit) {
|
fun AccountScreen(onDismiss: () -> Unit) {
|
||||||
Dialog(
|
Dialog(onDismissRequest = onDismiss, properties = DialogProperties(usePlatformDefaultWidth = false)) {
|
||||||
onDismissRequest = onDismiss,
|
|
||||||
properties = DialogProperties(usePlatformDefaultWidth = false)
|
|
||||||
) {
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
@ -404,6 +163,7 @@ fun AccountScreen(onDismiss: () -> Unit) {
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(48.dp))
|
Spacer(modifier = Modifier.height(48.dp))
|
||||||
|
|
||||||
Box {
|
Box {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@ -414,39 +174,448 @@ fun AccountScreen(onDismiss: () -> Unit) {
|
|||||||
) {
|
) {
|
||||||
Text("M", color = Color.White, fontWeight = FontWeight.Bold, fontSize = 40.sp)
|
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))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
Text("Hi, 202310715190!", fontSize = 20.sp)
|
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
|
@Composable
|
||||||
fun SettingsMenu(expanded: Boolean, onDismiss: () -> Unit) {
|
fun SettingsMenu(expanded: Boolean, onDismiss: () -> Unit) {
|
||||||
DropdownMenu(expanded = expanded, onDismissRequest = onDismiss) {
|
DropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = onDismiss
|
||||||
|
) {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text("NotebookLM Help") },
|
text = { Text("NotebookLM Help") },
|
||||||
onClick = { },
|
onClick = { /* TODO */ },
|
||||||
leadingIcon = { Icon(Icons.Default.HelpOutline, contentDescription = null) }
|
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
|
@Composable
|
||||||
fun GoogleAppsMenu(expanded: Boolean, onDismiss: () -> Unit) {
|
fun GoogleAppsMenu(expanded: Boolean, onDismiss: () -> Unit) {
|
||||||
val apps = listOf(
|
val apps = listOf(
|
||||||
"Account" to Icons.Default.AccountCircle,
|
"Account" to Icons.Default.AccountCircle, "Gmail" to Icons.Default.Mail, "Drive" to Icons.Default.Cloud,
|
||||||
"Gmail" to Icons.Default.Mail,
|
"Classroom" to Icons.Default.School, "Docs" to Icons.Default.Article, "Gemini" to Icons.Default.AutoAwesome,
|
||||||
"Drive" to Icons.Default.Cloud
|
"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
|
||||||
)
|
)
|
||||||
DropdownMenu(expanded = expanded, onDismissRequest = onDismiss) {
|
val appChunks = apps.chunked(3)
|
||||||
apps.forEach { (name, icon) ->
|
|
||||||
DropdownMenuItem(
|
DropdownMenu(
|
||||||
text = { Text(name) },
|
expanded = expanded,
|
||||||
onClick = { },
|
onDismissRequest = onDismiss,
|
||||||
leadingIcon = { Icon(icon, contentDescription = name) }
|
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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
31
app/src/main/java/com/example/notebook/data/DatabaseTest.kt
Normal file
31
app/src/main/java/com/example/notebook/data/DatabaseTest.kt
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,131 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,194 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user