feat: attachment picker menu — camera, gallery, and file options
Replaced the generic file picker with a dropdown menu offering three options: take photo (camera preview), pick from gallery (image/*), or choose any file (*/*). Camera captures are compressed as JPEG and uploaded like any other file. Upload logic extracted into shared uploadPickedFile helper.
This commit is contained in:
parent
5c54b9b9e3
commit
09489318bf
3 changed files with 66 additions and 18 deletions
|
|
@ -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<PendingFile>() }
|
||||
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))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@
|
|||
<string name="chat_remove">Remove</string>
|
||||
<string name="chat_error">Error</string>
|
||||
<string name="chat_open_menu">Open menu</string>
|
||||
<string name="attach_camera">Take Photo</string>
|
||||
<string name="attach_gallery">Photo & Video</string>
|
||||
<string name="attach_file">Choose File</string>
|
||||
|
||||
<!-- Stream blocks -->
|
||||
<string name="stream_reasoning">Reasoning</string>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@
|
|||
<string name="chat_remove">Entfernen</string>
|
||||
<string name="chat_error">Fehler</string>
|
||||
<string name="chat_open_menu">Menü öffnen</string>
|
||||
<string name="attach_camera">Foto aufnehmen</string>
|
||||
<string name="attach_gallery">Foto & Video</string>
|
||||
<string name="attach_file">Datei auswählen</string>
|
||||
|
||||
<!-- Stream blocks -->
|
||||
<string name="stream_reasoning">Gedankengang</string>
|
||||
|
|
|
|||
Loading…
Reference in a new issue