Tes Fungsi Database

This commit is contained in:
202310715297 RAIHAN ARIQ MUZAKKI 2025-11-13 11:07:11 +07:00
parent 793600dd5a
commit 5867c80588
5 changed files with 606 additions and 0 deletions

View File

@ -2,6 +2,13 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"

View File

@ -60,6 +60,17 @@ fun NotebookApp(viewModel: NotebookViewModel) {
var showSettingsMenu by remember { mutableStateOf(false) }
var showAccountScreen by remember { mutableStateOf(false) }
var chatInput by remember { mutableStateOf("") }
var selectedNotebookId by remember { mutableIntStateOf(-1) }
// Kalau ada notebook yang dipilih, tampilkan detail screen
if (selectedNotebookId != -1) {
com.example.notebook.ui.screens.NotebookDetailScreen(
viewModel = viewModel,
notebookId = selectedNotebookId,
onBack = { selectedNotebookId = -1 }
)
return
}
if (showAccountScreen) {
AccountScreen(onDismiss = { showAccountScreen = false })

View File

@ -0,0 +1,430 @@
package com.example.notebook.ui.screens
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
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.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
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.notebook.data.ChatMessageEntity
import com.example.notebook.data.NotebookEntity
import com.example.notebook.data.SourceEntity
import com.example.notebook.viewmodel.NotebookViewModel
import java.text.SimpleDateFormat
import java.util.*
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NotebookDetailScreen(
viewModel: NotebookViewModel,
notebookId: Int,
onBack: () -> Unit
) {
val context = LocalContext.current
val notebook by viewModel.currentNotebook.collectAsState()
val sources by viewModel.sources.collectAsState()
val chatMessages by viewModel.chatMessages.collectAsState()
var selectedTab by remember { mutableIntStateOf(0) }
val tabs = listOf("Chat", "Sources")
var chatInput by remember { mutableStateOf("") }
var showUploadMenu by remember { mutableStateOf(false) }
// File picker launcher
val filePickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent()
) { uri: Uri? ->
uri?.let {
viewModel.uploadFile(context, it, notebookId)
}
}
// Load notebook data
LaunchedEffect(notebookId) {
viewModel.selectNotebook(notebookId)
}
Scaffold(
topBar = {
TopAppBar(
title = {
Column {
Text(
text = notebook?.title ?: "Loading...",
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
if (notebook != null) {
Text(
text = "${sources.size} sources",
fontSize = 12.sp,
color = Color.Gray
)
}
}
},
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
}
},
actions = {
Box {
IconButton(onClick = { showUploadMenu = true }) {
Icon(Icons.Default.Add, contentDescription = "Upload")
}
DropdownMenu(
expanded = showUploadMenu,
onDismissRequest = { showUploadMenu = false }
) {
DropdownMenuItem(
text = { Text("Upload File") },
onClick = {
filePickerLauncher.launch("*/*")
showUploadMenu = false
},
leadingIcon = { Icon(Icons.Default.CloudUpload, null) }
)
}
}
}
)
},
bottomBar = {
if (selectedTab == 0) { // Chat tab
BottomAppBar {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
OutlinedTextField(
value = chatInput,
onValueChange = { chatInput = it },
placeholder = { Text("Ask anything about your sources...") },
modifier = Modifier.weight(1f)
)
IconButton(
onClick = {
if (chatInput.isNotBlank()) {
viewModel.sendUserMessage(notebookId, chatInput)
chatInput = ""
}
}
) {
Icon(Icons.AutoMirrored.Filled.Send, contentDescription = "Send")
}
}
}
}
}
) { padding ->
Column(modifier = Modifier.padding(padding)) {
TabRow(selectedTabIndex = selectedTab) {
tabs.forEachIndexed { index, title ->
Tab(
selected = selectedTab == index,
onClick = { selectedTab = index },
text = { Text(title) }
)
}
}
when (selectedTab) {
0 -> ChatTab(
messages = chatMessages,
sources = sources,
onUploadClick = { filePickerLauncher.launch("*/*") }
)
1 -> SourcesTab(
sources = sources,
onUploadClick = { filePickerLauncher.launch("*/*") },
onDeleteSource = { source ->
// TODO: Implement delete
}
)
}
}
}
}
@Composable
fun ChatTab(
messages: List<ChatMessageEntity>,
sources: List<SourceEntity>,
onUploadClick: () -> Unit
) {
if (messages.isEmpty()) {
// Empty state
Column(
modifier = Modifier
.fillMaxSize()
.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
Icons.Default.Chat,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = Color.Gray
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = if (sources.isEmpty()) "Upload sources to start chatting"
else "Ask me anything about your sources!",
style = MaterialTheme.typography.titleMedium,
textAlign = TextAlign.Center
)
if (sources.isEmpty()) {
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = onUploadClick) {
Icon(Icons.Default.CloudUpload, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text("Upload Source")
}
}
}
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(messages) { message ->
ChatBubble(message = message)
}
}
}
}
@Composable
fun ChatBubble(message: ChatMessageEntity) {
val dateFormat = SimpleDateFormat("HH:mm", Locale.getDefault())
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = if (message.isUserMessage) Arrangement.End else Arrangement.Start
) {
if (!message.isUserMessage) {
// AI Avatar
Box(
modifier = Modifier
.size(32.dp)
.clip(CircleShape)
.background(Color(0xFF6200EE)),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Default.AutoAwesome,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(20.dp)
)
}
Spacer(modifier = Modifier.width(8.dp))
}
Column(
modifier = Modifier.widthIn(max = 280.dp),
horizontalAlignment = if (message.isUserMessage) Alignment.End else Alignment.Start
) {
Card(
colors = CardDefaults.cardColors(
containerColor = if (message.isUserMessage)
Color(0xFF6200EE)
else
Color(0xFFF0F0F0)
),
shape = RoundedCornerShape(
topStart = 16.dp,
topEnd = 16.dp,
bottomStart = if (message.isUserMessage) 16.dp else 4.dp,
bottomEnd = if (message.isUserMessage) 4.dp else 16.dp
)
) {
Text(
text = message.message,
modifier = Modifier.padding(12.dp),
color = if (message.isUserMessage) Color.White else Color.Black
)
}
Text(
text = dateFormat.format(Date(message.timestamp)),
fontSize = 10.sp,
color = Color.Gray,
modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp)
)
}
if (message.isUserMessage) {
Spacer(modifier = Modifier.width(8.dp))
// User Avatar
Box(
modifier = Modifier
.size(32.dp)
.clip(CircleShape)
.background(Color.Magenta),
contentAlignment = Alignment.Center
) {
Text("M", color = Color.White, fontWeight = FontWeight.Bold)
}
}
}
}
@Composable
fun SourcesTab(
sources: List<SourceEntity>,
onUploadClick: () -> Unit,
onDeleteSource: (SourceEntity) -> Unit
) {
if (sources.isEmpty()) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
Icons.Default.Description,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = Color.Gray
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "No sources yet",
style = MaterialTheme.typography.titleMedium,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Upload documents to get started",
style = MaterialTheme.typography.bodyMedium,
color = Color.Gray,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = onUploadClick) {
Icon(Icons.Default.CloudUpload, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text("Upload Source")
}
}
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(sources) { source ->
SourceCard(
source = source,
onDelete = { onDeleteSource(source) }
)
}
}
}
}
@Composable
fun SourceCard(source: SourceEntity, onDelete: () -> Unit) {
val dateFormat = SimpleDateFormat("dd MMM yyyy", Locale.getDefault())
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = Color(0xFFF8F9FA))
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
// File icon
Box(
modifier = Modifier
.size(40.dp)
.clip(RoundedCornerShape(8.dp))
.background(
when (source.fileType) {
"PDF" -> Color(0xFFE53935)
"Image" -> Color(0xFF43A047)
"Text", "Markdown" -> Color(0xFF1E88E5)
"Audio" -> Color(0xFFFF6F00)
else -> Color.Gray
}.copy(alpha = 0.1f)
),
contentAlignment = Alignment.Center
) {
Icon(
when (source.fileType) {
"PDF" -> Icons.Default.PictureAsPdf
"Image" -> Icons.Default.Image
"Text", "Markdown" -> Icons.Default.Description
"Audio" -> Icons.Default.AudioFile
else -> Icons.Default.InsertDriveFile
},
contentDescription = null,
tint = when (source.fileType) {
"PDF" -> Color(0xFFE53935)
"Image" -> Color(0xFF43A047)
"Text", "Markdown" -> Color(0xFF1E88E5)
"Audio" -> Color(0xFFFF6F00)
else -> Color.Gray
}
)
}
Spacer(modifier = Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = source.fileName,
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium,
maxLines = 1
)
Spacer(modifier = Modifier.height(4.dp))
Row {
Text(
text = source.fileType,
style = MaterialTheme.typography.bodySmall,
color = Color.Gray
)
Text(
text = "${dateFormat.format(Date(source.uploadedAt))}",
style = MaterialTheme.typography.bodySmall,
color = Color.Gray
)
}
}
IconButton(onClick = onDelete) {
Icon(Icons.Default.Delete, contentDescription = "Delete", tint = Color.Gray)
}
}
}
}

