attachments: preview bar + combined send (Android + desktop)
Android: file picker → pending preview bar (name+size+X) →
combined send with text; cache copy, save dialog for downloads
Desktop: attach stores pending, preview bar before send,
combined with text in onSendClicked
C: attachment_sender.c — include filename in msg data for display
This commit is contained in:
1 parent
e3df5008da
commit
6a02a16543
7 files changed
+374
-131
No files matched your search
+151
-105
@@ -25,6 +25,7 @@ import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.utun.chat.data.Message
|
||||
@@ -36,7 +37,8 @@ import kotlinx.coroutines.delay
|
||||
fun MessageBubble(message: Message, playbackState: ChatViewModel.PlaybackState = ChatViewModel.PlaybackState(),
|
||||
onPlayVoice: (String, Int) -> Unit = { _, _ -> },
|
||||
onSeekVoice: (String, Int, Int) -> Unit = { _, _, _ -> },
|
||||
onDownloadFile: (Message) -> Unit = {}) {
|
||||
onDownloadFile: (Message) -> Unit = {},
|
||||
onOpenFile: (Message) -> Unit = {}) {
|
||||
val bgColor = if (message.isOutgoing) Color(0xFF4A90D9) else Color(0xFFE8E8E8)
|
||||
val textColor = if (message.isOutgoing) Color.White else Color.Black
|
||||
val alignment = if (message.isOutgoing) Arrangement.End else Arrangement.Start
|
||||
@@ -63,10 +65,8 @@ fun MessageBubble(message: Message, playbackState: ChatViewModel.PlaybackState =
|
||||
|
||||
when {
|
||||
isVoice -> VoiceBubble(message, textColor, isActive, playbackState, onPlayVoice, onSeekVoice)
|
||||
isMedia -> FileBubble(message, textColor, onDownloadFile)
|
||||
isMedia -> FileBubble(message, textColor, onDownloadFile, onOpenFile)
|
||||
else -> {
|
||||
Text(message.author, fontSize = 13.sp, fontWeight = FontWeight.Bold, color = textColor.copy(alpha = 0.7f))
|
||||
Spacer(Modifier.height(2.dp))
|
||||
Text(message.text, fontSize = 17.sp, color = textColor)
|
||||
}
|
||||
}
|
||||
@@ -171,7 +171,8 @@ private fun VoiceBubble(message: Message, textColor: Color, isActive: Boolean,
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FileBubble(message: Message, textColor: Color, onDownload: (Message) -> Unit = {}) {
|
||||
private fun FileBubble(message: Message, textColor: Color, onDownload: (Message) -> Unit = {},
|
||||
onOpen: (Message) -> Unit = {}) {
|
||||
val state = message.downloadState
|
||||
val stateIcon = when {
|
||||
state == "downloaded" -> "\u2705"
|
||||
@@ -179,13 +180,18 @@ private fun FileBubble(message: Message, textColor: Color, onDownload: (Message)
|
||||
else -> "\u2B07\uFE0F"
|
||||
}
|
||||
val stateText = when {
|
||||
state == "downloaded" -> "Downloaded"
|
||||
state == "downloaded" -> "Tap to save"
|
||||
state == "downloading" -> "Downloading..."
|
||||
else -> "Tap to download"
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.clickable { if (state.isEmpty()) onDownload(message) }
|
||||
modifier = Modifier.clickable {
|
||||
when {
|
||||
state == "downloaded" -> onOpen(message)
|
||||
state.isEmpty() -> onDownload(message)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(stateIcon, fontSize = 18.sp)
|
||||
@@ -235,6 +241,9 @@ fun InputBar(
|
||||
onVoiceSend: () -> Unit = {},
|
||||
onVoiceCancel: () -> Unit = {},
|
||||
onAttachClicked: () -> Unit = {},
|
||||
pendingAttachmentName: String = "",
|
||||
pendingAttachmentSize: Long = 0L,
|
||||
onClearAttachment: () -> Unit = {},
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val ctx = LocalContext.current
|
||||
@@ -251,126 +260,163 @@ fun InputBar(
|
||||
hasMicPerm = granted
|
||||
}
|
||||
|
||||
val filePicker = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { _ -> }
|
||||
val hasAttachment = pendingAttachmentName.isNotEmpty()
|
||||
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth().padding(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
/* Attach button */
|
||||
if (!isRecording) {
|
||||
IconButton(onClick = { filePicker.launch("*/*") }, enabled = enabled) {
|
||||
Text("\uD83D\uDCCE", fontSize = 20.sp)
|
||||
}
|
||||
} else {
|
||||
Spacer(Modifier.width(4.dp))
|
||||
}
|
||||
|
||||
/* Recording overlay or text input */
|
||||
if (isRecording) {
|
||||
Column(modifier = modifier.fillMaxWidth().padding(8.dp)) {
|
||||
/* Attachment preview bar */
|
||||
if (hasAttachment) {
|
||||
Surface(
|
||||
modifier = Modifier.weight(1f).height(48.dp),
|
||||
modifier = Modifier.fillMaxWidth().height(36.dp).padding(bottom = 4.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
color = Color(0xFFE53935)
|
||||
color = MaterialTheme.colorScheme.primaryContainer
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxSize().padding(horizontal = 12.dp),
|
||||
modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text("\uD83D\uDCCE", fontSize = 14.sp)
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text(
|
||||
text = "\uD83D\uDD34 ${formatDuration(recordingDurationMs)}",
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 16.sp,
|
||||
pendingAttachmentName,
|
||||
fontSize = 14.sp,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
val lvlPct = (recordingLevel * 40f).coerceIn(0f, 1f)
|
||||
LinearProgressIndicator(
|
||||
progress = { lvlPct },
|
||||
modifier = Modifier.width(60.dp).height(4.dp).padding(end = 8.dp),
|
||||
color = Color.White,
|
||||
trackColor = Color.White.copy(alpha = 0.3f),
|
||||
)
|
||||
if (pendingAttachmentSize > 0) {
|
||||
Text(
|
||||
formatFileSize(pendingAttachmentSize),
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.6f)
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
}
|
||||
IconButton(
|
||||
onClick = onClearAttachment,
|
||||
modifier = Modifier.size(24.dp)
|
||||
) {
|
||||
Text("\u2715", fontSize = 14.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
OutlinedTextField(
|
||||
value = text,
|
||||
onValueChange = onTextChange,
|
||||
enabled = enabled,
|
||||
modifier = Modifier.weight(1f),
|
||||
placeholder = { Text("Message...") },
|
||||
maxLines = 4
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.width(4.dp))
|
||||
|
||||
if (isRecording && isRecordingLocked) {
|
||||
/* Locked: cancel + send voice */
|
||||
IconButton(onClick = onVoiceCancel) {
|
||||
Text("\u2715", fontSize = 20.sp, color = Color(0xFFE53935))
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
/* Attach button */
|
||||
if (!isRecording) {
|
||||
IconButton(onClick = onAttachClicked, enabled = enabled) {
|
||||
Text("\uD83D\uDCCE", fontSize = 20.sp)
|
||||
}
|
||||
} else {
|
||||
Spacer(Modifier.width(4.dp))
|
||||
}
|
||||
IconButton(onClick = onVoiceSend) {
|
||||
Text("\u25B6", fontSize = 20.sp, color = Color(0xFF4CAF50))
|
||||
|
||||
/* Recording overlay or text input */
|
||||
if (isRecording) {
|
||||
Surface(
|
||||
modifier = Modifier.weight(1f).height(48.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
color = Color(0xFFE53935)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxSize().padding(horizontal = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "\uD83D\uDD34 ${formatDuration(recordingDurationMs)}",
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 16.sp,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
val lvlPct = (recordingLevel * 40f).coerceIn(0f, 1f)
|
||||
LinearProgressIndicator(
|
||||
progress = { lvlPct },
|
||||
modifier = Modifier.width(60.dp).height(4.dp).padding(end = 8.dp),
|
||||
color = Color.White,
|
||||
trackColor = Color.White.copy(alpha = 0.3f),
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
OutlinedTextField(
|
||||
value = text,
|
||||
onValueChange = onTextChange,
|
||||
enabled = enabled,
|
||||
modifier = Modifier.weight(1f),
|
||||
placeholder = { Text("Message...") },
|
||||
maxLines = 4
|
||||
)
|
||||
}
|
||||
} else if (!isRecording && text.isNotBlank()) {
|
||||
/* Send button — round, primary color */
|
||||
val sendColor = MaterialTheme.colorScheme.primary
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(44.dp)
|
||||
.background(sendColor, CircleShape)
|
||||
.clickable { onSend() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("\u27A4", fontSize = 22.sp, color = Color.White)
|
||||
}
|
||||
} else {
|
||||
/* PTT Box — podcast/recording, same identity in idle and holding */
|
||||
val pttBg = if (isRecording) Color(0xFFE53935) else Color.Transparent
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.background(pttBg, RoundedCornerShape(4.dp))
|
||||
.pointerInput(hasMicPerm) {
|
||||
awaitPointerEventScope {
|
||||
while (true) {
|
||||
val down = awaitPointerEvent()
|
||||
val ch = down.changes.firstOrNull() ?: continue
|
||||
if (!ch.pressed) continue
|
||||
ch.consume()
|
||||
|
||||
if (!hasMicPerm) {
|
||||
permLauncher.launch(Manifest.permission.RECORD_AUDIO)
|
||||
continue
|
||||
}
|
||||
|
||||
onPttPressed()
|
||||
var locked = false
|
||||
var totalDragY = 0f
|
||||
var lastY = ch.position.y
|
||||
Spacer(Modifier.width(4.dp))
|
||||
|
||||
if (isRecording && isRecordingLocked) {
|
||||
/* Locked: cancel + send voice */
|
||||
IconButton(onClick = onVoiceCancel) {
|
||||
Text("\u2715", fontSize = 20.sp, color = Color(0xFFE53935))
|
||||
}
|
||||
IconButton(onClick = onVoiceSend) {
|
||||
Text("\u25B6", fontSize = 20.sp, color = Color(0xFF4CAF50))
|
||||
}
|
||||
} else if (!isRecording && (text.isNotBlank() || hasAttachment)) {
|
||||
/* Send button — round, primary color */
|
||||
val sendColor = MaterialTheme.colorScheme.primary
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(44.dp)
|
||||
.background(sendColor, CircleShape)
|
||||
.clickable { onSend() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("\u27A4", fontSize = 22.sp, color = Color.White)
|
||||
}
|
||||
} else {
|
||||
/* PTT Box — podcast/recording, same identity in idle and holding */
|
||||
val pttBg = if (isRecording) Color(0xFFE53935) else Color.Transparent
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.background(pttBg, RoundedCornerShape(4.dp))
|
||||
.pointerInput(hasMicPerm) {
|
||||
awaitPointerEventScope {
|
||||
while (true) {
|
||||
val event = awaitPointerEvent()
|
||||
val change = event.changes.firstOrNull() ?: break
|
||||
if (change.pressed) {
|
||||
val dy = change.position.y - lastY
|
||||
lastY = change.position.y
|
||||
totalDragY += dy
|
||||
if (totalDragY < -lockThresholdPx && !locked) { locked = true; onPttLock() }
|
||||
change.consume()
|
||||
} else {
|
||||
change.consume(); break
|
||||
val down = awaitPointerEvent()
|
||||
val ch = down.changes.firstOrNull() ?: continue
|
||||
if (!ch.pressed) continue
|
||||
ch.consume()
|
||||
|
||||
if (!hasMicPerm) {
|
||||
permLauncher.launch(Manifest.permission.RECORD_AUDIO)
|
||||
continue
|
||||
}
|
||||
|
||||
onPttPressed()
|
||||
var locked = false
|
||||
var totalDragY = 0f
|
||||
var lastY = ch.position.y
|
||||
|
||||
while (true) {
|
||||
val event = awaitPointerEvent()
|
||||
val change = event.changes.firstOrNull() ?: break
|
||||
if (change.pressed) {
|
||||
val dy = change.position.y - lastY
|
||||
lastY = change.position.y
|
||||
totalDragY += dy
|
||||
if (totalDragY < -lockThresholdPx && !locked) { locked = true; onPttLock() }
|
||||
change.consume()
|
||||
} else {
|
||||
change.consume(); break
|
||||
}
|
||||
}
|
||||
onPttReleased()
|
||||
}
|
||||
onPttReleased()
|
||||
}
|
||||
}
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("\uD83C\uDFA4", fontSize = 20.sp, color = if (isRecording) Color.White else Color.Unspecified)
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("\uD83C\uDFA4", fontSize = 20.sp, color = if (isRecording) Color.White else Color.Unspecified)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
package com.utun.chat.ui.screens
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
@@ -10,6 +15,7 @@ import androidx.compose.material.icons.filled.People
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.utun.chat.data.Channel
|
||||
import com.utun.chat.data.Message
|
||||
@@ -17,7 +23,10 @@ import com.utun.chat.data.NativeLib
|
||||
import com.utun.chat.ui.components.InputBar
|
||||
import com.utun.chat.ui.components.MessageBubble
|
||||
import com.utun.chat.viewmodel.ChatViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -36,6 +45,47 @@ fun ChatScreen(
|
||||
val isRecordingLocked by viewModel.isRecordingLocked.collectAsState()
|
||||
val recordingDurationMs by viewModel.recordingDurationMs.collectAsState()
|
||||
var recordingLevel by remember { mutableStateOf(0f) }
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
/* pending attachment state */
|
||||
var pendingAttachUri by remember { mutableStateOf<Uri?>(null) }
|
||||
var pendingAttachName by remember { mutableStateOf("") }
|
||||
var pendingAttachSize by remember { mutableStateOf(0L) }
|
||||
|
||||
/* file picker */
|
||||
val filePickerLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
||||
if (uri != null) {
|
||||
val cr: ContentResolver = context.contentResolver
|
||||
var name = "file"
|
||||
var size = 0L
|
||||
cr.query(uri, null, null, null, null)?.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
val nameIdx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
val sizeIdx = cursor.getColumnIndex(OpenableColumns.SIZE)
|
||||
if (nameIdx >= 0) name = cursor.getString(nameIdx) ?: name
|
||||
if (sizeIdx >= 0) size = cursor.getLong(sizeIdx)
|
||||
}
|
||||
}
|
||||
pendingAttachUri = uri
|
||||
pendingAttachName = name
|
||||
pendingAttachSize = size
|
||||
}
|
||||
}
|
||||
|
||||
/* save dialog for downloaded files */
|
||||
var pendingSaveFilePath by remember { mutableStateOf<String?>(null) }
|
||||
val saveFileLauncher = rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("*/*")) { saveUri ->
|
||||
if (saveUri != null) {
|
||||
val filePath = pendingSaveFilePath
|
||||
pendingSaveFilePath = null
|
||||
if (filePath != null) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
viewModel.saveDownloadedFile(filePath, saveUri)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Poll peak level during recording */
|
||||
LaunchedEffect(isRecording) {
|
||||
@@ -73,7 +123,13 @@ fun ChatScreen(
|
||||
MessageBubble(messages[idx], playbackState = playbackState,
|
||||
onPlayVoice = { path, durMs -> viewModel.playVoiceMessage(path, durMs) },
|
||||
onSeekVoice = { path, seekMs, durMs -> viewModel.seekVoice(path, seekMs, durMs) },
|
||||
onDownloadFile = { msg -> viewModel.downloadAttachment(msg) })
|
||||
onDownloadFile = { msg -> viewModel.downloadAttachment(msg) },
|
||||
onOpenFile = { msg ->
|
||||
if (msg.filePath.isNotEmpty()) {
|
||||
pendingSaveFilePath = msg.filePath
|
||||
saveFileLauncher.launch(msg.text) /* suggest original filename */
|
||||
}
|
||||
})
|
||||
}
|
||||
item { Spacer(Modifier.height(8.dp)) }
|
||||
}
|
||||
@@ -81,8 +137,23 @@ fun ChatScreen(
|
||||
text = text,
|
||||
onTextChange = { text = it },
|
||||
onSend = {
|
||||
onSendMessage(text)
|
||||
text = ""
|
||||
val attachUri = pendingAttachUri
|
||||
val attachName = pendingAttachName
|
||||
pendingAttachUri = null
|
||||
pendingAttachName = ""
|
||||
pendingAttachSize = 0L
|
||||
if (attachUri != null) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val cachePath = viewModel.copyUriToCache(context, attachUri, attachName)
|
||||
if (cachePath != null) {
|
||||
viewModel.sendAttachment(cachePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (text.isNotBlank()) {
|
||||
onSendMessage(text)
|
||||
text = ""
|
||||
}
|
||||
},
|
||||
enabled = true,
|
||||
isRecording = isRecording,
|
||||
@@ -99,7 +170,14 @@ fun ChatScreen(
|
||||
viewModel.cancelVoiceRecording()
|
||||
recordingLevel = 0f
|
||||
},
|
||||
onAttachClicked = {},
|
||||
onAttachClicked = { filePickerLauncher.launch("*/*") },
|
||||
pendingAttachmentName = pendingAttachName,
|
||||
pendingAttachmentSize = pendingAttachSize,
|
||||
onClearAttachment = {
|
||||
pendingAttachUri = null
|
||||
pendingAttachName = ""
|
||||
pendingAttachSize = 0L
|
||||
},
|
||||
modifier = Modifier.navigationBarsPadding()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package com.utun.chat.viewmodel
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
@@ -12,6 +15,8 @@ import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
class ChatViewModel : ViewModel() {
|
||||
|
||||
@@ -443,6 +448,39 @@ class ChatViewModel : ViewModel() {
|
||||
})
|
||||
}
|
||||
|
||||
fun copyUriToCache(context: Context, uri: Uri, fileName: String): String? {
|
||||
try {
|
||||
val cacheDir = File(context.cacheDir, "attachments")
|
||||
cacheDir.mkdirs()
|
||||
val cacheFile = File(cacheDir, fileName)
|
||||
context.contentResolver.openInputStream(uri)?.use { input ->
|
||||
FileOutputStream(cacheFile).use { output -> input.copyTo(output) }
|
||||
} ?: return null
|
||||
LogManager.addLog("INFO", "VM", "copied attachment to cache: ${cacheFile.absolutePath}")
|
||||
return cacheFile.absolutePath
|
||||
} catch (e: Exception) {
|
||||
LogManager.addLog("ERROR", "VM", "copyUriToCache failed: ${e.message}")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun saveDownloadedFile(srcPath: String, saveUri: Uri) {
|
||||
try {
|
||||
val src = File(srcPath)
|
||||
if (!src.exists()) {
|
||||
LogManager.addLog("ERROR", "VM", "saveDownloadedFile: src not found $srcPath")
|
||||
return
|
||||
}
|
||||
val ctx = ChatApplication.instance
|
||||
ctx.contentResolver.openOutputStream(saveUri)?.use { output ->
|
||||
src.inputStream().use { input -> input.copyTo(output) }
|
||||
}
|
||||
LogManager.addLog("INFO", "VM", "file saved: $srcPath -> $saveUri")
|
||||
} catch (e: Exception) {
|
||||
LogManager.addLog("ERROR", "VM", "saveDownloadedFile failed: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
fun clearState() {
|
||||
repo?.close()
|
||||
repo = null
|
||||
|
||||
Reference in new issue
Block a user