From 31973498cedd7422c9ab32f682605ed96b25469f Mon Sep 17 00:00:00 2001 From: santi Date: Wed, 10 Jul 2024 21:22:11 +0200 Subject: [PATCH 1/8] some work on max items for file selection. committing to create pr and add notes there --- .../vinceglb/filekit/core/FileKit.android.kt | 84 +-- .../vinceglb/filekit/core/PickerMode.kt | 2 +- .../vinceglb/filekit/core/FileKit.js.kt | 2 + .../vinceglb/filekit/core/FileKit.jvm.kt | 4 +- .../core/platform/awt/AwtFilePicker.kt | 2 + .../core/platform/mac/MacOSFilePicker.kt | 297 +++++----- .../platform/windows/api/JnaFileChooser.kt | 530 +++++++++--------- .../vinceglb/filekit/core/FileKit.wasmJs.kt | 2 + .../composeApp/src/commonMain/kotlin/App.kt | 4 +- 9 files changed, 474 insertions(+), 453 deletions(-) diff --git a/filekit-core/src/androidMain/kotlin/io/github/vinceglb/filekit/core/FileKit.android.kt b/filekit-core/src/androidMain/kotlin/io/github/vinceglb/filekit/core/FileKit.android.kt index 8a43a70..8fc083a 100644 --- a/filekit-core/src/androidMain/kotlin/io/github/vinceglb/filekit/core/FileKit.android.kt +++ b/filekit-core/src/androidMain/kotlin/io/github/vinceglb/filekit/core/FileKit.android.kt @@ -4,6 +4,7 @@ import android.content.Context import android.net.Uri import android.webkit.MimeTypeMap import androidx.activity.ComponentActivity +import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultRegistry import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts @@ -49,61 +50,68 @@ public actual object FileKit { PickerType.Image, PickerType.Video, PickerType.ImageAndVideo -> { - when (mode) { - is PickerMode.Single -> { - val contract = PickVisualMedia() - val launcher = registry.register(key, contract) { uri -> - val result = uri?.let { listOf(PlatformFile(it, context)) } - continuation.resume(result) - } + val request = when (type) { + PickerType.Image -> PickVisualMediaRequest(ImageOnly) + PickerType.Video -> PickVisualMediaRequest(VideoOnly) + PickerType.ImageAndVideo -> PickVisualMediaRequest(ImageAndVideo) + else -> throw IllegalArgumentException("Unsupported type: $type") + } - val request = when (type) { - PickerType.Image -> PickVisualMediaRequest(ImageOnly) - PickerType.Video -> PickVisualMediaRequest(VideoOnly) - PickerType.ImageAndVideo -> PickVisualMediaRequest(ImageAndVideo) - else -> throw IllegalArgumentException("Unsupported type: $type") - } + fun singleMediaLauncher(): ActivityResultLauncher { + val contract = PickVisualMedia() + return registry.register(key, contract) { uri -> + val result = uri?.let { listOf(PlatformFile(it, context)) } + continuation.resume(result) + } + } - launcher.launch(request) + val launcher = when (mode) { + is PickerMode.Single -> { + singleMediaLauncher() } is PickerMode.Multiple -> { - val contract = ActivityResultContracts.PickMultipleVisualMedia() - val launcher = registry.register(key, contract) { uri -> - val result = uri.map { PlatformFile(it, context) } - continuation.resume(result) - } - - val request = when (type) { - PickerType.Image -> PickVisualMediaRequest(ImageOnly) - PickerType.Video -> PickVisualMediaRequest(VideoOnly) - PickerType.ImageAndVideo -> PickVisualMediaRequest(ImageAndVideo) - else -> throw IllegalArgumentException("Unsupported type: $type") + if (mode.maxItems == 1) singleMediaLauncher() + else { + val contract = + ActivityResultContracts.PickMultipleVisualMedia(mode.maxItems) + registry.register(key, contract) { uri -> + val result = uri.map { PlatformFile(it, context) } + continuation.resume(result) + } } - - launcher.launch(request) } } + launcher.launch(request) } is PickerType.File -> { + fun openSingleDocument() { + val contract = ActivityResultContracts.OpenDocument() + val launcher = registry.register(key, contract) { uri -> + val result = uri?.let { listOf(PlatformFile(it, context)) } + continuation.resume(result) + } + launcher.launch(getMimeTypes(type.extensions)) + } when (mode) { is PickerMode.Single -> { - val contract = ActivityResultContracts.OpenDocument() - val launcher = registry.register(key, contract) { uri -> - val result = uri?.let { listOf(PlatformFile(it, context)) } - continuation.resume(result) - } - launcher.launch(getMimeTypes(type.extensions)) + openSingleDocument() } is PickerMode.Multiple -> { - val contract = ActivityResultContracts.OpenMultipleDocuments() - val launcher = registry.register(key, contract) { uris -> - val result = uris.map { PlatformFile(it, context) } - continuation.resume(result) + if (mode.maxItems == 1) { + openSingleDocument() + } else { + // TODO there might be a way to limit the amount of documents, but + // I haven't found it yet. + val contract = ActivityResultContracts.OpenMultipleDocuments() + val launcher = registry.register(key, contract) { uris -> + val result = uris.map { PlatformFile(it, context) } + continuation.resume(result) + } + launcher.launch(getMimeTypes(type.extensions)) } - launcher.launch(getMimeTypes(type.extensions)) } } } diff --git a/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/core/PickerMode.kt b/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/core/PickerMode.kt index ba8d1cc..f641b0b 100644 --- a/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/core/PickerMode.kt +++ b/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/core/PickerMode.kt @@ -9,7 +9,7 @@ public sealed class PickerMode { } } - public data object Multiple : PickerMode() { + public data class Multiple(val maxItems: Int = Int.MAX_VALUE) : PickerMode() { override fun parseResult(value: PlatformFiles?): PlatformFiles? { return value?.takeIf { it.isNotEmpty() } } diff --git a/filekit-core/src/jsMain/kotlin/io/github/vinceglb/filekit/core/FileKit.js.kt b/filekit-core/src/jsMain/kotlin/io/github/vinceglb/filekit/core/FileKit.js.kt index fbdb548..bdbc58f 100644 --- a/filekit-core/src/jsMain/kotlin/io/github/vinceglb/filekit/core/FileKit.js.kt +++ b/filekit-core/src/jsMain/kotlin/io/github/vinceglb/filekit/core/FileKit.js.kt @@ -40,6 +40,8 @@ public actual object FileKit { // Set the multiple attribute multiple = mode is PickerMode.Multiple + + // max is not supported for file inputs } // Setup the change listener diff --git a/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/FileKit.jvm.kt b/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/FileKit.jvm.kt index 87dbe0b..1d46b58 100644 --- a/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/FileKit.jvm.kt +++ b/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/FileKit.jvm.kt @@ -25,14 +25,14 @@ public actual object FileKit { // Open native file picker val result = when (mode) { - PickerMode.Single -> PlatformFilePicker.current.pickFile( + is PickerMode.Single -> PlatformFilePicker.current.pickFile( title = title, initialDirectory = initialDirectory, fileExtensions = extensions, parentWindow = platformSettings?.parentWindow, )?.let { listOf(PlatformFile(it)) } - PickerMode.Multiple -> PlatformFilePicker.current.pickFiles( + is PickerMode.Multiple -> PlatformFilePicker.current.pickFiles( title = title, initialDirectory = initialDirectory, fileExtensions = extensions, diff --git a/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/platform/awt/AwtFilePicker.kt b/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/platform/awt/AwtFilePicker.kt index 5f92362..a00ef93 100644 --- a/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/platform/awt/AwtFilePicker.kt +++ b/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/platform/awt/AwtFilePicker.kt @@ -79,6 +79,8 @@ internal class AwtFilePicker : PlatformFilePicker { // Set multiple mode dialog.isMultipleMode = isMultipleMode + // MaxItems is not supported by FileDialog + // Set mime types dialog.filenameFilter = FilenameFilter { _, name -> fileExtensions?.any { name.endsWith(it) } ?: true diff --git a/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/platform/mac/MacOSFilePicker.kt b/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/platform/mac/MacOSFilePicker.kt index 3819251..685fc66 100644 --- a/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/platform/mac/MacOSFilePicker.kt +++ b/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/platform/mac/MacOSFilePicker.kt @@ -7,150 +7,155 @@ import java.awt.Window import java.io.File internal class MacOSFilePicker : PlatformFilePicker { - override suspend fun pickFile( - initialDirectory: String?, - fileExtensions: List?, - title: String?, - parentWindow: Window?, - ): File? { - return callNativeMacOSPicker( - mode = MacOSFilePickerMode.SingleFile, - initialDirectory = initialDirectory, - fileExtensions = fileExtensions, - title = title - ) - } - - override suspend fun pickFiles( - initialDirectory: String?, - fileExtensions: List?, - title: String?, - parentWindow: Window?, - ): List? { - return callNativeMacOSPicker( - mode = MacOSFilePickerMode.MultipleFiles, - initialDirectory = initialDirectory, - fileExtensions = fileExtensions, - title = title - ) - } - - override fun pickDirectory( - initialDirectory: String?, - title: String?, - parentWindow: Window?, - ): File? { - return callNativeMacOSPicker( - mode = MacOSFilePickerMode.Directories, - initialDirectory = initialDirectory, - fileExtensions = null, - title = title - ) - } - - private fun callNativeMacOSPicker( - mode: MacOSFilePickerMode, - initialDirectory: String?, - fileExtensions: List?, - title: String?, - ): T? { - val pool = Foundation.NSAutoreleasePool() - return try { - var response: T? = null - - Foundation.executeOnMainThread( - withAutoreleasePool = false, - waitUntilDone = true, - ) { - // Create the file picker - val openPanel = Foundation.invoke("NSOpenPanel", "new") - - // Setup single, multiple selection or directory mode - mode.setupPickerMode(openPanel) - - // Set the title - title?.let { - Foundation.invoke(openPanel, "setMessage:", Foundation.nsString(it)) - } - - // Set initial directory - initialDirectory?.let { - Foundation.invoke(openPanel, "setDirectoryURL:", Foundation.nsURL(it)) - } - - // Set file extensions - fileExtensions?.let { extensions -> - val items = extensions.map { Foundation.nsString(it) } - val nsData = Foundation.invokeVarArg("NSArray", "arrayWithObjects:", *items.toTypedArray()) - Foundation.invoke(openPanel, "setAllowedFileTypes:", nsData) - } - - // Open the file picker - val result = Foundation.invoke(openPanel, "runModal") - - // Get the path(s) from the file picker if the user validated the selection - if (result.toInt() == 1) { - response = mode.getResult(openPanel) - } - } - - response - } finally { - pool.drain() - } - } - - private companion object { - fun singlePath(openPanel: ID): File? { - val url = Foundation.invoke(openPanel, "URL") - val nsPath = Foundation.invoke(url, "path") - val path = Foundation.toStringViaUTF8(nsPath) - return path?.let { File(it) } - } - - fun multiplePaths(openPanel: ID): List? { - val urls = Foundation.invoke(openPanel, "URLs") - val urlCount = Foundation.invoke(urls, "count").toInt() - - return (0 until urlCount).mapNotNull { index -> - val url = Foundation.invoke(urls, "objectAtIndex:", index) - val nsPath = Foundation.invoke(url, "path") - val path = Foundation.toStringViaUTF8(nsPath) - path?.let { File(it) } - }.ifEmpty { null } - } - } - - private sealed class MacOSFilePickerMode { - abstract fun setupPickerMode(openPanel: ID) - abstract fun getResult(openPanel: ID): T? - - data object SingleFile : MacOSFilePickerMode() { - override fun setupPickerMode(openPanel: ID) { - Foundation.invoke(openPanel, "setCanChooseFiles:", true) - Foundation.invoke(openPanel, "setCanChooseDirectories:", false) - } - - override fun getResult(openPanel: ID): File? = singlePath(openPanel) - } - - data object MultipleFiles : MacOSFilePickerMode>() { - override fun setupPickerMode(openPanel: ID) { - Foundation.invoke(openPanel, "setCanChooseFiles:", true) - Foundation.invoke(openPanel, "setCanChooseDirectories:", false) - Foundation.invoke(openPanel, "setAllowsMultipleSelection:", true) - } - - override fun getResult(openPanel: ID): List? = multiplePaths(openPanel) - } - - data object Directories : MacOSFilePickerMode() { - override fun setupPickerMode(openPanel: ID) { - Foundation.invoke(openPanel, "setCanChooseFiles:", false) - Foundation.invoke(openPanel, "setCanChooseDirectories:", true) - } - - override fun getResult(openPanel: ID): File? = singlePath(openPanel) - } - } + override suspend fun pickFile( + initialDirectory: String?, + fileExtensions: List?, + title: String?, + parentWindow: Window?, + ): File? { + return callNativeMacOSPicker( + mode = MacOSFilePickerMode.SingleFile, + initialDirectory = initialDirectory, + fileExtensions = fileExtensions, + title = title + ) + } + + override suspend fun pickFiles( + initialDirectory: String?, + fileExtensions: List?, + title: String?, + parentWindow: Window?, + ): List? { + return callNativeMacOSPicker( + mode = MacOSFilePickerMode.MultipleFiles, + initialDirectory = initialDirectory, + fileExtensions = fileExtensions, + title = title + ) + } + + override fun pickDirectory( + initialDirectory: String?, + title: String?, + parentWindow: Window?, + ): File? { + return callNativeMacOSPicker( + mode = MacOSFilePickerMode.Directories, + initialDirectory = initialDirectory, + fileExtensions = null, + title = title + ) + } + + private fun callNativeMacOSPicker( + mode: MacOSFilePickerMode, + initialDirectory: String?, + fileExtensions: List?, + title: String?, + ): T? { + val pool = Foundation.NSAutoreleasePool() + return try { + var response: T? = null + + Foundation.executeOnMainThread( + withAutoreleasePool = false, + waitUntilDone = true, + ) { + // Create the file picker + val openPanel = Foundation.invoke("NSOpenPanel", "new") + + // Setup single, multiple selection or directory mode + mode.setupPickerMode(openPanel) + + // Set the title + title?.let { + Foundation.invoke(openPanel, "setMessage:", Foundation.nsString(it)) + } + + // Set initial directory + initialDirectory?.let { + Foundation.invoke(openPanel, "setDirectoryURL:", Foundation.nsURL(it)) + } + + // Set file extensions + fileExtensions?.let { extensions -> + val items = extensions.map { Foundation.nsString(it) } + val nsData = Foundation.invokeVarArg( + "NSArray", + "arrayWithObjects:", + *items.toTypedArray() + ) + Foundation.invoke(openPanel, "setAllowedFileTypes:", nsData) + } + + // Open the file picker + val result = Foundation.invoke(openPanel, "runModal") + + // Get the path(s) from the file picker if the user validated the selection + if (result.toInt() == 1) { + response = mode.getResult(openPanel) + } + } + + response + } finally { + pool.drain() + } + } + + private companion object { + fun singlePath(openPanel: ID): File? { + val url = Foundation.invoke(openPanel, "URL") + val nsPath = Foundation.invoke(url, "path") + val path = Foundation.toStringViaUTF8(nsPath) + return path?.let { File(it) } + } + + fun multiplePaths(openPanel: ID): List? { + val urls = Foundation.invoke(openPanel, "URLs") + val urlCount = Foundation.invoke(urls, "count").toInt() + + return (0 until urlCount).mapNotNull { index -> + val url = Foundation.invoke(urls, "objectAtIndex:", index) + val nsPath = Foundation.invoke(url, "path") + val path = Foundation.toStringViaUTF8(nsPath) + path?.let { File(it) } + }.ifEmpty { null } + } + } + + private sealed class MacOSFilePickerMode { + abstract fun setupPickerMode(openPanel: ID) + abstract fun getResult(openPanel: ID): T? + + data object SingleFile : MacOSFilePickerMode() { + override fun setupPickerMode(openPanel: ID) { + Foundation.invoke(openPanel, "setCanChooseFiles:", true) + Foundation.invoke(openPanel, "setCanChooseDirectories:", false) + } + + override fun getResult(openPanel: ID): File? = singlePath(openPanel) + } + + data object MultipleFiles : MacOSFilePickerMode>() { + override fun setupPickerMode(openPanel: ID) { + Foundation.invoke(openPanel, "setCanChooseFiles:", true) + Foundation.invoke(openPanel, "setCanChooseDirectories:", false) + Foundation.invoke(openPanel, "setAllowsMultipleSelection:", true) + // MaxItems is not supported by FileDialog + } + + override fun getResult(openPanel: ID): List? = multiplePaths(openPanel) + } + + data object Directories : MacOSFilePickerMode() { + override fun setupPickerMode(openPanel: ID) { + Foundation.invoke(openPanel, "setCanChooseFiles:", false) + Foundation.invoke(openPanel, "setCanChooseDirectories:", true) + } + + override fun getResult(openPanel: ID): File? = singlePath(openPanel) + } + } } diff --git a/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/platform/windows/api/JnaFileChooser.kt b/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/platform/windows/api/JnaFileChooser.kt index 9feab92..da94f7c 100644 --- a/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/platform/windows/api/JnaFileChooser.kt +++ b/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/platform/windows/api/JnaFileChooser.kt @@ -29,268 +29,270 @@ import javax.swing.filechooser.FileNameExtensionFilter * @see JFileChooser, WindowsFileChooser, WindowsFileBrowser */ internal class JnaFileChooser() { - private enum class Action { - Open, Save - } - - /** - * the availabe selection modes of the dialog - */ - enum class Mode(val jFileChooserValue: Int) { - Files(JFileChooser.FILES_ONLY), - Directories(JFileChooser.DIRECTORIES_ONLY), - FilesAndDirectories(JFileChooser.FILES_AND_DIRECTORIES) - } - - var selectedFiles: Array - protected set - var currentDirectory: File? = null - protected set - protected var filters: ArrayList> = ArrayList() - - /** - * sets whether to enable multiselection - * - * @param enabled true to enable multiselection, false to disable it - */ - var isMultiSelectionEnabled: Boolean = false - - /** - * sets the selection mode - * - * @param mode the selection mode - */ - var mode: Mode = Mode.Files - - private var defaultFile: String = "" - private var dialogTitle: String = "" - private var openButtonText: String = "" - private var saveButtonText: String = "" - - /** - * creates a new file chooser with multiselection disabled and mode set - * to allow file selection only. - */ - init { - selectedFiles = arrayOf(null) - } - - /** - * creates a new file chooser with the specified initial directory - * - * @param currentDirectory the initial directory - */ - constructor(currentDirectory: File?) : this() { - if (currentDirectory != null) { - this.currentDirectory = - if (currentDirectory.isDirectory) currentDirectory else currentDirectory.parentFile - } - } - - /** - * creates a new file chooser with the specified initial directory - * - * @param currentDirectory the initial directory - */ - constructor(currentDirectoryPath: String?) : this( - if (currentDirectoryPath != null) File( - currentDirectoryPath - ) else null - ) - - /** - * shows a dialog for opening files - * - * @param parent the parent window - * - * @return true if the user clicked OK - */ - fun showOpenDialog(parent: Window?): Boolean { - return showDialog(parent, Action.Open) - } - - /** - * shows a dialog for saving files - * - * @param parent the parent window - * - * @return true if the user clicked OK - */ - fun showSaveDialog(parent: Window): Boolean { - return showDialog(parent, Action.Save) - } - - private fun showDialog(parent: Window?, action: Action): Boolean { - // native windows filechooser doesn't support mixed selection mode - if (Platform.isWindows() && mode != Mode.FilesAndDirectories) { - // windows filechooser can only multiselect files - if (isMultiSelectionEnabled && mode == Mode.Files) { - // TODO Here we would use the native windows dialog - // to choose multiple files. However I haven't been able - // to get it to work properly yet because it requires - // tricky callback magic and somehow this didn't work for me - // quite as documented (probably because I messed something up). - // Because I don't need this feature right now I've put it on - // hold to get on with stuff. - // Example code: http://support.microsoft.com/kb/131462/en-us - // GetOpenFileName: http://msdn.microsoft.com/en-us/library/ms646927.aspx - // OFNHookProc: http://msdn.microsoft.com/en-us/library/ms646931.aspx - // CDN_SELCHANGE: http://msdn.microsoft.com/en-us/library/ms646865.aspx - // SendMessage: http://msdn.microsoft.com/en-us/library/ms644950.aspx - } else if (!isMultiSelectionEnabled) { - if (mode == Mode.Files) { - return showWindowsFileChooser(parent, action) - } else if (mode == Mode.Directories) { - return showWindowsFolderBrowser(parent) - } - } - } - - // fallback to Swing - return showSwingFileChooser(parent, action) - } - - private fun showSwingFileChooser(parent: Window?, action: Action): Boolean { - val fc = JFileChooser(currentDirectory) - fc.isMultiSelectionEnabled = isMultiSelectionEnabled - fc.fileSelectionMode = mode.jFileChooserValue - - // set select file - if (!defaultFile.isEmpty() and (action == Action.Save)) { - val fsel = File(defaultFile) - fc.selectedFile = fsel - } - if (!dialogTitle.isEmpty()) { - fc.dialogTitle = dialogTitle - } - if ((action == Action.Open) and !openButtonText.isEmpty()) { - fc.approveButtonText = openButtonText - } else if ((action == Action.Save) and !saveButtonText.isEmpty()) { - fc.approveButtonText = saveButtonText - } - - // build filters - if (filters.size > 0) { - var useAcceptAllFilter = false - for (spec in filters) { - // the "All Files" filter is handled specially by JFileChooser - if (spec[1] == "*") { - useAcceptAllFilter = true - continue - } - fc.addChoosableFileFilter( - FileNameExtensionFilter( - spec[0], *Arrays.copyOfRange(spec, 1, spec.size) - ) - ) - } - fc.isAcceptAllFileFilterUsed = useAcceptAllFilter - } - - var result = -1 - result = if (action == Action.Open) { - fc.showOpenDialog(parent) - } else { - if (saveButtonText.isEmpty()) { - fc.showSaveDialog(parent) - } else { - fc.showDialog(parent, null) - } - } - if (result == JFileChooser.APPROVE_OPTION) { - selectedFiles = - if (isMultiSelectionEnabled) fc.selectedFiles else arrayOf(fc.selectedFile) - currentDirectory = fc.currentDirectory - return true - } - - return false - } - - private fun showWindowsFileChooser(parent: Window?, action: Action): Boolean { - val fc = WindowsFileChooser(currentDirectory) - fc.setFilters(filters) - - if (!defaultFile.isEmpty()) fc.setDefaultFilename(defaultFile) - - if (!dialogTitle.isEmpty()) { - fc.setTitle(dialogTitle) - } - - val result = fc.showDialog(parent, action == Action.Open) - if (result) { - selectedFiles = arrayOf(fc.selectedFile) - currentDirectory = fc.currentDirectory - } - return result - } - - private fun showWindowsFolderBrowser(parent: Window?): Boolean { - val fb = WindowsFolderBrowser() - if (!dialogTitle.isEmpty()) { - fb.setTitle(dialogTitle) - } - val file = fb.showDialog(parent) - if (file != null) { - selectedFiles = arrayOf(file) - currentDirectory = if (file.parentFile != null) file.parentFile else file - return true - } - - return false - } - - /** - * add a filter to the user-selectable list of file filters - * - * @param name name of the filter - * @param filter you must pass at least 1 argument, the arguments are the file - * extensions. - */ - fun addFilter(name: String, vararg filter: String) { - require(filter.isNotEmpty()) - val parts = ArrayList() - parts.add(name) - Collections.addAll(parts, *filter) - filters.add(parts.toTypedArray()) - } - - fun setCurrentDirectory(currentDirectoryPath: String?) { - this.currentDirectory = - (if (currentDirectoryPath != null) File(currentDirectoryPath) else null) - } - - fun setDefaultFileName(dfile: String) { - this.defaultFile = dfile - } - - /** - * set a title name - * - * @param Title of dialog - */ - fun setTitle(title: String) { - this.dialogTitle = title - } - - /** - * set a open button name - * - * @param open button text - */ - fun setOpenButtonText(buttonText: String) { - this.openButtonText = buttonText - } - - /** - * set a saveFile button name - * - * @param saveFile button text - */ - fun setSaveButtonText(buttonText: String) { - this.saveButtonText = buttonText - } - - val selectedFile: File? - get() = selectedFiles[0] + private enum class Action { + Open, Save + } + + /** + * the availabe selection modes of the dialog + */ + enum class Mode(val jFileChooserValue: Int) { + Files(JFileChooser.FILES_ONLY), + Directories(JFileChooser.DIRECTORIES_ONLY), + FilesAndDirectories(JFileChooser.FILES_AND_DIRECTORIES) + } + + var selectedFiles: Array + protected set + var currentDirectory: File? = null + protected set + protected var filters: ArrayList> = ArrayList() + + /** + * sets whether to enable multiselection + * + * @param enabled true to enable multiselection, false to disable it + */ + var isMultiSelectionEnabled: Boolean = false + + /** + * sets the selection mode + * + * @param mode the selection mode + */ + var mode: Mode = Mode.Files + + private var defaultFile: String = "" + private var dialogTitle: String = "" + private var openButtonText: String = "" + private var saveButtonText: String = "" + + /** + * creates a new file chooser with multiselection disabled and mode set + * to allow file selection only. + */ + init { + selectedFiles = arrayOf(null) + } + + /** + * creates a new file chooser with the specified initial directory + * + * @param currentDirectory the initial directory + */ + constructor(currentDirectory: File?) : this() { + if (currentDirectory != null) { + this.currentDirectory = + if (currentDirectory.isDirectory) currentDirectory else currentDirectory.parentFile + } + } + + /** + * creates a new file chooser with the specified initial directory + * + * @param currentDirectory the initial directory + */ + constructor(currentDirectoryPath: String?) : this( + if (currentDirectoryPath != null) File( + currentDirectoryPath + ) else null + ) + + /** + * shows a dialog for opening files + * + * @param parent the parent window + * + * @return true if the user clicked OK + */ + fun showOpenDialog(parent: Window?): Boolean { + return showDialog(parent, Action.Open) + } + + /** + * shows a dialog for saving files + * + * @param parent the parent window + * + * @return true if the user clicked OK + */ + fun showSaveDialog(parent: Window): Boolean { + return showDialog(parent, Action.Save) + } + + private fun showDialog(parent: Window?, action: Action): Boolean { + // native windows filechooser doesn't support mixed selection mode + if (Platform.isWindows() && mode != Mode.FilesAndDirectories) { + // windows filechooser can only multiselect files + if (isMultiSelectionEnabled && mode == Mode.Files) { + // TODO Here we would use the native windows dialog + // to choose multiple files. However I haven't been able + // to get it to work properly yet because it requires + // tricky callback magic and somehow this didn't work for me + // quite as documented (probably because I messed something up). + // Because I don't need this feature right now I've put it on + // hold to get on with stuff. + // Example code: http://support.microsoft.com/kb/131462/en-us + // GetOpenFileName: http://msdn.microsoft.com/en-us/library/ms646927.aspx + // OFNHookProc: http://msdn.microsoft.com/en-us/library/ms646931.aspx + // CDN_SELCHANGE: http://msdn.microsoft.com/en-us/library/ms646865.aspx + // SendMessage: http://msdn.microsoft.com/en-us/library/ms644950.aspx + } else if (!isMultiSelectionEnabled) { + if (mode == Mode.Files) { + return showWindowsFileChooser(parent, action) + } else if (mode == Mode.Directories) { + return showWindowsFolderBrowser(parent) + } + } + } + + // fallback to Swing + return showSwingFileChooser(parent, action) + } + + private fun showSwingFileChooser(parent: Window?, action: Action): Boolean { + val fc = JFileChooser(currentDirectory) + fc.isMultiSelectionEnabled = isMultiSelectionEnabled + fc.fileSelectionMode = mode.jFileChooserValue + + // MaxItems is not supported by FileDialog + + // set select file + if (!defaultFile.isEmpty() and (action == Action.Save)) { + val fsel = File(defaultFile) + fc.selectedFile = fsel + } + if (!dialogTitle.isEmpty()) { + fc.dialogTitle = dialogTitle + } + if ((action == Action.Open) and !openButtonText.isEmpty()) { + fc.approveButtonText = openButtonText + } else if ((action == Action.Save) and !saveButtonText.isEmpty()) { + fc.approveButtonText = saveButtonText + } + + // build filters + if (filters.size > 0) { + var useAcceptAllFilter = false + for (spec in filters) { + // the "All Files" filter is handled specially by JFileChooser + if (spec[1] == "*") { + useAcceptAllFilter = true + continue + } + fc.addChoosableFileFilter( + FileNameExtensionFilter( + spec[0], *Arrays.copyOfRange(spec, 1, spec.size) + ) + ) + } + fc.isAcceptAllFileFilterUsed = useAcceptAllFilter + } + + var result = -1 + result = if (action == Action.Open) { + fc.showOpenDialog(parent) + } else { + if (saveButtonText.isEmpty()) { + fc.showSaveDialog(parent) + } else { + fc.showDialog(parent, null) + } + } + if (result == JFileChooser.APPROVE_OPTION) { + selectedFiles = + if (isMultiSelectionEnabled) fc.selectedFiles else arrayOf(fc.selectedFile) + currentDirectory = fc.currentDirectory + return true + } + + return false + } + + private fun showWindowsFileChooser(parent: Window?, action: Action): Boolean { + val fc = WindowsFileChooser(currentDirectory) + fc.setFilters(filters) + + if (!defaultFile.isEmpty()) fc.setDefaultFilename(defaultFile) + + if (!dialogTitle.isEmpty()) { + fc.setTitle(dialogTitle) + } + + val result = fc.showDialog(parent, action == Action.Open) + if (result) { + selectedFiles = arrayOf(fc.selectedFile) + currentDirectory = fc.currentDirectory + } + return result + } + + private fun showWindowsFolderBrowser(parent: Window?): Boolean { + val fb = WindowsFolderBrowser() + if (!dialogTitle.isEmpty()) { + fb.setTitle(dialogTitle) + } + val file = fb.showDialog(parent) + if (file != null) { + selectedFiles = arrayOf(file) + currentDirectory = if (file.parentFile != null) file.parentFile else file + return true + } + + return false + } + + /** + * add a filter to the user-selectable list of file filters + * + * @param name name of the filter + * @param filter you must pass at least 1 argument, the arguments are the file + * extensions. + */ + fun addFilter(name: String, vararg filter: String) { + require(filter.isNotEmpty()) + val parts = ArrayList() + parts.add(name) + Collections.addAll(parts, *filter) + filters.add(parts.toTypedArray()) + } + + fun setCurrentDirectory(currentDirectoryPath: String?) { + this.currentDirectory = + (if (currentDirectoryPath != null) File(currentDirectoryPath) else null) + } + + fun setDefaultFileName(dfile: String) { + this.defaultFile = dfile + } + + /** + * set a title name + * + * @param Title of dialog + */ + fun setTitle(title: String) { + this.dialogTitle = title + } + + /** + * set a open button name + * + * @param open button text + */ + fun setOpenButtonText(buttonText: String) { + this.openButtonText = buttonText + } + + /** + * set a saveFile button name + * + * @param saveFile button text + */ + fun setSaveButtonText(buttonText: String) { + this.saveButtonText = buttonText + } + + val selectedFile: File? + get() = selectedFiles[0] } diff --git a/filekit-core/src/wasmJsMain/kotlin/io/github/vinceglb/filekit/core/FileKit.wasmJs.kt b/filekit-core/src/wasmJsMain/kotlin/io/github/vinceglb/filekit/core/FileKit.wasmJs.kt index 09711fb..7a2a31b 100644 --- a/filekit-core/src/wasmJsMain/kotlin/io/github/vinceglb/filekit/core/FileKit.wasmJs.kt +++ b/filekit-core/src/wasmJsMain/kotlin/io/github/vinceglb/filekit/core/FileKit.wasmJs.kt @@ -42,6 +42,8 @@ public actual object FileKit { // Set the multiple attribute multiple = mode is PickerMode.Multiple + + // max is not supported for file inputs } // Setup the change listener diff --git a/samples/sample-compose/composeApp/src/commonMain/kotlin/App.kt b/samples/sample-compose/composeApp/src/commonMain/kotlin/App.kt index 538b56a..a28fa5f 100644 --- a/samples/sample-compose/composeApp/src/commonMain/kotlin/App.kt +++ b/samples/sample-compose/composeApp/src/commonMain/kotlin/App.kt @@ -54,7 +54,7 @@ private fun SampleApp() { val multipleFilesPicker = rememberFilePickerLauncher( type = PickerType.Image, - mode = PickerMode.Multiple, + mode = PickerMode.Multiple(), title = "Multiple files picker", initialDirectory = directory?.path, onResult = { file -> file?.let { files += it } } @@ -69,7 +69,7 @@ private fun SampleApp() { val filesPicker = rememberFilePickerLauncher( type = PickerType.File(listOf("png")), - mode = PickerMode.Multiple, + mode = PickerMode.Multiple(), title = "Multiple files picker, only png", initialDirectory = directory?.path, onResult = { file -> file?.let { files += it } } From 7a62851fe39bd01ccb4ea06707531c72bbd86e53 Mon Sep 17 00:00:00 2001 From: santi Date: Wed, 10 Jul 2024 21:32:19 +0200 Subject: [PATCH 2/8] minor refactors and fix comments --- .../filekit/core/platform/mac/MacOSFilePicker.kt | 4 ++-- .../core/platform/windows/api/JnaFileChooser.kt | 13 ++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/platform/mac/MacOSFilePicker.kt b/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/platform/mac/MacOSFilePicker.kt index 685fc66..dc3bc10 100644 --- a/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/platform/mac/MacOSFilePicker.kt +++ b/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/platform/mac/MacOSFilePicker.kt @@ -84,7 +84,7 @@ internal class MacOSFilePicker : PlatformFilePicker { val nsData = Foundation.invokeVarArg( "NSArray", "arrayWithObjects:", - *items.toTypedArray() + *items.toTypedArray(), ) Foundation.invoke(openPanel, "setAllowedFileTypes:", nsData) } @@ -143,7 +143,7 @@ internal class MacOSFilePicker : PlatformFilePicker { Foundation.invoke(openPanel, "setCanChooseFiles:", true) Foundation.invoke(openPanel, "setCanChooseDirectories:", false) Foundation.invoke(openPanel, "setAllowsMultipleSelection:", true) - // MaxItems is not supported by FileDialog + // MaxItems is not supported by MacOSFilePicker } override fun getResult(openPanel: ID): List? = multiplePaths(openPanel) diff --git a/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/platform/windows/api/JnaFileChooser.kt b/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/platform/windows/api/JnaFileChooser.kt index da94f7c..e3f1dc6 100644 --- a/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/platform/windows/api/JnaFileChooser.kt +++ b/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/platform/windows/api/JnaFileChooser.kt @@ -155,19 +155,19 @@ internal class JnaFileChooser() { fc.isMultiSelectionEnabled = isMultiSelectionEnabled fc.fileSelectionMode = mode.jFileChooserValue - // MaxItems is not supported by FileDialog + // MaxItems is not supported by JFileChooser // set select file - if (!defaultFile.isEmpty() and (action == Action.Save)) { + if (defaultFile.isNotEmpty() and (action == Action.Save)) { val fsel = File(defaultFile) fc.selectedFile = fsel } - if (!dialogTitle.isEmpty()) { + if (dialogTitle.isNotEmpty()) { fc.dialogTitle = dialogTitle } - if ((action == Action.Open) and !openButtonText.isEmpty()) { + if ((action == Action.Open) and openButtonText.isNotEmpty()) { fc.approveButtonText = openButtonText - } else if ((action == Action.Save) and !saveButtonText.isEmpty()) { + } else if ((action == Action.Save) and saveButtonText.isNotEmpty()) { fc.approveButtonText = saveButtonText } @@ -189,8 +189,7 @@ internal class JnaFileChooser() { fc.isAcceptAllFileFilterUsed = useAcceptAllFilter } - var result = -1 - result = if (action == Action.Open) { + val result = if (action == Action.Open) { fc.showOpenDialog(parent) } else { if (saveButtonText.isEmpty()) { From 0aa9cb5a01eb8cd477f85b982f9eb7b37c3852fe Mon Sep 17 00:00:00 2001 From: santiwanti Date: Thu, 11 Jul 2024 10:24:58 +0200 Subject: [PATCH 3/8] add support for maxItems on iOS photo picker --- .../vinceglb/filekit/core/FileKit.ios.kt | 11 +++++---- .../xcshareddata/swiftpm/Package.resolved | 24 ------------------- 2 files changed, 7 insertions(+), 28 deletions(-) delete mode 100644 samples/sample-core/appleApps/PickerKotlinSampleCore.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/filekit-core/src/iosMain/kotlin/io/github/vinceglb/filekit/core/FileKit.ios.kt b/filekit-core/src/iosMain/kotlin/io/github/vinceglb/filekit/core/FileKit.ios.kt index 4d18e1a..f526adf 100644 --- a/filekit-core/src/iosMain/kotlin/io/github/vinceglb/filekit/core/FileKit.ios.kt +++ b/filekit-core/src/iosMain/kotlin/io/github/vinceglb/filekit/core/FileKit.ios.kt @@ -40,7 +40,7 @@ public actual object FileKit { // Use PHPickerViewController for images and videos is PickerType.Image, is PickerType.Video -> callPhPicker( - isMultipleMode = mode is PickerMode.Multiple, + mode = mode, type = type )?.map { PlatformFile(it) }?.let { mode.parseResult(it) } @@ -153,8 +153,8 @@ public actual object FileKit { ) } - private suspend fun callPhPicker( - isMultipleMode: Boolean, + private suspend fun callPhPicker( + mode: PickerMode, type: PickerType, ): List? { val pickerResults: List = suspendCoroutine { continuation -> @@ -167,7 +167,10 @@ public actual object FileKit { val configuration = PHPickerConfiguration(sharedPhotoLibrary()) // Number of medias to select - configuration.selectionLimit = if (isMultipleMode) 0 else 1 + configuration.selectionLimit = when (mode) { + is PickerMode.Multiple -> mode.maxItems.toLong() + PickerMode.Single -> 1 + } // Filter configuration configuration.filter = when (type) { diff --git a/samples/sample-core/appleApps/PickerKotlinSampleCore.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/samples/sample-core/appleApps/PickerKotlinSampleCore.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index fa3bd43..0000000 --- a/samples/sample-core/appleApps/PickerKotlinSampleCore.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,24 +0,0 @@ -{ - "originHash" : "3eff510532cbd886b428b7d14e1f4ed286820714fa9ef4fbe832fd481f0742d1", - "pins" : [ - { - "identity" : "kmm-viewmodel", - "kind" : "remoteSourceControl", - "location" : "https://github.com/rickclephas/KMM-ViewModel.git", - "state" : { - "revision" : "677d657ed678fadf50efac37b5d177170d6873a8", - "version" : "1.0.0-ALPHA-20" - } - }, - { - "identity" : "kmp-observableviewmodel", - "kind" : "remoteSourceControl", - "location" : "https://github.com/rickclephas/KMP-ObservableViewModel.git", - "state" : { - "branch" : "master", - "revision" : "a94525baa10dfb5870d16b9a7155e0854deb705c" - } - } - ], - "version" : 3 -} From 1db47379bc701ca53be31759782a52f1c8757c2e Mon Sep 17 00:00:00 2001 From: santi Date: Thu, 11 Jul 2024 10:31:44 +0200 Subject: [PATCH 4/8] Add comment explaining what maxItems does and when it is used. --- .../kotlin/io/github/vinceglb/filekit/core/PickerMode.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/core/PickerMode.kt b/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/core/PickerMode.kt index f641b0b..2baece2 100644 --- a/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/core/PickerMode.kt +++ b/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/core/PickerMode.kt @@ -9,6 +9,10 @@ public sealed class PickerMode { } } + /** + * @property maxItems sets the limit of how many items can be selected. NOTE: This is only + * supported by Android / iOS and only when picking media files, not any kind of file. + */ public data class Multiple(val maxItems: Int = Int.MAX_VALUE) : PickerMode() { override fun parseResult(value: PlatformFiles?): PlatformFiles? { return value?.takeIf { it.isNotEmpty() } From 739f2466b49916c0966efd1caff3d5b8cbcdbf10 Mon Sep 17 00:00:00 2001 From: Vincent Guillebaud Date: Sat, 13 Jul 2024 01:49:23 +0200 Subject: [PATCH 5/8] =?UTF-8?q?=F0=9F=99=88=20Add=20Package.resolved=20to?= =?UTF-8?q?=20.gitignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 1e9eed6..0dfc64b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ xcuserdata .kotlin .idea/* !.idea/runConfigurations/ +Package.resolved \ No newline at end of file From 13c5597fcc909c04dc70a8f9e125c020fad556c9 Mon Sep 17 00:00:00 2001 From: Vincent Guillebaud Date: Sat, 13 Jul 2024 01:53:12 +0200 Subject: [PATCH 6/8] =?UTF-8?q?=E2=9C=A8=20Multiple.maxItems=20property=20?= =?UTF-8?q?is=20now=20nullable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It must be contained between 2 and 50 to prevent crash on Android --- .../vinceglb/filekit/core/FileKit.android.kt | 64 ++++++++----------- .../vinceglb/filekit/core/PickerMode.kt | 6 +- .../vinceglb/filekit/core/FileKit.ios.kt | 2 +- 3 files changed, 32 insertions(+), 40 deletions(-) diff --git a/filekit-core/src/androidMain/kotlin/io/github/vinceglb/filekit/core/FileKit.android.kt b/filekit-core/src/androidMain/kotlin/io/github/vinceglb/filekit/core/FileKit.android.kt index 8fc083a..8137acb 100644 --- a/filekit-core/src/androidMain/kotlin/io/github/vinceglb/filekit/core/FileKit.android.kt +++ b/filekit-core/src/androidMain/kotlin/io/github/vinceglb/filekit/core/FileKit.android.kt @@ -4,10 +4,10 @@ import android.content.Context import android.net.Uri import android.webkit.MimeTypeMap import androidx.activity.ComponentActivity -import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultRegistry import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.result.contract.ActivityResultContracts.PickMultipleVisualMedia import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.ImageAndVideo import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.ImageOnly @@ -57,28 +57,23 @@ public actual object FileKit { else -> throw IllegalArgumentException("Unsupported type: $type") } - fun singleMediaLauncher(): ActivityResultLauncher { - val contract = PickVisualMedia() - return registry.register(key, contract) { uri -> - val result = uri?.let { listOf(PlatformFile(it, context)) } - continuation.resume(result) - } - } - val launcher = when (mode) { is PickerMode.Single -> { - singleMediaLauncher() + val contract = PickVisualMedia() + registry.register(key, contract) { uri -> + val result = uri?.let { listOf(PlatformFile(it, context)) } + continuation.resume(result) + } } is PickerMode.Multiple -> { - if (mode.maxItems == 1) singleMediaLauncher() - else { - val contract = - ActivityResultContracts.PickMultipleVisualMedia(mode.maxItems) - registry.register(key, contract) { uri -> - val result = uri.map { PlatformFile(it, context) } - continuation.resume(result) - } + val contract = when { + mode.maxItems != null -> PickMultipleVisualMedia(mode.maxItems) + else -> PickMultipleVisualMedia() + } + registry.register(key, contract) { uri -> + val result = uri.map { PlatformFile(it, context) } + continuation.resume(result) } } } @@ -86,32 +81,25 @@ public actual object FileKit { } is PickerType.File -> { - fun openSingleDocument() { - val contract = ActivityResultContracts.OpenDocument() - val launcher = registry.register(key, contract) { uri -> - val result = uri?.let { listOf(PlatformFile(it, context)) } - continuation.resume(result) - } - launcher.launch(getMimeTypes(type.extensions)) - } when (mode) { is PickerMode.Single -> { - openSingleDocument() + val contract = ActivityResultContracts.OpenDocument() + val launcher = registry.register(key, contract) { uri -> + val result = uri?.let { listOf(PlatformFile(it, context)) } + continuation.resume(result) + } + launcher.launch(getMimeTypes(type.extensions)) } is PickerMode.Multiple -> { - if (mode.maxItems == 1) { - openSingleDocument() - } else { - // TODO there might be a way to limit the amount of documents, but - // I haven't found it yet. - val contract = ActivityResultContracts.OpenMultipleDocuments() - val launcher = registry.register(key, contract) { uris -> - val result = uris.map { PlatformFile(it, context) } - continuation.resume(result) - } - launcher.launch(getMimeTypes(type.extensions)) + // TODO there might be a way to limit the amount of documents, but + // I haven't found it yet. + val contract = ActivityResultContracts.OpenMultipleDocuments() + val launcher = registry.register(key, contract) { uris -> + val result = uris.map { PlatformFile(it, context) } + continuation.resume(result) } + launcher.launch(getMimeTypes(type.extensions)) } } } diff --git a/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/core/PickerMode.kt b/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/core/PickerMode.kt index 2baece2..3530330 100644 --- a/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/core/PickerMode.kt +++ b/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/core/PickerMode.kt @@ -13,7 +13,11 @@ public sealed class PickerMode { * @property maxItems sets the limit of how many items can be selected. NOTE: This is only * supported by Android / iOS and only when picking media files, not any kind of file. */ - public data class Multiple(val maxItems: Int = Int.MAX_VALUE) : PickerMode() { + public data class Multiple(val maxItems: Int? = null) : PickerMode() { + init { + require(maxItems == null || maxItems in 2..50) { "maxItems must be contained between 2 <= maxItems <= 50 but current value is $maxItems" } + } + override fun parseResult(value: PlatformFiles?): PlatformFiles? { return value?.takeIf { it.isNotEmpty() } } diff --git a/filekit-core/src/iosMain/kotlin/io/github/vinceglb/filekit/core/FileKit.ios.kt b/filekit-core/src/iosMain/kotlin/io/github/vinceglb/filekit/core/FileKit.ios.kt index ba59755..dc2ae63 100644 --- a/filekit-core/src/iosMain/kotlin/io/github/vinceglb/filekit/core/FileKit.ios.kt +++ b/filekit-core/src/iosMain/kotlin/io/github/vinceglb/filekit/core/FileKit.ios.kt @@ -178,7 +178,7 @@ public actual object FileKit { // Number of medias to select configuration.selectionLimit = when (mode) { - is PickerMode.Multiple -> mode.maxItems.toLong() + is PickerMode.Multiple -> mode.maxItems?.toLong() ?: 0 PickerMode.Single -> 1 } From ec05209c40db9eb896ece38fd9ce9343e52278b3 Mon Sep 17 00:00:00 2001 From: Vincent Guillebaud Date: Sat, 13 Jul 2024 01:54:36 +0200 Subject: [PATCH 7/8] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Adapt=20samples=20to?= =?UTF-8?q?=20the=20new=20PickerMode.Multiple()=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sample-compose/composeApp/src/commonMain/kotlin/App.kt | 2 +- .../kotlin/io/github/vinceglb/sample/core/MainViewModel.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/sample-compose/composeApp/src/commonMain/kotlin/App.kt b/samples/sample-compose/composeApp/src/commonMain/kotlin/App.kt index a28fa5f..9c6ba4c 100644 --- a/samples/sample-compose/composeApp/src/commonMain/kotlin/App.kt +++ b/samples/sample-compose/composeApp/src/commonMain/kotlin/App.kt @@ -54,7 +54,7 @@ private fun SampleApp() { val multipleFilesPicker = rememberFilePickerLauncher( type = PickerType.Image, - mode = PickerMode.Multiple(), + mode = PickerMode.Multiple(maxItems = 4), title = "Multiple files picker", initialDirectory = directory?.path, onResult = { file -> file?.let { files += it } } diff --git a/samples/sample-core/shared/src/commonMain/kotlin/io/github/vinceglb/sample/core/MainViewModel.kt b/samples/sample-core/shared/src/commonMain/kotlin/io/github/vinceglb/sample/core/MainViewModel.kt index cc1b0d8..0bd807e 100644 --- a/samples/sample-core/shared/src/commonMain/kotlin/io/github/vinceglb/sample/core/MainViewModel.kt +++ b/samples/sample-core/shared/src/commonMain/kotlin/io/github/vinceglb/sample/core/MainViewModel.kt @@ -38,7 +38,7 @@ class MainViewModel : ViewModel() { // Pick files val files = FileKit.pickFile( type = PickerType.Image, - mode = PickerMode.Multiple + mode = PickerMode.Multiple() ) // Add files to the state @@ -66,7 +66,7 @@ class MainViewModel : ViewModel() { // Pick files val files = FileKit.pickFile( type = PickerType.File(extensions = listOf("png")), - mode = PickerMode.Multiple + mode = PickerMode.Multiple() ) // Add files to the state From 65e356f71fe1eaac622edfcaf4b34eb88625d7fb Mon Sep 17 00:00:00 2001 From: Vincent Guillebaud Date: Wed, 17 Jul 2024 18:02:27 +0200 Subject: [PATCH 8/8] =?UTF-8?q?=E2=9C=A8=20Update=20maxItems=20range=20to?= =?UTF-8?q?=201=20to=2050?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../io/github/vinceglb/filekit/core/FileKit.android.kt | 8 +++++--- .../kotlin/io/github/vinceglb/filekit/core/PickerMode.kt | 4 +++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/filekit-core/src/androidMain/kotlin/io/github/vinceglb/filekit/core/FileKit.android.kt b/filekit-core/src/androidMain/kotlin/io/github/vinceglb/filekit/core/FileKit.android.kt index 8137acb..94659a3 100644 --- a/filekit-core/src/androidMain/kotlin/io/github/vinceglb/filekit/core/FileKit.android.kt +++ b/filekit-core/src/androidMain/kotlin/io/github/vinceglb/filekit/core/FileKit.android.kt @@ -57,8 +57,8 @@ public actual object FileKit { else -> throw IllegalArgumentException("Unsupported type: $type") } - val launcher = when (mode) { - is PickerMode.Single -> { + val launcher = when { + mode is PickerMode.Single || mode is PickerMode.Multiple && mode.maxItems == 1 -> { val contract = PickVisualMedia() registry.register(key, contract) { uri -> val result = uri?.let { listOf(PlatformFile(it, context)) } @@ -66,7 +66,7 @@ public actual object FileKit { } } - is PickerMode.Multiple -> { + mode is PickerMode.Multiple -> { val contract = when { mode.maxItems != null -> PickMultipleVisualMedia(mode.maxItems) else -> PickMultipleVisualMedia() @@ -76,6 +76,8 @@ public actual object FileKit { continuation.resume(result) } } + + else -> throw IllegalArgumentException("Unsupported mode: $mode") } launcher.launch(request) } diff --git a/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/core/PickerMode.kt b/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/core/PickerMode.kt index 3530330..2638f8b 100644 --- a/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/core/PickerMode.kt +++ b/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/core/PickerMode.kt @@ -15,7 +15,9 @@ public sealed class PickerMode { */ public data class Multiple(val maxItems: Int? = null) : PickerMode() { init { - require(maxItems == null || maxItems in 2..50) { "maxItems must be contained between 2 <= maxItems <= 50 but current value is $maxItems" } + require(maxItems == null || maxItems in 1..50) { + "maxItems must be contained between 1 <= maxItems <= 50 but current value is $maxItems" + } } override fun parseResult(value: PlatformFiles?): PlatformFiles? {