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:
Stanislav Dmitrenko
2023-08-03 18:24:43 +03:00
committed by GitHub
parent 4e27a4ea4f
commit 65391756ef
5 changed files with 126 additions and 65 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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(

View File

@@ -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

View File

@@ -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())
}
}