View File

@ -0,0 +1,114 @@
package com.example.notebook.utils
import android.content.Context
import android.net.Uri
import android.provider.OpenableColumns
import java.io.File
import java.io.FileOutputStream
/**
* Helper untuk handle file operations
*/
object FileHelper {
/**
* Copy file dari URI ke internal storage
* Return: Path file yang disimpan
*/
fun copyFileToInternalStorage(context: Context, uri: Uri, notebookId: Int): String? {
try {
val fileName = getFileName(context, uri) ?: "file_${System.currentTimeMillis()}"
// Buat folder untuk notebook ini
val notebookDir = File(context.filesDir, "notebooks/$notebookId")
if (!notebookDir.exists()) {
notebookDir.mkdirs()
}
// Buat file baru
val destinationFile = File(notebookDir, fileName)
// Copy file
context.contentResolver.openInputStream(uri)?.use { input ->
FileOutputStream(destinationFile).use { output ->
input.copyTo(output)
}
}
return destinationFile.absolutePath
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
/**
* Ambil nama file dari URI
*/
fun getFileName(context: Context, uri: Uri): String? {
var fileName: String? = null
context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if (nameIndex >= 0) {
fileName = cursor.getString(nameIndex)
}
}
}
return fileName
}
/**
* Deteksi tipe file dari extension
*/
fun getFileType(fileName: String): String {
return when (fileName.substringAfterLast('.').lowercase()) {
"pdf" -> "PDF"
"txt" -> "Text"
"md", "markdown" -> "Markdown"
"jpg", "jpeg", "png", "gif" -> "Image"
"mp3", "wav", "m4a" -> "Audio"
"mp4", "avi", "mkv" -> "Video"
else -> "Unknown"
}
}
/**
* Baca text dari file
*/
fun readTextFromFile(filePath: String): String? {
return try {
File(filePath).readText()
} catch (e: Exception) {
e.printStackTrace()
null
}
}
/**
* Format ukuran file
*/
fun formatFileSize(size: Long): String {
if (size < 1024) return "$size B"
val kb = size / 1024.0
if (kb < 1024) return "%.2f KB".format(kb)
val mb = kb / 1024.0
if (mb < 1024) return "%.2f MB".format(mb)
val gb = mb / 1024.0
return "%.2f GB".format(gb)
}
/**
* Hapus file
*/
fun deleteFile(filePath: String): Boolean {
return try {
File(filePath).delete()
} catch (e: Exception) {
e.printStackTrace()
false
}
}
}

