diff --git a/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt b/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt index 9f4ffed..c24b24f 100644 --- a/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt +++ b/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt @@ -72,10 +72,17 @@ import dev.kaizen.app.settings.SettingsScreen import dev.kaizen.app.settings.SettingsViewModel import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.material.icons.rounded.CameraAlt +import androidx.compose.material.icons.rounded.Image +import androidx.compose.material.icons.automirrored.rounded.InsertDriveFile +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.LifecycleResumeEffect import kotlinx.coroutines.async import kotlinx.coroutines.launch +import java.io.ByteArrayOutputStream // Screen enumeration for modular state-driven navigation (Zero Technical Debt) enum class AppScreen { Chat, Settings } @@ -123,17 +130,14 @@ fun ChatScreen( val pendingFiles = remember { mutableStateListOf() } val context = LocalContext.current - val filePicker = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> - uri ?: return@rememberLauncherForActivityResult - val cfg = session.config ?: return@rememberLauncherForActivityResult - val cr = context.contentResolver - val mimeType = cr.getType(uri) ?: "application/octet-stream" - val fileName = uri.lastPathSegment?.substringAfterLast('/') ?: "file" - val bytes = cr.openInputStream(uri)?.use { it.readBytes() } ?: return@rememberLauncherForActivityResult - val pf = PendingFile(name = fileName, mimeType = mimeType, bytes = bytes) + var showAttachMenu by remember { mutableStateOf(false) } + + fun uploadPickedFile(name: String, mimeType: String, bytes: ByteArray) { + val cfg = session.config ?: return + val pf = PendingFile(name = name, mimeType = mimeType, bytes = bytes) pendingFiles.add(pf) scope.launch { - when (val r = KaizenApi.uploadFile(cfg.baseUrl, cfg.token, fileName, mimeType, bytes)) { + when (val r = KaizenApi.uploadFile(cfg.baseUrl, cfg.token, name, mimeType, bytes)) { is FetchResult.Ok -> { val idx = pendingFiles.indexOfFirst { it.id == pf.id } if (idx >= 0) pendingFiles[idx] = pendingFiles[idx].copy(uploaded = r.data, uploading = false) @@ -146,6 +150,22 @@ fun ChatScreen( } } + val filePicker = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> + uri ?: return@rememberLauncherForActivityResult + val cr = context.contentResolver + val mimeType = cr.getType(uri) ?: "application/octet-stream" + val fileName = uri.lastPathSegment?.substringAfterLast('/') ?: "file" + val bytes = cr.openInputStream(uri)?.use { it.readBytes() } ?: return@rememberLauncherForActivityResult + uploadPickedFile(fileName, mimeType, bytes) + } + + val cameraPicker = rememberLauncherForActivityResult(ActivityResultContracts.TakePicturePreview()) { bitmap -> + bitmap ?: return@rememberLauncherForActivityResult + val stream = ByteArrayOutputStream() + bitmap.compress(android.graphics.Bitmap.CompressFormat.JPEG, 92, stream) + uploadPickedFile("photo_${System.currentTimeMillis()}.jpg", "image/jpeg", stream.toByteArray()) + } + LaunchedEffect(session.config?.baseUrl, session.config?.token) { val cfg = session.config ?: return@LaunchedEffect loadError = null @@ -525,15 +545,37 @@ fun ChatScreen( }, ) Spacer(Modifier.height(10.dp)) - ChatInput( - value = input, - onValueChange = { input = it }, - onSend = { send(input) }, - enabled = !isStreaming, - pendingFiles = pendingFiles, - onAddClick = { filePicker.launch("*/*") }, - onRemoveFile = { id -> pendingFiles.removeAll { it.id == id } }, - ) + Box { + ChatInput( + value = input, + onValueChange = { input = it }, + onSend = { send(input) }, + enabled = !isStreaming, + pendingFiles = pendingFiles, + onAddClick = { showAttachMenu = true }, + onRemoveFile = { id -> pendingFiles.removeAll { it.id == id } }, + ) + DropdownMenu( + expanded = showAttachMenu, + onDismissRequest = { showAttachMenu = false }, + ) { + DropdownMenuItem( + text = { Text(stringResource(R.string.attach_camera)) }, + onClick = { showAttachMenu = false; cameraPicker.launch(null) }, + leadingIcon = { Icon(Icons.Rounded.CameraAlt, null, modifier = Modifier.size(20.dp)) }, + ) + DropdownMenuItem( + text = { Text(stringResource(R.string.attach_gallery)) }, + onClick = { showAttachMenu = false; filePicker.launch("image/*") }, + leadingIcon = { Icon(Icons.Rounded.Image, null, modifier = Modifier.size(20.dp)) }, + ) + DropdownMenuItem( + text = { Text(stringResource(R.string.attach_file)) }, + onClick = { showAttachMenu = false; filePicker.launch("*/*") }, + leadingIcon = { Icon(Icons.AutoMirrored.Rounded.InsertDriveFile, null, modifier = Modifier.size(20.dp)) }, + ) + } + } Spacer(Modifier.height(16.dp)) } diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index eee8728..bef20bf 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -18,6 +18,9 @@ Remove Error Open menu + Take Photo + Photo & Video + Choose File Reasoning diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1a9b95e..49873d9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -20,6 +20,9 @@ Entfernen Fehler Menü öffnen + Foto aufnehmen + Foto & Video + Datei auswählen Gedankengang