desktop: files Drag & Drop support (#2843)
* desktop: files Drag&Drop support * reduce diff * move * move 2 * move 3 --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
4e27a4ea4f
commit
65391756ef
@@ -2,6 +2,7 @@ package chat.simplex.common.platform
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import com.google.accompanist.insets.navigationBarsWithImePadding
|
||||
|
||||
actual fun Modifier.navigationBarsWithImePadding(): Modifier = navigationBarsWithImePadding()
|
||||
@@ -14,3 +15,11 @@ actual fun ProvideWindowInsets(
|
||||
) {
|
||||
com.google.accompanist.insets.ProvideWindowInsets(content = content)
|
||||
}
|
||||
|
||||
@Composable
|
||||
actual fun Modifier.desktopOnExternalDrag(
|
||||
enabled: Boolean,
|
||||
onFiles: (List<String>) -> Unit,
|
||||
onImage: (Painter) -> Unit,
|
||||
onText: (String) -> Unit
|
||||
): Modifier = this
|
||||
|
||||
@@ -2,6 +2,7 @@ package chat.simplex.common.platform
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
|
||||
expect fun Modifier.navigationBarsWithImePadding(): Modifier
|
||||
|
||||
@@ -11,3 +12,11 @@ expect fun ProvideWindowInsets(
|
||||
windowInsetsAnimationsEnabled: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
)
|
||||
|
||||
@Composable
|
||||
expect fun Modifier.desktopOnExternalDrag(
|
||||
enabled: Boolean = true,
|
||||
onFiles: (List<String>) -> Unit = {},
|
||||
onImage: (Painter) -> Unit = {},
|
||||
onText: (String) -> Unit = {}
|
||||
): Modifier
|
||||
|
||||
@@ -11,8 +11,7 @@ import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.mapSaver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.*
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.*
|
||||
@@ -410,6 +409,31 @@ fun ChatLayout(
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.desktopOnExternalDrag(
|
||||
enabled = !composeState.value.attachmentDisabled && rememberUpdatedState(chat.userCanSend).value,
|
||||
onFiles = { paths ->
|
||||
val uris = paths.map { URI.create(it) }
|
||||
val groups = uris.groupBy { isImage(it) }
|
||||
val images = groups[true] ?: emptyList()
|
||||
val files = groups[false] ?: emptyList()
|
||||
if (images.isNotEmpty()) {
|
||||
composeState.processPickedMedia(images, null)
|
||||
} else if (files.isNotEmpty()) {
|
||||
composeState.processPickedFile(uris.first(), null)
|
||||
}
|
||||
},
|
||||
onImage = {
|
||||
val tmpFile = File.createTempFile("image", ".bmp", tmpDir)
|
||||
tmpFile.deleteOnExit()
|
||||
chatModel.filesToDelete.add(tmpFile)
|
||||
val uri = tmpFile.toURI()
|
||||
composeState.processPickedMedia(listOf(uri), null)
|
||||
},
|
||||
onText = {
|
||||
// Need to parse HTML in order to correctly display the content
|
||||
//composeState.value = composeState.value.copy(message = composeState.value.message + it)
|
||||
},
|
||||
)
|
||||
) {
|
||||
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
||||
ModalBottomSheetLayout(
|
||||
|
||||
@@ -124,6 +124,8 @@ data class ComposeState(
|
||||
}
|
||||
}
|
||||
|
||||
private val maxFileSize = getMaxFileSize(FileProtocol.XFTP)
|
||||
|
||||
sealed class RecordingState {
|
||||
object NotStarted: RecordingState()
|
||||
class Started(val filePath: String, val progressMs: Int = 0): RecordingState()
|
||||
@@ -155,6 +157,66 @@ expect fun AttachmentSelection(
|
||||
processPickedMedia: (List<URI>, String?) -> Unit
|
||||
)
|
||||
|
||||
fun MutableState<ComposeState>.processPickedFile(uri: URI?, text: String?) {
|
||||
if (uri != null) {
|
||||
val fileSize = getFileSize(uri)
|
||||
if (fileSize != null && fileSize <= maxFileSize) {
|
||||
val fileName = getFileName(uri)
|
||||
if (fileName != null) {
|
||||
value = value.copy(message = text ?: value.message, preview = ComposePreview.FilePreview(fileName, uri))
|
||||
}
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.large_file),
|
||||
String.format(generalGetString(MR.strings.maximum_supported_file_size), formatBytes(maxFileSize))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun MutableState<ComposeState>.processPickedMedia(uris: List<URI>, text: String?) {
|
||||
val content = ArrayList<UploadContent>()
|
||||
val imagesPreview = ArrayList<String>()
|
||||
uris.forEach { uri ->
|
||||
var bitmap: ImageBitmap?
|
||||
when {
|
||||
isImage(uri) -> {
|
||||
// Image
|
||||
val drawable = getDrawableFromUri(uri)
|
||||
bitmap = getBitmapFromUri(uri)
|
||||
if (isAnimImage(uri, drawable)) {
|
||||
// It's a gif or webp
|
||||
val fileSize = getFileSize(uri)
|
||||
if (fileSize != null && fileSize <= maxFileSize) {
|
||||
content.add(UploadContent.AnimatedImage(uri))
|
||||
} else {
|
||||
bitmap = null
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.large_file),
|
||||
String.format(generalGetString(MR.strings.maximum_supported_file_size), formatBytes(maxFileSize))
|
||||
)
|
||||
}
|
||||
} else {
|
||||
content.add(UploadContent.SimpleImage(uri))
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// Video
|
||||
val res = getBitmapFromVideo(uri)
|
||||
bitmap = res.preview
|
||||
val durationMs = res.duration
|
||||
content.add(UploadContent.Video(uri, durationMs?.div(1000)?.toInt() ?: 0))
|
||||
}
|
||||
}
|
||||
if (bitmap != null) {
|
||||
imagesPreview.add(resizeImageToStrSize(bitmap, maxDataSize = 14000))
|
||||
}
|
||||
}
|
||||
if (imagesPreview.isNotEmpty()) {
|
||||
value = value.copy(message = text ?: value.message, preview = ComposePreview.MediaPreview(imagesPreview, content))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ComposeView(
|
||||
chatModel: ChatModel,
|
||||
@@ -168,70 +230,11 @@ fun ComposeView(
|
||||
val pendingLinkUrl = rememberSaveable { mutableStateOf<String?>(null) }
|
||||
val cancelledLinks = rememberSaveable { mutableSetOf<String>() }
|
||||
val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get()
|
||||
val maxFileSize = getMaxFileSize(FileProtocol.XFTP)
|
||||
val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground)
|
||||
val textStyle = remember(MaterialTheme.colors.isLight) { mutableStateOf(smallFont) }
|
||||
val processPickedMedia = { uris: List<URI>, text: String? ->
|
||||
val content = ArrayList<UploadContent>()
|
||||
val imagesPreview = ArrayList<String>()
|
||||
uris.forEach { uri ->
|
||||
var bitmap: ImageBitmap?
|
||||
when {
|
||||
isImage(uri) -> {
|
||||
// Image
|
||||
val drawable = getDrawableFromUri(uri)
|
||||
bitmap = getBitmapFromUri(uri)
|
||||
if (isAnimImage(uri, drawable)) {
|
||||
// It's a gif or webp
|
||||
val fileSize = getFileSize(uri)
|
||||
if (fileSize != null && fileSize <= maxFileSize) {
|
||||
content.add(UploadContent.AnimatedImage(uri))
|
||||
} else {
|
||||
bitmap = null
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.large_file),
|
||||
String.format(generalGetString(MR.strings.maximum_supported_file_size), formatBytes(maxFileSize))
|
||||
)
|
||||
}
|
||||
} else {
|
||||
content.add(UploadContent.SimpleImage(uri))
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// Video
|
||||
val res = getBitmapFromVideo(uri)
|
||||
bitmap = res.preview
|
||||
val durationMs = res.duration
|
||||
content.add(UploadContent.Video(uri, durationMs?.div(1000)?.toInt() ?: 0))
|
||||
}
|
||||
}
|
||||
if (bitmap != null) {
|
||||
imagesPreview.add(resizeImageToStrSize(bitmap, maxDataSize = 14000))
|
||||
}
|
||||
}
|
||||
if (imagesPreview.isNotEmpty()) {
|
||||
composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.MediaPreview(imagesPreview, content))
|
||||
}
|
||||
}
|
||||
val processPickedFile = { uri: URI?, text: String? ->
|
||||
if (uri != null) {
|
||||
val fileSize = getFileSize(uri)
|
||||
if (fileSize != null && fileSize <= maxFileSize) {
|
||||
val fileName = getFileName(uri)
|
||||
if (fileName != null) {
|
||||
composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.FilePreview(fileName, uri))
|
||||
}
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.large_file),
|
||||
String.format(generalGetString(MR.strings.maximum_supported_file_size), formatBytes(maxFileSize))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
val recState: MutableState<RecordingState> = remember { mutableStateOf(RecordingState.NotStarted) }
|
||||
|
||||
AttachmentSelection(composeState, attachmentOption, processPickedFile, processPickedMedia)
|
||||
AttachmentSelection(composeState, attachmentOption, composeState::processPickedFile, composeState::processPickedMedia)
|
||||
|
||||
fun isSimplexLink(link: String): Boolean =
|
||||
link.startsWith("https://simplex.chat", true) || link.startsWith("http://simplex.chat", true)
|
||||
@@ -620,8 +623,8 @@ fun ComposeView(
|
||||
|
||||
when (val shared = chatModel.sharedContent.value) {
|
||||
is SharedContent.Text -> onMessageChange(shared.text)
|
||||
is SharedContent.Media -> processPickedMedia(shared.uris, shared.text)
|
||||
is SharedContent.File -> processPickedFile(shared.uri, shared.text)
|
||||
is SharedContent.Media -> composeState.processPickedMedia(shared.uris, shared.text)
|
||||
is SharedContent.File -> composeState.processPickedFile(shared.uri, shared.text)
|
||||
null -> {}
|
||||
}
|
||||
chatModel.sharedContent.value = null
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
package chat.simplex.common.platform
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.*
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
|
||||
actual fun Modifier.navigationBarsWithImePadding(): Modifier = this
|
||||
|
||||
@@ -13,3 +14,18 @@ actual fun ProvideWindowInsets(
|
||||
) {
|
||||
content()
|
||||
}
|
||||
|
||||
@Composable
|
||||
actual fun Modifier.desktopOnExternalDrag(
|
||||
enabled: Boolean,
|
||||
onFiles: (List<String>) -> Unit,
|
||||
onImage: (Painter) -> Unit,
|
||||
onText: (String) -> Unit
|
||||
): Modifier =
|
||||
onExternalDrag(enabled) {
|
||||
when(val data = it.dragData) {
|
||||
is DragData.FilesList -> onFiles(data.readFiles())
|
||||
is DragData.Image -> onImage(data.readImage())
|
||||
is DragData.Text -> onText(data.readText())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user