View File

@ -146,6 +146,50 @@ class NotebookViewModel(application: Application) : AndroidViewModel(application
}
}
/**
* Handle file upload dari URI
*/
fun uploadFile(context: android.content.Context, uri: android.net.Uri, notebookId: Int) {
viewModelScope.launch {
_isLoading.value = true
try {
val fileName = com.example.notebook.utils.FileHelper.getFileName(context, uri) ?: "unknown"
val filePath = com.example.notebook.utils.FileHelper.copyFileToInternalStorage(context, uri, notebookId)
if (filePath != null) {
val fileType = com.example.notebook.utils.FileHelper.getFileType(fileName)
repository.addSource(notebookId, fileName, fileType, filePath)
println("✅ File berhasil diupload: $fileName")
} else {
println("❌ Error: Gagal menyimpan file")
}
} catch (e: Exception) {
println("❌ Error upload file: ${e.message}")
} finally {
_isLoading.value = false
}
}
}
/**
* Hapus source
*/
fun deleteSource(source: com.example.notebook.data.SourceEntity) {
viewModelScope.launch {
_isLoading.value = true
try {
repository.deleteSource(source)
// Hapus file fisik juga
com.example.notebook.utils.FileHelper.deleteFile(source.filePath)
println("✅ Source berhasil dihapus: ${source.fileName}")
} catch (e: Exception) {
println("❌ Error menghapus source: ${e.message}")
} finally {
_isLoading.value = false
}
}
}
// === CHAT FUNCTIONS ===
/**