Skip to content

Commit

Permalink
Merge pull request #94 from MohamedRejeb/0.5.x
Browse files Browse the repository at this point in the history
Add file picker extensions support
  • Loading branch information
MohamedRejeb authored Jun 23, 2024
2 parents 80ba46c + 9ee522e commit c493fd4
Show file tree
Hide file tree
Showing 30 changed files with 3,537 additions and 147 deletions.
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,17 +364,22 @@ Button(
* `FilePickerFileType.Document` - Allows you to pick documents only
* `FilePickerFileType.Text` - Allows you to pick text files only
* `FilePickerFileType.Pdf` - Allows you to pick PDF files only
* `FilePickerFileType.Presentation` - Allows you to pick presentation files only
* `FilePickerFileType.Spreadsheet` - Allows you to pick spreadsheet files only
* `FilePickerFileType.Word` - Allows you to pick compressed word only
* `FilePickerFileType.All` - Allows you to pick all types of files
* `FilePickerFileType.Folder` - Allows you to pick folders

You can also specify the file types you want to pick by using the `FilePickerFileType.Custom` type:
You can filter files by custom mime types using `FilePickerFileType.Custom`.

```kotlin
val type = FilePickerFileType.Custom(
"text/plain"
listOf("text/plain")
)
```

You can also filter files by custom extensions using `FilePickerFileType.Extension`.

```kotlin
val type = FilePickerFileType.Extension(
listOf("txt")
)
```

Expand Down
4 changes: 4 additions & 0 deletions calf-file-picker/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,8 @@ kotlin {
sourceSets.androidMain.dependencies {
implementation(libs.activity.compose)
}

sourceSets.desktopMain.dependencies {
implementation(libs.jna)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.mohamedrejeb.calf.picker

import android.webkit.MimeTypeMap
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
Expand Down Expand Up @@ -60,7 +61,7 @@ private fun pickSingleVisualMedia(
type: FilePickerFileType,
onResult: (List<KmpFile>) -> Unit,
): FilePickerLauncher {
val singlePhotoPickerLauncher =
val mediaPickerLauncher =
rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickVisualMedia(),
onResult = { uri ->
Expand All @@ -73,7 +74,7 @@ private fun pickSingleVisualMedia(
type = type,
selectionMode = FilePickerSelectionMode.Single,
onLaunch = {
singlePhotoPickerLauncher.launch(
mediaPickerLauncher.launch(
type.toPickVisualMediaRequest(),
)
},
Expand All @@ -86,7 +87,7 @@ fun pickMultipleVisualMedia(
type: FilePickerFileType,
onResult: (List<KmpFile>) -> Unit,
): FilePickerLauncher {
val singlePhotoPickerLauncher =
val mediaPickerLauncher =
rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickMultipleVisualMedia(),
onResult = { uriList ->
Expand All @@ -103,7 +104,7 @@ fun pickMultipleVisualMedia(
type = type,
selectionMode = FilePickerSelectionMode.Multiple,
onLaunch = {
singlePhotoPickerLauncher.launch(
mediaPickerLauncher.launch(
type.toPickVisualMediaRequest(),
)
},
Expand All @@ -116,7 +117,7 @@ private fun pickSingleFile(
type: FilePickerFileType,
onResult: (List<KmpFile>) -> Unit,
): FilePickerLauncher {
val singlePhotoPickerLauncher =
val filePickerLauncher =
rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument(),
onResult = { uri ->
Expand All @@ -129,8 +130,13 @@ private fun pickSingleFile(
type = type,
selectionMode = FilePickerSelectionMode.Single,
onLaunch = {
singlePhotoPickerLauncher.launch(
type.value.toList().toTypedArray(),
val mimeTypeMap = MimeTypeMap.getSingleton()

filePickerLauncher.launch(
if (type is FilePickerFileType.Extension)
type.value.mapNotNull { mimeTypeMap.getMimeTypeFromExtension(it) }.toTypedArray()
else
type.value.toList().toTypedArray()
)
},
)
Expand All @@ -142,7 +148,7 @@ private fun pickMultipleFiles(
type: FilePickerFileType,
onResult: (List<KmpFile>) -> Unit,
): FilePickerLauncher {
val singlePhotoPickerLauncher =
val filePickerLauncher =
rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenMultipleDocuments(),
onResult = { uriList ->
Expand All @@ -159,8 +165,13 @@ private fun pickMultipleFiles(
type = type,
selectionMode = FilePickerSelectionMode.Multiple,
onLaunch = {
singlePhotoPickerLauncher.launch(
type.value.toList().toTypedArray(),
val mimeTypeMap = MimeTypeMap.getSingleton()

filePickerLauncher.launch(
if (type is FilePickerFileType.Extension)
type.value.mapNotNull { mimeTypeMap.getMimeTypeFromExtension(it) }.toTypedArray()
else
type.value.toList().toTypedArray()
)
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ sealed class FilePickerFileType(vararg val value: String) {
)
data object Pdf: FilePickerFileType(PdfContentType)
data object Text: FilePickerFileType(TextContentType)
data object Folder: FilePickerFileType("folder")
data object Folder: FilePickerFileType(FolderContentType)
data object All: FilePickerFileType(AllContentType)

/**
Expand All @@ -43,6 +43,13 @@ sealed class FilePickerFileType(vararg val value: String) {
*/
data class Custom(val contentType: List<String>): FilePickerFileType(*contentType.toTypedArray())

/**
* Custom file extensions
*
* @param extensions List of extensions
*/
data class Extension(val extensions: List<String>): FilePickerFileType(*extensions.toTypedArray())

companion object {
const val FolderContentType = "folder"
const val AudioContentType = "audio/*"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,90 +1,54 @@
package com.mohamedrejeb.calf.picker

import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.window.AwtWindow
import androidx.compose.runtime.rememberCoroutineScope
import com.mohamedrejeb.calf.io.KmpFile
import java.awt.FileDialog
import java.awt.Frame
import com.mohamedrejeb.calf.picker.platform.PlatformFilePicker
import jodd.net.MimeTypes
import kotlinx.coroutines.launch
import java.io.File
import java.net.URLConnection

@Composable
actual fun rememberFilePickerLauncher(
type: FilePickerFileType,
selectionMode: FilePickerSelectionMode,
onResult: (List<KmpFile>) -> Unit,
): FilePickerLauncher {
var fileDialogVisible by rememberSaveable { mutableStateOf(false) }

if (fileDialogVisible) {
AwtWindow(
create = {
val frame: Frame? = null
val fileDialog =
object : FileDialog(
frame,
"Select ${if (type == FilePickerFileType.Folder) "Folder" else "File"}",
if (type == FilePickerFileType.Folder) SAVE else LOAD,
) {
override fun setVisible(value: Boolean) {
super.setVisible(value)
if (value) {
onResult(files.orEmpty().map { KmpFile(it) })
fileDialogVisible = false
}
}
}

fileDialog.isMultipleMode = selectionMode == FilePickerSelectionMode.Multiple

val mimeType =
when (type) {
FilePickerFileType.Folder -> listOf("folder")
FilePickerFileType.All -> emptyList()
else ->
type.value
.map {
it
.removeSuffix("/*")
.removeSuffix("/")
.removeSuffix("*")
}
.filter {
it.isNotEmpty()
}
}
fileDialog.setFilenameFilter { file, name ->
if (mimeType.isEmpty()) {
true
} else if (mimeType.first().contains("folder", true)) {
file.isDirectory
} else {
val contentType = URLConnection.guessContentTypeFromName(name) ?: ""
mimeType.any {
contentType.startsWith(it, true)
}
}
}

fileDialog
},
dispose = {
it.dispose()
},
)
}
val scope = rememberCoroutineScope()

return remember {
FilePickerLauncher(
type = type,
selectionMode = selectionMode,
onLaunch = {
fileDialogVisible = true
scope.launch {
if (type == FilePickerFileType.Folder)
PlatformFilePicker.current.launchDirectoryPicker(
initialDirectory = null,
title = "Select a folder",
parentWindow = null,
onResult = { file ->
onResult(
if (file == null)
emptyList()
else
listOf(KmpFile(file))
)
}
)
else
PlatformFilePicker.current.launchFilePicker(
initialDirectory = null,
type = type,
selectionMode = selectionMode,
title = "Select a file",
parentWindow = null,
onResult = { files ->
onResult(files.map { KmpFile(it) })
}
)
}
},
)
}
Expand All @@ -102,3 +66,9 @@ actual class FilePickerLauncher actual constructor(

val File.extension: String
get() = name.substringAfterLast(".")

fun main() {
MimeTypes.findExtensionsByMimeTypes("video/*", true).also {
println(it)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.mohamedrejeb.calf.picker.platform

import com.mohamedrejeb.calf.picker.FilePickerFileType
import com.mohamedrejeb.calf.picker.FilePickerSelectionMode
import com.mohamedrejeb.calf.picker.platform.awt.AwtFilePicker
import com.mohamedrejeb.calf.picker.platform.mac.MacOSFilePicker
import com.mohamedrejeb.calf.picker.platform.util.Platform
import com.mohamedrejeb.calf.picker.platform.util.PlatformUtil
import com.mohamedrejeb.calf.picker.platform.windows.WindowsFilePicker
import java.awt.Window
import java.io.File

interface PlatformFilePicker {

suspend fun launchFilePicker(
initialDirectory: String?,
type: FilePickerFileType,
selectionMode: FilePickerSelectionMode,
title: String?,
parentWindow: Window?,
onResult: (List<File>) -> Unit,
)

suspend fun launchDirectoryPicker(
initialDirectory: String?,
title: String?,
parentWindow: Window?,
onResult: (File?) -> Unit,
)

companion object {
val current: PlatformFilePicker by lazy { createPlatformFilePicker() }

private fun createPlatformFilePicker(): PlatformFilePicker {
return when (PlatformUtil.current) {
Platform.Windows ->
WindowsFilePicker()

Platform.MacOS ->
MacOSFilePicker()

Platform.Linux ->
AwtFilePicker()
}
}
}

}
Loading

0 comments on commit c493fd4

Please sign in to comment.