Tes Fungsi Database
This commit is contained in:
parent
793600dd5a
commit
5867c80588
@ -2,6 +2,13 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
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
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
|||||||
@ -60,6 +60,17 @@ fun NotebookApp(viewModel: NotebookViewModel) {
|
|||||||
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("") }
|
||||||
|
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) {
|
if (showAccountScreen) {
|
||||||
AccountScreen(onDismiss = { showAccountScreen = false })
|
AccountScreen(onDismiss = { showAccountScreen = false })
|
||||||
|
|||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
114
app/src/main/java/com/example/notebook/utils/FileHelper.kt
Normal file
114
app/src/main/java/com/example/notebook/utils/FileHelper.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 ===
|
// === CHAT FUNCTIONS ===
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user