mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 18:39:55 +02:00
feat(desktop): polish Linux native experience
This commit is contained in:
@@ -2,6 +2,12 @@ package com.vnidrop.app.core
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import io.github.vinceglb.filekit.FileKit
|
||||
import io.github.vinceglb.filekit.dialogs.FileKitDialogSettings
|
||||
import io.github.vinceglb.filekit.dialogs.FileKitMode
|
||||
import io.github.vinceglb.filekit.dialogs.openDirectoryPicker
|
||||
import io.github.vinceglb.filekit.dialogs.openFilePicker
|
||||
import java.awt.EventQueue
|
||||
import java.awt.FileDialog
|
||||
import java.awt.Frame
|
||||
@@ -12,34 +18,59 @@ import java.io.File
|
||||
import javax.imageio.ImageIO
|
||||
import javax.swing.JFileChooser
|
||||
import javax.swing.filechooser.FileSystemView
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Composable
|
||||
actual fun rememberShareFilePicker(
|
||||
onFilesPicked: (List<PickedShareFile>) -> Unit,
|
||||
onError: (String) -> Unit,
|
||||
): ShareFilePicker = remember(onFilesPicked, onError) {
|
||||
object : ShareFilePicker {
|
||||
override fun pickFiles() {
|
||||
openPicker(onError) {
|
||||
val selected = pickShareFiles()
|
||||
if (selected.isNotEmpty()) onFilesPicked(selected)
|
||||
): ShareFilePicker {
|
||||
val scope = rememberCoroutineScope()
|
||||
return remember(onFilesPicked, onError, scope) {
|
||||
object : ShareFilePicker {
|
||||
override fun pickFiles() {
|
||||
if (jvmFilePickerBackend(System.getProperty("os.name")) == JvmFilePickerBackend.XdgPortal) {
|
||||
scope.launch {
|
||||
try {
|
||||
val selected = withContext(Dispatchers.IO) { pickShareFilesWithPortal() }
|
||||
if (selected.isNotEmpty()) onFilesPicked(selected)
|
||||
} catch (error: CancellationException) {
|
||||
throw error
|
||||
} catch (error: Throwable) {
|
||||
onError(error.message ?: error.toString())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
openPicker(onError) {
|
||||
val selected = pickShareFiles()
|
||||
if (selected.isNotEmpty()) onFilesPicked(selected)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun pickFolder() {
|
||||
openPicker(onError) {
|
||||
val selected = pickDirectory(title = "Select folder to share") ?: return@openPicker
|
||||
onFilesPicked(
|
||||
listOf(
|
||||
PickedShareFile(
|
||||
value = selected.absolutePath,
|
||||
displayName = selected.name.ifBlank { selected.absolutePath },
|
||||
sizeBytes = null,
|
||||
thumbnailBytes = selected.systemIconPng(),
|
||||
isDirectory = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
override fun pickFolder() {
|
||||
if (jvmFilePickerBackend(System.getProperty("os.name")) == JvmFilePickerBackend.XdgPortal) {
|
||||
scope.launch {
|
||||
try {
|
||||
val selected = withContext(Dispatchers.IO) {
|
||||
pickDirectoryWithPortal("Select folder to share")?.toPickedShareFile(isDirectory = true)
|
||||
} ?: return@launch
|
||||
onFilesPicked(listOf(selected))
|
||||
} catch (error: CancellationException) {
|
||||
throw error
|
||||
} catch (error: Throwable) {
|
||||
onError(error.message ?: error.toString())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
openPicker(onError) {
|
||||
val selected = pickDirectory(title = "Select folder to share") ?: return@openPicker
|
||||
onFilesPicked(listOf(selected.toPickedShareFile(isDirectory = true)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,23 +80,43 @@ actual fun rememberShareFilePicker(
|
||||
actual fun rememberReceiveFolderPicker(
|
||||
onFolderPicked: (ReceiveFolder) -> Unit,
|
||||
onError: (String) -> Unit,
|
||||
): ReceiveFolderPicker = remember(onFolderPicked, onError) {
|
||||
object : ReceiveFolderPicker {
|
||||
override fun pickFolder() {
|
||||
openPicker(onError) {
|
||||
val selected = pickDirectory(title = "Select receive folder") ?: return@openPicker
|
||||
onFolderPicked(
|
||||
ReceiveFolder(
|
||||
kind = ReceiveFolderKind.FileSystemPath,
|
||||
value = selected.absolutePath,
|
||||
displayName = selected.name.ifBlank { selected.absolutePath },
|
||||
),
|
||||
)
|
||||
): ReceiveFolderPicker {
|
||||
val scope = rememberCoroutineScope()
|
||||
return remember(onFolderPicked, onError, scope) {
|
||||
object : ReceiveFolderPicker {
|
||||
override fun pickFolder() {
|
||||
if (jvmFilePickerBackend(System.getProperty("os.name")) == JvmFilePickerBackend.XdgPortal) {
|
||||
scope.launch {
|
||||
try {
|
||||
val selected = withContext(Dispatchers.IO) {
|
||||
pickDirectoryWithPortal("Select receive folder")
|
||||
} ?: return@launch
|
||||
onFolderPicked(selected.toReceiveFolder())
|
||||
} catch (error: CancellationException) {
|
||||
throw error
|
||||
} catch (error: Throwable) {
|
||||
onError(error.message ?: error.toString())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
openPicker(onError) {
|
||||
val selected = pickDirectory(title = "Select receive folder") ?: return@openPicker
|
||||
onFolderPicked(selected.toReceiveFolder())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal enum class JvmFilePickerBackend {
|
||||
XdgPortal,
|
||||
AwtSwing,
|
||||
}
|
||||
|
||||
internal fun jvmFilePickerBackend(osName: String?): JvmFilePickerBackend =
|
||||
if (osName.orEmpty().startsWith("Linux", ignoreCase = true)) JvmFilePickerBackend.XdgPortal else JvmFilePickerBackend.AwtSwing
|
||||
|
||||
private fun openPicker(
|
||||
onError: (String) -> Unit,
|
||||
block: () -> Unit,
|
||||
@@ -99,20 +150,39 @@ private fun pickShareFiles(): List<PickedShareFile> {
|
||||
val names = dialog.files?.map { it.name }.orEmpty().ifEmpty {
|
||||
dialog.file?.let { listOf(it) }.orEmpty()
|
||||
}
|
||||
names.map { name ->
|
||||
val selected = File(directory, name)
|
||||
PickedShareFile(
|
||||
selected.absolutePath,
|
||||
selected.name,
|
||||
selected.length().takeIf { it >= 0L }?.toULong(),
|
||||
selected.systemIconPng(),
|
||||
)
|
||||
}
|
||||
names.map { name -> File(directory, name).toPickedShareFile(isDirectory = false) }
|
||||
} finally {
|
||||
dialog.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun pickShareFilesWithPortal(): List<PickedShareFile> =
|
||||
FileKit.openFilePicker(
|
||||
mode = FileKitMode.Multiple(),
|
||||
dialogSettings = FileKitDialogSettings(title = "Select files to share", parentWindow = activeFrame()),
|
||||
).orEmpty().map { it.file.toPickedShareFile(isDirectory = false) }
|
||||
|
||||
private suspend fun pickDirectoryWithPortal(title: String): File? =
|
||||
FileKit.openDirectoryPicker(
|
||||
dialogSettings = FileKitDialogSettings(title = title, parentWindow = activeFrame()),
|
||||
)?.file
|
||||
|
||||
private fun File.toPickedShareFile(isDirectory: Boolean): PickedShareFile =
|
||||
PickedShareFile(
|
||||
value = absolutePath,
|
||||
displayName = name.ifBlank { absolutePath },
|
||||
sizeBytes = if (isDirectory) null else length().takeIf { it >= 0L }?.toULong(),
|
||||
thumbnailBytes = systemIconPng(),
|
||||
isDirectory = isDirectory,
|
||||
)
|
||||
|
||||
private fun File.toReceiveFolder(): ReceiveFolder =
|
||||
ReceiveFolder(
|
||||
kind = ReceiveFolderKind.FileSystemPath,
|
||||
value = absolutePath,
|
||||
displayName = name.ifBlank { absolutePath },
|
||||
)
|
||||
|
||||
private fun File.systemIconPng(): ByteArray? = runCatching {
|
||||
val icon = FileSystemView.getFileSystemView().getSystemIcon(this, 128, 128)
|
||||
val image = BufferedImage(icon.iconWidth, icon.iconHeight, BufferedImage.TYPE_INT_ARGB)
|
||||
|
||||
Reference in New Issue
Block a user